Next.js payment webhook entitlement test matrix
Before a webhook grants paid access, prove every provider event maps to one boring decision: grant, renew, pause, revoke, ignore, or quarantine.
Why entitlement tests fail late
Most checkout bugs are not just signature bugs. The route verifies a valid event, then grants access on the wrong status, runs fulfillment twice after duplicate replay, revokes too early after cancellation, or forgets the refund path. A matrix makes those product decisions visible before live checkout traffic arrives.
Minimum Next.js matrix
| Event class | Trusted fields | Expected entitlement decision | Regression test |
|---|---|---|---|
| Paid checkout | event_name, provider object ID, paid status, currency, product or price ID, customer ID |
Grant access once and attach the provider object to the user record. | Signed fixture returns 2xx and writes exactly one entitlement. |
| Renewal or payment success | Subscription ID, invoice/payment ID, current period, paid status | Extend access once for the paid period without recreating the user. | Duplicate replay keeps one extension for the same invoice/payment. |
| Cancellation | Subscription ID, cancellation mode, period end, provider status | Schedule revocation at period end, or revoke immediately only when the provider semantics require it. | Cancellation fixture does not erase paid access before the expected date. |
| Failed payment | Subscription ID, invoice/payment ID, attempt status, grace period policy | Mark billing risk, notify support or the user, and keep access according to the grace policy. | Failed payment fixture never grants new paid access. |
| Refund or chargeback | Payment ID, refund ID, amount, reason, original order or invoice | Record rollback evidence, revoke or flag access according to refund policy, and avoid duplicate support actions. | Refund replay produces one rollback record and one access decision. |
| Unknown event | Provider event ID, type, raw payload hash | Return a safe 2xx or quarantine according to policy, but never grant access by default. | Unknown signed fixture writes no entitlement. |
Handler order
Keep the webhook route deterministic. Read the exact raw body, verify the provider signature, parse JSON, build an idempotency key, validate provider object fields, then evaluate the entitlement matrix.
export async function POST(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get("x-signature");
await verifyProviderSignature(rawBody, signature);
const event = JSON.parse(rawBody);
const key = buildWebhookIdempotencyKey(event);
await runOnce(key, async () => {
const decision = decideEntitlement(event);
await applyEntitlementDecision(decision);
});
return new Response("ok", { status: 200 });
}
Provider examples
Start with order_created, subscription payment success, cancellation, license key, and refund flows. Keep x-signature checks before paid-state decisions.
Cover checkout.session.completed, invoice.paid, customer.subscription.deleted, and refund or dispute events. Match entitlements to price IDs, not UI labels.
Cover transaction.completed, subscription lifecycle events, pause/cancel states, and adjustments. Verify Paddle-Signature against request.text().
Run valid signature, mutated body, duplicate replay, unpaid status, refund rollback, and unknown event tests before exposing a public buy link.
FAQ
What should an entitlement test matrix cover for payment webhooks?
Cover paid checkout, renewal, cancellation, refund, failed payment, duplicate replay, unknown events, and the exact access decision expected for each event.
Where should entitlement logic run in a Next.js payment webhook?
Run entitlement decisions only after raw-body signature verification, idempotency checks, provider object validation, and paid-state checks have passed.
Use the free tools before buying the Pro Kit
The browser-only tools generate an entitlement matrix and launch plan for free. The CNY 69 Pro Kit is for teams that want copy-ready fixtures, tests, and review evidence in one package.