End-to-end guide: feature gating, Stripe checkout, billing management, and troubleshooting.
NEXT_PUBLIC_PAYWALLOS_API_KEY=<your API key> NEXT_PUBLIC_PAYWALLOS_APP_ID=<your App UUID>
Use a provider to manage tier state and expose testVerb(verb), createCheckout(), and createPortalSession().
PaywallOS enables CORS on all endpoints. Call directly from the browser. If you hit issues, use a server-side proxy to forward requests to https://paywallos.openverb.org.
checkVerb(verb) or verb="export_data" attributes/api/stripe/checkout{
"appId": "your-app-uuid",
"planId": "tier-uuid",
"userId": "your-user-id",
"successUrl": "https://yourapp.com/success?session_id={CHECKOUT_SESSION_ID}",
"cancelUrl": "https://yourapp.com/plans"
}planId is the tier UUID from the dashboard. Include {CHECKOUT_SESSION_ID} in successUrl for post-checkout polling.
Stripe webhooks can take a few seconds. Poll /api/openverb/user-tier every 2–3s until tier is pro (or ~60s max).
async function pollUntilPro(appId, userId, maxAttempts = 20) {
for (let i = 0; i < maxAttempts; i++) {
const res = await fetch(
`https://paywallos.openverb.org/api/openverb/user-tier?appId=${appId}&userId=${userId}`,
{ headers: { Authorization: `Bearer ${apiKey}` } }
)
const { tier } = await res.json()
if (tier === 'pro') return true
await new Promise(r => setTimeout(r, 3000))
}
return false
}/api/stripe/customer-portal{
"appId": "your-app-uuid",
"userId": "your-user-id",
"returnUrl": "https://yourapp.com/account"
}Redirect the user to the returned url. Stripe hosts the portal—cancel at period end, update payment, view invoices.
PaywallOS receives webhooks to sync subscriptions. Configure in Stripe Dashboard:
https://paywallos.openverb.org/api/stripe/webhooks (note: webhooks plural)Run 07-subscriptions-plan-id-flexible.sql if your DB has an FK on subscriptions.plan_id. This allows tier UUIDs and adds cancel_at_period_end.
| Issue | Solution |
|---|---|
| user-tier returns "free" after checkout | Configure webhook to .../webhooks, set STRIPE_WEBHOOK_SECRET, run migration 07 |
| 401 Invalid API key | Use Public Key from Dashboard → Apps → API Keys |
| Plan not found at checkout | Use tier UUID as planId, not slug. Link Stripe Price to tier. |
| CORS errors | PaywallOS supports CORS. Use server proxy if needed. |
| Always denied | Verb name must match exactly. Check tier mapping. |