All Docs
FeaturesMaking Tax DigitalUpdated March 11, 2026

Stripe Webhook & Inngest Error Handling

Stripe Webhook & Inngest Error Handling

Release: v1.0.397

The Problem

The Stripe webhook endpoint (/api/webhooks/stripe) previously only wrapped the constructEvent() signature verification step in a try/catch. Everything that followed — retrieving the customer record, dispatching events via inngest.send(), and constructing the HTTP response — ran outside any error boundary.

This created two failure modes:

  1. Unstructured 500 responses. If Inngest was unavailable or the Stripe payload had an unexpected shape, Next.js would throw a runtime exception and return a raw 500 with no structured body. Stripe treats any non-2xx response as a delivery failure.
  2. Webhook retries and duplicate events. Because Stripe retries failed deliveries, an infrastructure blip (e.g. a momentary Inngest outage) could trigger the same webhook multiple times, risking duplicate downstream effects such as double-counted payments or repeated subscription activations.

The /api/inngest route similarly used the Inngest serve() adapter with no outer error wrapper, leaving adapter-level failures unhandled.


The Fix

Outer try/catch on the full POST handler

The entire POST handler body in src/app/api/webhooks/stripe/route.ts is now wrapped in a try/catch:

export async function POST(req: Request) {
  try {
    // signature verification, customer retrieval, inngest.send(), ...
    return new Response('OK', { status: 200 });
  } catch (err) {
    return new Response('Internal error', { status: 500 });
  }
}

This guarantees that every code path through the handler returns a well-formed HTTP response.

Isolated try/catch for inngest.send()

The inngest.send() call uses its own nested try/catch with different semantics: on failure it still returns HTTP 200 to Stripe while reporting the error to Sentry:

try {
  await inngest.send({ name: 'stripe/event', data: event });
} catch (err) {
  captureError(err); // reports to Sentry
  // intentionally returns 200 — do NOT let Stripe retry
  return new Response('OK', { status: 200 });
}

Returning 200 here is intentional. Inngest failures are an internal concern; they should be logged and investigated, not surfaced to Stripe as a reason to re-deliver the webhook.

Inngest route wrapper

The src/app/api/inngest/route.ts adapter now has an outer error wrapper to handle unexpected failures at the serve() adapter level, preventing unhandled exceptions from escaping to the runtime.


Behaviour Summary

Failure scenarioBefore v1.0.397After v1.0.397
Inngest unavailable during inngest.send()Raw 500 → Stripe retries → potential duplicatesReturns 200, logs to Sentry, no retry
Unexpected Stripe payload shapeRaw 500 → Stripe retriesReturns 500 (structured), Stripe may retry once
Customer retrieval throwsUnhandled runtime crashCaught by outer handler, returns 500
Inngest adapter errorUnhandled runtime crashCaught by outer wrapper

Monitoring

Inngest send failures are forwarded to Sentry via captureError(). Look for errors tagged with the Stripe webhook source in your Sentry project to identify Inngest delivery problems without needing to cross-reference Stripe's webhook dashboard.