Provider integrations

How Stripe Webhooks Actually Work: Signing, Verification and Forwarding

Stripe sends signed event deliveries directly to your endpoint; your application must preserve the raw body, verify, acknowledge, and process idempotently.

A Stripe webhook is an HTTP request that Stripe sends to the endpoint you configure. It is not the same as an API response, and it does not automatically pass through the service that made the original Stripe API call. The request is an independent delivery path with its own endpoint secret, version, retry behavior, and application state.

The direct delivery flow

Stripe event
    → HTTPS POST to your configured endpoint
    → your application verifies Stripe-Signature
    → your application records and processes the event
    → your application returns 2xx after durable acceptance

Stripe delivers directly to the callback URL. Your application owns the endpoint, the signing secret, duplicate handling, and business-state transition. A successful API request may cause a later event, but the API response and the webhook are not one transaction.

Manual endpoint setup

For a normal manual setup, open the Stripe Dashboard or Workbench webhook configuration for the correct account and mode. Test mode and live mode are separate provider contexts, so create and inspect separate endpoints rather than reusing a test endpoint when deploying live behavior.

Create a test endpoint first and enter the application's HTTPS callback URL. Select only the events the application actually handles. Subscribing to every event makes testing, storage, and incident review noisier; it also increases the number of payloads the receiver must safely ignore. Save the endpoint, then copy its endpoint-specific signing secret, which begins with whsec_, into protected server-side configuration.

Repeat the setup for the live endpoint when the application is ready. Keep the live and test callback URLs, secrets, event lists, logs, and queues distinguishable. If the callback URL or event list changes, update the endpoint configuration and verify the resulting secret and subscriptions. Changing an endpoint can affect which events are sent and which signing secret the receiver must use.

A Stripe CLI listener is a different setup. The CLI can forward events to a local process and displays a listener signing secret for that forwarding session. That secret is not the same as the secret for a Dashboard or Workbench endpoint. Do not put a CLI whsec_ value into production configuration, and do not assume a Dashboard endpoint will verify events forwarded by a different CLI listener.

Stripe's webhook setup guidance and webhook endpoint API describe the provider configuration. The exact dashboard labels can change, but the separation between endpoint, mode, event selection, callback URL, and endpoint secret remains important.

What Stripe signs

Stripe sends a Stripe-Signature header containing a timestamp and one or more signatures. The signature is calculated over the timestamp and the exact raw request body using the endpoint secret. Verification therefore answers: did Stripe sign these bytes for this endpoint recently?

Use Stripe's maintained library for your language when possible. It receives the raw body, the Stripe-Signature header, and the endpoint secret. It checks the timestamp tolerance and the signed payload. Do not replace that process with a check that merely sees whether the header exists.

Preserve the raw body

Do not parse JSON and then serialize it again before verification. Whitespace, escaping, key order, and the exact bytes matter. Configure the framework so the verification step receives the original request body, then parse the JSON only after the signature has passed.

A concise Node and Express example looks like this:

app.post('/stripe/webhook',
  express.raw({type: 'application/json'}),
  (request, response) => {
    let event;
    try {
      event = stripe.webhooks.constructEvent(
        request.body,
        request.headers['stripe-signature'],
        process.env.STRIPE_WEBHOOK_SECRET
      );
    } catch (error) {
      response.status(400).send('invalid signature');
      return;
    }

    // Persist event.id and the raw or safely serialized event before side effects.
    enqueueVerifiedStripeEvent(event);
    response.sendStatus(200);
  }
);

The framework is incidental. In another framework the equivalent may require disabling JSON middleware for this route or reading the request stream before parsing. The invariant is that verification receives the original bytes, the signature header, and the endpoint-specific secret. Reject invalid signatures before business processing.

Protect the endpoint secret like a server credential. It is not the same as a Stripe API key, and it should not be sent to browser code or written to logs.

Durable acceptance and prompt responses

Return a prompt 2xx only after the event has been accepted durably enough for your recovery model. A good handler verifies the signature, validates basic event fields, records the event ID and payload or a safe reference, commits that record, and queues heavier business work.

Do not keep the Stripe connection open while waiting for an email, shipment, long-running job, or third-party API call. If processing fails after durable acceptance, retry from your own queue or mark the event for repair. A fast acknowledgement does not mean you have finished the business action; it means the event is safely in your system.

