A complete guide to connecting your external application to PaywallOS. All endpoints support CORS for browser-based calls.
PaywallOS exposes a REST API at https://paywallos.openverb.org. You need:
All endpoints (except health) require: Authorization: Bearer YOUR_API_KEY
/api/openverb/healthfetch('https://paywallos.openverb.org/api/openverb/health'){
"status": "ok",
"message": "PaywallOS Service is active"
}/api/openverb/check-verbappId — Your app UUIDuserId — Your user's unique IDverb — The action (e.g. export_data)tier — User's tier slug (e.g. free, pro), not the tier UUID. Optional, defaults to free.fetch(
'https://paywallos.openverb.org/api/openverb/check-verb?appId=YOUR_APP_ID&userId=user_123&verb=export_data&tier=free',
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } }
){
"allowed": true,
"verb": "export_data",
"tier": "pro",
"message": "Access granted"
}{
"allowed": false,
"verb": "export_data",
"tier": "free",
"requiredTier": "pro",
"message": "This feature requires the pro plan"
}/api/openverb/check{
"verbId": "export_data",
"actor": { "type": "user", "id": "user_123" },
"context": { "tenantId": "YOUR_APP_ID", "planId": "pro" }
}tenantId = App ID. planId = user's tier slug.
{
"ok": true,
"receipt": {
"executionId": "...",
"verbId": "export_data",
"status": "ok",
"actorId": "user_123",
"tenantId": "YOUR_APP_ID"
}
}{
"denied": true,
"reason": {
"code": "tier_required",
"message": "Upgrade to Pro"
},
"upsell": {
"suggestedPlanId": "pro",
"cta": "Upgrade Now"
}
}/api/stripe/checkoutfetch('https://paywallos.openverb.org/api/stripe/checkout', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
appId: 'YOUR_APP_ID',
planId: 'tier-uuid', // Tier UUID from dashboard, not slug
userId: 'user_123',
successUrl: 'https://yourapp.com/success?session_id={CHECKOUT_SESSION_ID}',
cancelUrl: 'https://yourapp.com/plans'
})
}){ "url": "https://checkout.stripe.com/...", "sessionId": "cs_..." }Redirect the user to url. Include {CHECKOUT_SESSION_ID} in successUrl so you can poll for tier updates after checkout.
/api/stripe/customer-portalfetch('https://paywallos.openverb.org/api/stripe/customer-portal', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
appId: 'YOUR_APP_ID',
userId: 'user_123',
returnUrl: 'https://yourapp.com/account'
})
}){ "url": "https://billing.stripe.com/..." }Redirect the user to url. Stripe hosts the portal—users can cancel at period end, update payment, view invoices.
/api/openverb/user-tierappId — Your app UUIDuserId — Your user's external ID (must exist as end_user in PaywallOS)fetch(
'https://paywallos.openverb.org/api/openverb/user-tier?appId=YOUR_APP_ID&userId=user_123',
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } }
){
"userId": "user_123",
"tier": "pro"
}Returns tier: "free" if user has no active subscription.
All OpenVerb API endpoints support CORS for browser-based requests:
Access-Control-Allow-Origin: *Access-Control-Allow-Headers: Authorization, Content-TypeYou can call these endpoints directly from your frontend (e.g. fetch from React). Keep your API key in an env var (e.g. NEXT_PUBLIC_PAYWALLOS_API_KEY). The public key is safe for client-side use.
/api/openverb/health to verify connectivity (optional)/api/openverb/user-tier to get their tier, or use your own tier if you manage itinitPaywallOS(apiKey, appId, userId, userTier, { baseUrl: 'https://paywallos.openverb.org' })verb="export_data" to buttons/sections that need protection/api/openverb/check-verb directly for programmatic checks/api/stripe/checkout → redirect to url. Use {CHECKOUT_SESSION_ID} in successUrl./api/openverb/user-tier every 2–3s until tier is pro (webhooks can delay a few seconds)/api/stripe/customer-portal → redirect to urlPaywallOS receives Stripe webhooks to keep subscriptions in sync. If you run PaywallOS, configure:
https://paywallos.openverb.org/api/stripe/webhooks (note: webhooks plural)checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_succeeded, invoice.payment_failedSTRIPE_WEBHOOK_SECRET in VercelIf user-tier returns free after checkout, the webhook may not be configured. Run migration 07-subscriptions-plan-id-flexible.sql if needed.