Skip to main content

Stripe Webhook Flow

Relay integrates Stripe Checkout and Billing Portal to manage subscriptions, usage metering, and payouts. This guide walks through the HTTP flows and provides code samples for server-side handlers.

Endpoints

EndpointDescription
POST /billing/checkoutCreates Stripe Checkout session for plan upgrades
GET /billing/portalGenerates customer billing portal session
POST /billing/webhookHandles Stripe events (checkout completion, subscription updates, invoice payments)

Checkout Flow

@app.post('/billing/checkout')
async def create_checkout_session(user: ClerkUser):
session = stripe.checkout.Session.create(
customer=user.stripe_customer_id,
mode='subscription',
line_items=[{'price': PLAN_PRICE_ID, 'quantity': 1}],
success_url='https://app.deployrelay.app/billing/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url='https://app.deployrelay.app/billing/cancelled',
)
return {'checkout_url': session.url}

Frontend snippet:

const response = await fetch('/billing/checkout', { method: 'POST' });
const { checkout_url } = await response.json();
window.location.href = checkout_url;

Billing Portal

@app.get('/billing/portal')
async def get_billing_portal(user: ClerkUser):
session = stripe.billing_portal.Session.create(
customer=user.stripe_customer_id,
return_url='https://app.deployrelay.app/settings/billing'
)
return {'portal_url': session.url}

Webhook Handler

@app.post('/billing/webhook')
async def stripe_webhook(request: Request):
payload = await request.body()
sig = request.headers.get('Stripe-Signature', '')
event = stripe.Webhook.construct_event(payload, sig, STRIPE_WEBHOOK_SECRET)

if event['type'] == 'checkout.session.completed':
handle_checkout_completed(event['data']['object'])
elif event['type'] == 'customer.subscription.updated':
handle_subscription_update(event['data']['object'])
elif event['type'] == 'invoice.paid':
handle_invoice_paid(event['data']['object'])
else:
logger.info('Unhandled event %s', event['type'])

return {'ok': True}

Testing Steps

# Checkout smoke
test_checkout() {
curl -X POST https://api.deployrelay.dev/billing/checkout \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json'
}

# Billing portal
curl -X GET https://api.deployrelay.dev/billing/portal \
-H "Authorization: Bearer $TOKEN"

Use Stripe CLI or dashboard to replay events against /billing/webhook in staging.

Event Handling Notes

  • Store Stripe customer IDs on the Relay account model.
  • Map subscriptions to Relay plans; update entitlements after webhook handling.
  • Record invoices and payment status for analytics.

Security & Reliability

  • Verify webhook signatures (STRIPE_WEBHOOK_SECRET).
  • Log event IDs for audit trails.
  • Idempotently handle retries using Stripe event IDs.
  • scripts/testing/smoke_test_full_auth_billing.py
  • scripts/testing/run_smoke_tests.py

Errors & Recovery

SymptomFix
StripeSignatureVerificationErrorEnsure webhook secret matches Stripe dashboard
Checkout returns 401Verify Clerk token → Stripe customer mapping
Portal returns 404Confirm customer has active subscription
Invoice webhook failsStripe retries automatically; ensure idempotent storage

Keep this document in sync with deployment scripts and product pricing updates.