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
| Endpoint | Description |
|---|---|
POST /billing/checkout | Creates Stripe Checkout session for plan upgrades |
GET /billing/portal | Generates customer billing portal session |
POST /billing/webhook | Handles 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.
Related Scripts
scripts/testing/smoke_test_full_auth_billing.pyscripts/testing/run_smoke_tests.py
Errors & Recovery
| Symptom | Fix |
|---|---|
StripeSignatureVerificationError | Ensure webhook secret matches Stripe dashboard |
| Checkout returns 401 | Verify Clerk token → Stripe customer mapping |
| Portal returns 404 | Confirm customer has active subscription |
| Invoice webhook fails | Stripe retries automatically; ensure idempotent storage |
Keep this document in sync with deployment scripts and product pricing updates.