← Back to blog
BillingPublished 2026-03-21· 8 min read

Next.js Subscription Billing: The Webhook Contract That Actually Works

By make1day · Updated 2026-11-07

Most subscription-billing tutorials stop at the happy path: create a checkout session, redirect, done. Production billing lives in the parts they skip.

The webhook is the source of truth

A browser redirect lies. A user closes the tab, a provider retries, a test fires twice. Your database must be the single source of truth, updated only by a verified webhook.

The verification contract is worth stating precisely, because getting it backwards is a security bug:

  • Missing, invalid or malformed signature → throw, and the route answers 400.
  • Validly signed event that needs no handling → return an empty list, and the route answers 200 with an ignored flag.
  • Already-seen event id → skip before writing anything.

Every adapter in this repo follows that contract. An earlier version returned an empty list on verification failure, which meant a forged request was acknowledged with 200. Do not make that trade.

Gate features on the server, not the UI

Never trust the client. Read the plan from the session or database on every protected route. If the plan is inactive, the AI endpoint returns 402 — even if someone edits the DOM in devtools.

Budget and quotas are separate

Subscription status is not the same as usage limits. The AI endpoint has its own per-user daily quota (AI_MESSAGES_PER_DAY, default 50) backed by either a Supabase RPC or the local SQLite ai_usage table. Exhausting it returns 429 with an X-AI-Quota-Reset header and a truthful Retry-After in seconds, not a hard-coded 86400.

Test it offline

You do not need six sandbox accounts to validate the contract. The repository ships an offline signature probe (npm run probe:webhook) that sends valid, missing, corrupted and malformed bodies to a running server and asserts the status codes. Sandbox end-to-end runs are still required before launch, but the security invariant can be checked in CI. See docs/payment-testing.md for the four-layer strategy.

FAQ

How do I make a webhook idempotent?+

Record the provider event id in a webhook_events table and check it before writing a plan or license. Signature verification happens first; only verified, unhandled events reach the deduplication step.

What is the difference between 402 and 429 here?+

402 means no download entitlement (no valid license, subscription or email claim). 429 means the daily AI message quota is exhausted. Preview-quota exhaustion is a separate 402 with a preview_limit_reached code.

Can I test all six gateways without sandbox accounts?+

You can test the shared verification contract offline with npm run probe:webhook. Each gateway's real checkout flow still needs its own sandbox credentials before launch; docs/payment-testing.md lists the required environment variables.

All frequently asked questions · Markdown version (for AI agents)

Next.js Subscription Billing: The Webhook Contract That Actually Works · make1day