Stripe retries failed deliveries. A timeout, connection failure, redirect, or non-2xx response is not the same as successful acceptance. Stripe's retry schedule and duration can differ by mode and provider behavior, so use the current event delivery behavior documentation rather than hard-coding a remembered number of attempts or days into your design. Your application queue still needs its own retry and dead-letter policy.

Duplicate delivery and event identity

Store processed Event IDs in a durable uniqueness constraint before applying a non-repeatable business action. If the same evt_... is delivered again, the receiver should recognize it and return success without charging, granting, provisioning, or sending again.

Event ID deduplication is not the same as object-state deduplication. Stripe may create separate Event objects for the same underlying object transition in circumstances such as separate endpoints, accounts, or provider workflows. A system that only checks whether an event ID is new can still apply equivalent business work twice.

For important transitions, combine the event type with the relevant object ID and your own business key, or reconcile the current provider state before committing a decision. For example, an entitlement consumer might record payment_intent.succeeded plus the PaymentIntent ID, then check the current PaymentIntent before granting access. Use a state machine that treats repeated and out-of-order observations as inputs to reconciliation rather than assuming a perfect event log.

Events are not a transaction log whose arrival order you can safely assume. A later-looking event can arrive first, and a retry can arrive after a newer event. Stripe's guidance on handling duplicate events is the right starting point.

Test mode, live mode, and Connect

Keep test and live endpoints, secrets, queues, and logs distinguishable. Inspect the event's livemode value; do not infer mode from an object ID or event type.

For Connect, events for connected accounts include a top-level account value. Use the authenticated event context to select the correct tenant and, when needed, make follow-up API calls as that connected account. Check both livemode and account before applying business effects. Stripe's Connect webhook documentation describes the available scopes.

Webhook payloads also have a version. An endpoint can be configured with an API version, and the Event object records the version used to render its data. Treat a webhook version change as a schema migration: test the receiver, types, fixtures, and downstream business decisions before changing it.

Forwarding creates a second trust relationship

Sometimes a generic relay receives Stripe's webhook and forwards it to another application:

Stripe -- Stripe-Signature --> Relay -- new relay signature --> Application

This section describes webhook relays generally, not Gateman. The relay must first preserve the raw body and verify Stripe's signature with the Stripe endpoint secret. After durable acceptance, it should deliver to the application using a separate application-specific signing key or other authenticated mechanism. The second signature should cover the exact downstream body plus delivery metadata such as a timestamp, destination, and delivery ID.

The application verifies the relay signature, checks timestamp freshness, and deduplicates the relay delivery. The relay's 2xx to Stripe should normally mean “verified and durably stored,” not “every downstream consumer has completed processing.” The relay needs its own queue, retry policy, dead-letter handling, and observability.

Forwarding Stripe's original signature alone is usually the wrong boundary. The downstream application would need the Stripe endpoint secret, and the original signature does not cover a wrapper, transformed body, destination, or retry attempt. Preserve the original signature as audit data if useful, but authenticate the new hop separately. A relay that changes bytes without re-signing creates an ambiguity that the receiver cannot safely resolve.

Where Gateman fits

Gateman's Stripe webhook setup is a provisioning path, not a webhook receiver. The write capability is stripe.webhook.write, with application scope stripe:webhook:write and route POST /stripe/webhook/setup. The authenticated application supplies an environment, HTTPS callback URL, and event list. Gateman configures the supported Stripe endpoint and stores its signing secret, but the setup response does not reveal that secret.

The read capability is stripe.webhook.read, with scope stripe:webhook:read and route POST /stripe/webhook-secret. An authorized application can retrieve the environment-selected signing secret and verify deliveries locally. The read route is separate from endpoint setup and does not receive webhook traffic.

Gateman treats staging and development as labels for the shared Stripe test endpoint association. Production uses its separate live endpoint association. After configuration, Stripe delivers events directly to the callback URL supplied by the application. Gateman does not receive, verify, store, or forward those Stripe deliveries.

That boundary is deliberate: Gateman removes endpoint setup and credential-plumbing work while the application retains control of event receipt, signature verification, deduplication, and business processing. See the Gateman webhook setup article, write capability reference, and read capability reference.

The durable rule is: verify each hop according to who sent it, acknowledge only after durable acceptance, and make every event safe to see more than once.

← All articles