Stripe webhook order of events in Next.js
Stripe webhooks should be treated as an eventually consistent event stream. Build the route so checkout, invoice, subscription, refund, and duplicate replay events can arrive in an inconvenient order without granting the wrong access.
Common event order you may see
Do not hardcode this as a guarantee. Use it as a fixture set to prove your Next.js route does not depend on one lucky delivery sequence.
checkout.session.completedcreates or confirms the customer checkout context.customer.subscription.createdorcustomer.subscription.updatedrecords subscription lifecycle state.invoice.paidproves a subscription invoice is paid and can extend access once.invoice.payment_failedrecords a failed renewal or dunning path without deleting paid access too early.charge.refunded,refund.created, or disputes trigger rollback decisions after support review.
Design for out-of-order delivery
Store the provider event ID, object ID, type, raw-body hash, and received timestamp before running billing side effects.
If an invoice arrives before your local subscription row exists, create a pending provider object instead of failing permanently.
Use keys such as stripe:invoice:in_123:grant so duplicate delivery cannot extend the same invoice twice.
A stale event should not downgrade newer paid state. Compare provider timestamps and local decision state before changing access.
Next.js handler shape
Keep raw-body verification separate from event sequencing logic. The route should accept a valid event, store it, then reconcile access based on durable provider objects.
export async function POST(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get("Stripe-Signature");
const event = verifyStripeEvent(rawBody, signature);
await recordProviderEvent({
provider: "stripe",
eventId: event.id,
eventType: event.type,
objectId: event.data.object.id,
rawBodyHash: sha256(rawBody)
});
await reconcileStripeBillingObject(event.data.object);
return new Response("ok", { status: 200 });
}
Sequencing tests to add
invoice.paidarrives beforecheckout.session.completed; the route records pending state and later grants once.customer.subscription.updatedarrives twice with older and newer timestamps; the older event does not overwrite current access.invoice.payment_failedarrives after a paid invoice; it opens a dunning hold path instead of deleting already-paid access.- The same
checkout.session.completedevent replays three times and only one welcome email, license, or access grant is created. - A refund event reaches the route after duplicate paid events and produces one rollback decision with support evidence.
FAQ
Does Stripe guarantee webhook event order?
No. A Stripe integration should tolerate events arriving out of order, duplicated, or retried. The handler should verify signatures, persist provider objects, and make entitlement decisions idempotently.
Which Stripe event should grant access after checkout?
Use the event that proves the product is paid for your business model. For one-time checkout this may be checkout.session.completed plus paid status checks. For subscriptions, invoice.paid often extends access, while subscription events update lifecycle state.
Turn event sequencing into release evidence
The free tools generate Stripe fixtures and test plans for sequencing checks. The CNY 69 Pro Kit is for teams that want copy-ready fixtures, duplicate replay tests, entitlement matrices, and review templates already packaged.