Last updated: August 10, 2026 · Last reviewed against Stripe docs: May 17, 2026
How to handle Stripe subscription webhooks
A Stripe subscription webhook is an HTTPS POST request Stripe sends to your server when a subscription is created, renewed, updated, or canceled. The request carries a signed JSON payload your handler verifies, deduplicates, and uses to update subscription state in your app.
This doc covers what a correct handler has to do with that webhook. The rules apply whether you build the handler yourself or use a service that handles webhooks for you.
Before configuring the integration, review how subscription events affect access across payment providers.
What Stripe sends to your webhook endpoint
The webhook payload is a JSON Event object with an event ID, an event type, a creation timestamp, and a data.object field holding the Stripe resource the event is about. For subscription events, data.object is a Subscription.
Stripe signs the request and includes a Stripe-Signature header. A valid signature is the only proof that Stripe sent the event.
Do not trust a Stripe webhook until you verify the signature with the raw request body and your endpoint signing secret.
What a correct Stripe webhook handler does
In short, verify the signature against the raw body, deduplicate by event ID, narrow to the event types the app uses, and upsert one record per subscription.
- Receive the webhook at an endpoint dedicated to Stripe.
- Read the raw request body before any framework parsing.
- Require the
Stripe-Signatureheader. - Verify the signature with the webhook endpoint signing secret from Stripe.
- Accept only the event types the integration actually needs.
- Deduplicate by Stripe event ID before processing.
- Link the Stripe subscription to a user in your app via subscription metadata.
- Upsert one subscription record so repeated events do not create duplicate rows.
Why the raw request body matters
Stripe signs the bytes of the request body as sent. Stripe's webhook docs require the unmodified body for verification. If a framework deserializes JSON, re-serializes it, normalizes whitespace, or rewrites property order before verification runs, the signature check fails even when Stripe really sent the event.
Capture the raw body first, verify the signature, then parse the JSON.
Narrowing to the events you actually use
Stripe lets you configure each webhook endpoint to receive only the event types your integration cares about. Subscribing to every event type adds noise, latency, and more ways for the handler to break.
The core subscription events are customer.subscription.created, customer.subscription.updated, and customer.subscription.deleted. Stripe emits a subscription.updated event when status moves to past_due or unpaid, so these cover the common state transitions.
Invoice events (invoice.paid, invoice.payment_failed) become relevant when you need:
- faster failure signals (they fire before the subscription status transitions)
- to know which specific invoice paid
- payment-recovery workflows (retries, customer emails, eventual cancellation)
How Stripe retries failed webhooks
Stripe retries failed live-mode webhook deliveries for up to three days with exponential backoff. In sandbox, Stripe retries three times over a few hours. See Stripe's webhook delivery docs for the current schedule.
Treat Stripe webhook delivery as at-least-once. The same event will arrive more than once.
Why idempotency is required
Because delivery is at-least-once, every Stripe webhook handler must be idempotent. Stripe's docs recommend tracking processed event IDs and skipping any event ID seen before.
Use two layers. First, dedupe by Stripe event ID before processing to skip obvious duplicates. Second, upsert the subscription record (keyed by subscription ID) so out-of-order or partial retries land on the same row.
Why event order is not guaranteed
Stripe does not guarantee webhook event delivery order. An updated event can arrive before the created event for the same subscription, and invoice events can interleave with subscription events for the same billing cycle.
Code that depends on event order will eventually break. Treat each event as a snapshot of the subscription and upsert it. The subscription object's fields (status, current period end, cancellation timestamps) are the source of truth, not the order events arrived in.
Linking a Stripe subscription to a user in your app
Stripe identifies subscriptions with its own IDs. To link them to users in your app, set subscription metadata at creation time with a value that identifies the user. Each event carries that metadata back on its subscription object, so the handler can look up the right user.
If the metadata is missing or empty, the event is valid but can't be linked to a user. See the response codes section below for how to handle that case.
Stripe API versioning and webhooks
Each webhook endpoint can pin its own Stripe API version, independent of the version your platform-wide API requests use. Stripe's versioning guide recommends matching the endpoint's API version to the version your statically typed SDK was generated against.
A mismatch can produce subtle deserialization failures when Stripe renames or restructures fields between versions. Pin the version explicitly rather than relying on the account default.
Live and test mode
Stripe includes livemode on each Event object. A production event has livemode: true. A test-mode event has livemode: false. Check that value before writing subscription state. For a step-by-step setup, see how to test Stripe subscriptions.
When to return 2xx and when to return non-2xx
A 2xx response tells Stripe the event was accepted. A non-2xx response makes Stripe retry the delivery.
On success, return 2xx. The response code on failure depends on whether the failure is permanent or transient. A permanent failure would produce the same result on retry, so return 2xx to stop retries. A transient failure might succeed on retry, so return non-2xx to trigger another retry.
Return 2xx for:
- events outside the set the handler subscribes to
- already-processed duplicate events (matched by event ID)
- events rendered with an incompatible Stripe API version (an existing event keeps the version it was created with)
- events missing the user-mapping metadata (the metadata will never self-correct on retry)
- events whose subscription payload cannot be normalized (the payload will not change on retry)
Return non-2xx for:
- unverifiable signatures (the next retry may carry a valid one)
- database or infrastructure failures during processing
- unhandled exceptions in the handler itself
After three days of retries with exponential backoff, Stripe stops regardless of the response code. Returning 5xx for a permanent failure uses up the entire three days on an event that can never succeed. The retries pile up in your error logs and crowd out the temporary failures that actually matter.
Common Stripe webhook mistakes
- Parsing the body before signature verification. Any body modification breaks the signature check. That includes JSON reparses, middleware that strips whitespace, and content-type coercion.
- Subscribing to every event type. Endpoints should accept only the events the integration actually uses.
- Treating delivery as exactly-once. Duplicate deliveries are routine; handlers must deduplicate by event ID.
- Depending on event order. Order is not guaranteed; rely on the subscription object's state, not the sequence in which events arrive.
- Mixing live and test mode. A handler that does not check
livemodecan apply Stripe test subscriptions to live users, or reject valid test events while debugging. - No user-mapping metadata. A valid event with no way to identify the user in your app becomes an orphaned record.
- Returning non-2xx on permanent failures. This keeps Stripe retrying for three days on events that will never succeed, masking real issues with retry noise.
How this fits across providers
If your app also takes payments through the App Store or Google Play, the same problems (signature verification, idempotency, retries, ordering, user mapping) show up with Apple App Store Server Notifications V2 and Google Play Real-time Developer Notifications. The mechanics differ (signing schemes, retry schedules, event names). See how subscription access works for how apps typically check whether a user is subscribed once the webhooks have been processed.
Frequently asked questions
SubTru is the subscription backend that sits between your payment providers and your app. Point your Stripe webhook endpoint at SubTru, and set subtru_external_user_id on each subscription's metadata so SubTru can link it to a user in your app. SubTru verifies signatures, deduplicates by event ID, handles retries, and normalizes subscription state. Subscription state is available from one access check endpoint over HTTP, and the response shape doesn't change as you add providers. Your Stripe account is still your Stripe account. SubTru receives Stripe's webhook events without ever sitting in front of your account.