Stripe webhooks look simple until they fail.
The usual symptom is familiar: you follow a guide, create a route, call Stripe’s verification helper, and get a signature error. Checkout works. The redirect works. The webhook does not.
In a SaaS application, that is more than a developer annoyance. Webhooks are how your app learns that a subscription was created, renewed, cancelled, downgraded, refunded, or marked unpaid. If webhook handling is fragile, your billing state is fragile.
This article explains the common failure modes in Next.js App Router and what a production-ready starter should do differently.
Webhooks are not normal JSON API calls
Most API routes receive JSON, parse it, validate it, and continue.
Stripe webhook verification is different. Stripe signs the exact request payload it sends. Your application must verify that signature against the same raw body bytes and the Stripe-Signature header.
If the body is parsed, changed, re-stringified, decoded incorrectly, or read more than once before verification, the signature can fail.
That is why webhook routes should be treated as a special integration boundary, not just another POST endpoint.
Mistake 1: parsing the body before verification
A common mistake is to do this too early:
const payload = await request.json()
Once you parse JSON, you no longer have the exact raw payload Stripe signed. Even if the parsed object contains the same data, the byte representation may not match.
The verification step needs the raw body string or buffer, the signature header, and the endpoint secret. Only after verification should your code trust the event and switch on event.type.
A safer mental model:
- Read raw body.
- Read
Stripe-Signatureheader. - Verify with the endpoint secret.
- Reject if verification fails.
- Process the trusted event.
Do not reverse that order.
Mistake 2: using the wrong endpoint secret
Stripe endpoint secrets are easy to mix up.
Local CLI forwarding, test-mode dashboard endpoints, live dashboard endpoints, preview deployments, and production deployments can all have different secrets. A webhook route can be correct and still fail because the wrong STRIPE_WEBHOOK_SECRET is configured.
A good SaaS starter should document:
- the difference between test and live keys;
- how to obtain the local CLI signing secret;
- where to store preview and production secrets;
- how to verify which endpoint is sending the event;
- how to rotate the secret if it leaks.
If a template provides .env.example without explaining each billing secret, expect confusion.
Mistake 3: assuming success redirects are enough
A user returning to /checkout/success does not prove the subscription state is final. Redirects are useful for user experience, but webhooks should update the durable billing state.
If a starter kit grants access from a success URL alone, it is vulnerable to inconsistent state. The user might close the browser. Payment confirmation might be delayed. The success route might be refreshed. A malicious user might experiment with URLs.
The webhook should be the source of truth for subscription updates. The success page can show a pending state while the webhook catches up.
Mistake 4: not storing processed event IDs
Stripe may retry events. Developers may replay events. Infrastructure may receive duplicates.
If your handler sends emails, creates entitlements, allocates credits, or writes audit logs, duplicate processing can create real customer problems.
A robust handler records the event ID before or during processing and prevents duplicate side effects. At minimum, your data model should make duplicate events harmless.
Example event log fields:
stripe_event_id;type;livemode;status;processed_at;error_message;customer_id;subscription_id.
This table also becomes valuable when support asks why a customer does or does not have access.
Mistake 5: treating all events as equally important
You do not need to handle every Stripe event on day one. You do need to handle the events that affect access.
For a subscription SaaS, that usually includes events around:
- checkout completion;
- subscription creation;
- subscription update;
- subscription deletion or cancellation;
- invoice payment success;
- invoice payment failure;
- trial ending;
- customer updates if you rely on customer metadata.
The exact list depends on your billing model. The key is to document which events are authoritative and why.
Mistake 6: no safe error path
When a webhook fails, the handler should return an appropriate status and log the reason without leaking secrets.
Avoid swallowing errors silently. Also avoid dumping full raw payloads into logs if they contain sensitive data you do not need.
A good route separates:
- verification errors;
- unknown event types;
- temporary processing errors;
- permanent data errors;
- duplicate events.
That separation makes it easier to replay events and diagnose issues.
Mistake 7: no local replay workflow
Developers need a repeatable way to test billing changes locally.
A starter kit should show how to:
- start the app;
- run the Stripe CLI;
- forward events to the local webhook URL;
- complete a test checkout;
- trigger specific test events;
- inspect the database;
- reset local billing state.
Without this, billing becomes guesswork. You only discover errors after deployment, which is exactly when billing bugs become expensive.
What a production-ready webhook route should prove
A reliable SaaS template should prove these behaviours:
- invalid signatures are rejected;
- valid events are accepted;
- duplicates do not duplicate side effects;
- subscription state updates correctly;
- failed payments are represented;
- cancellations are represented;
- errors are logged;
- local and production secrets are documented;
- the buyer can test the route without reading Stripe’s entire documentation set.
That is the practical difference between “Stripe included” and “Stripe wired.”
A simple review checklist
When reviewing a starter kit’s Stripe implementation, ask:
- Does the webhook route read the raw body before parsing JSON?
- Does it use the
Stripe-Signatureheader? - Are endpoint secrets separated by environment?
- Is there an event log or deduplication strategy?
- Are subscription updates handled from webhooks rather than only redirects?
- Are cancellations and failed payments modelled?
- Are unknown event types handled safely?
- Is local Stripe CLI testing documented?
- Is plan access centralised?
- Can support inspect billing history?
Template Empire angle
Template Empire’s billing resources focus on the failure modes that usually appear after launch: raw-body verification, endpoint-secret confusion, retries, idempotency, and access state. A checkout button is easy. Reliable billing is the system around it.