Stripe refund webhook rollback in Next.js
A refund route is not done when it returns 200. It is done when signed Stripe refund events revoke or hold access once, leave support evidence, and survive duplicate replay.
Events to cover
Map the charge to the original checkout session, payment intent, customer, price, and access grant before rollback.
Record refund ID, amount, reason, full or partial state, and the product policy that decides access changes.
Hold or revoke access according to dispute policy, then leave support-safe evidence without exposing customer data.
Return a safe response or quarantine for review, but never grant or revoke access by default.
Next.js App Router order
Read await request.text(), verify Stripe-Signature against the exact raw body, parse the event, build an idempotency key, then run the rollback decision once.
export async function POST(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get("Stripe-Signature");
const event = verifyStripeEvent(rawBody, signature);
if (event.type === "charge.refunded" || event.type === "refund.created") {
await runOnce(`stripe:${event.id}`, async () => {
await applyRefundRollback(event);
});
}
return new Response("ok", { status: 200 });
}
CI gates
- Valid signed
charge.refundedfixture returns 2xx and creates one rollback record. - Mutated raw body fails before entitlement, delivery, license, email, or support side effects.
- Duplicate replay of the same Stripe event runs rollback once.
- Partial refund follows documented policy instead of revoking all access silently.
- Dispute fixture follows hold or revoke policy and records review evidence.
Rollback evidence
Keep the report secret-free: event type, event ID, refund ID, mapped payment object, entitlement decision, duplicate replay result, support action, and CI run link are enough for launch review.
FAQ
Which Stripe refund events should a Next.js webhook test cover?
Start with charge.refunded, refund.created, charge.dispute.created, and the original checkout.session.completed or invoice.paid object used to grant access.
How should a Stripe refund webhook rollback stay idempotent?
Build an idempotency key from the Stripe event or refund object, write one rollback record, and prove duplicate replay cannot revoke, email, or log support actions more than once.
Use the free generators before launch
The Stripe fixture generator and refund rollback report help document the route before public checkout. The CNY 69 Pro Kit is for teams that want copy-ready fixtures, idempotency tests, and review templates in one package.