Webhooks
Stripe webhook integration for Themys: handle subscription events, payments, and idempotent processing.
Overview
Themys uses Stripe webhooks to handle subscription lifecycle events. When a customer subscribes, upgrades, cancels, or a payment succeeds or fails, Stripe sends an event to the webhook endpoint.
Webhook Endpoint
POST /api/webhooks/stripe
This endpoint is called by Stripe, not by your application. It handles signature verification, event parsing, and idempotent processing automatically.
Events Handled
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.paidinvoices tableinvoice.payment_failedpast_dueIdempotency
Stripe may deliver the same event more than once (retries, network issues). Themys handles this with the stripe_events table:
CREATE TABLE stripe_events (
id TEXT PRIMARY KEY, -- Stripe event ID
type TEXT NOT NULL,
livemode BOOLEAN,
created_at TIMESTAMP DEFAULT NOW()
);
Before processing any event:
1. Insert the event ID into stripe_events
2. If the insert succeeds (no duplicate), process the event
3. If the insert fails with a unique constraint violation (23505), skip processing and return 200 OK
This ensures each event is processed exactly once, even if Stripe retries.
Setting Up Webhooks
Stripe Dashboard
1. Go to Stripe Dashboard > Developers > Webhooks
2. Click Add endpoint
3. Enter your endpoint URL:
https://themys.ca/api/webhooks/stripe
4. Select events to listen for:
- checkout.session.completed
- customer.subscription.created
- customer.subscription.updated
- customer.subscription.deleted
- invoice.paid
- invoice.payment_failed
5. Click Add endpoint
6. Copy the Signing secret (whsec_...) for signature verification
Environment Variables
Set the signing secret in your environment:
STRIPE_WEBHOOK_SECRET=whsec_your_signing_secret_here
Webhook Signature Verification
Every webhook request from Stripe includes a signature header. Themys verifies this signature before processing:
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function POST(request) {
const body = await request.text();
const signature = request.headers.get("stripe-signature");
let event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error("Webhook signature verification failed:", err.message);
return new Response("Invalid signature", { status: 400 });
}
// Process the event...
}
Local Development
Use the Stripe CLI to forward webhook events to your local server:
Install the Stripe CLI
# macOS
brew install stripe/stripe-cli/stripe
# Login to your Stripe account
stripe login
Forward Events
stripe listen --forward-to localhost:3000/api/webhooks/stripe
The CLI outputs a webhook signing secret for local testing:
> Ready! Your webhook signing secret is whsec_test_...
Use this secret in your .env.local:
STRIPE_WEBHOOK_SECRET=whsec_test_...
Trigger Test Events
In a separate terminal, trigger specific events:
# Trigger a successful checkout
stripe trigger checkout.session.completed
# Trigger a subscription creation
stripe trigger customer.subscription.created
# Trigger a payment failure
stripe trigger invoice.payment_failed
Error Handling
Common Errors
400 Invalid signatureSTRIPE_WEBHOOK_SECRET env var400 Missing stripe-signature header500 Webhook handler failed500 Webhook secret not configuredSTRIPE_WEBHOOK_SECRET in your environmentImportant: Raw Body
Stripe signature verification requires the raw request body, not the parsed JSON. In Next.js, read the body as text:
// Correct: raw body for signature verification
const body = await request.text();
event = stripe.webhooks.constructEvent(body, signature, secret);
// Wrong: parsed JSON breaks signature verification
const body = await request.json();
Retry Behavior
Stripe retries webhook delivery with exponential backoff:
Themys returns 200 OK for all successfully processed events (including idempotent duplicates). If your endpoint returns a non-2xx status, Stripe will retry.
Security Best Practices
1. Always verify signatures. Never process an unverified event.
2. Use HTTPS. Stripe only sends webhooks to HTTPS endpoints in production.
3. Return 200 quickly. Process events asynchronously if needed; don't keep Stripe waiting.
4. Log all events. Store raw events for debugging (the stripe_events table).
5. Rotate secrets. Periodically regenerate your webhook signing secret.
Was this page helpful?