Providers / April 24, 2026

How to receive Stripe webhooks reliably (signing, retries, replay)

Stripe webhook delivery flow with verification, retries, and replay

Stripe sends webhooks for almost every event in your account: payments, subscriptions, refunds, disputes, invoices, customer changes. Receiving them is easy. Receiving them reliably — without dropping events, double-charging customers, or losing the ability to replay — is the part most teams underestimate.

This guide covers both: the minimum you need to receive Stripe webhooks at all, and what production reliability actually requires.

What Stripe sends

Stripe webhooks are JSON POST requests. A payment_intent.succeeded event:

{
  "id": "evt_1NxV5y2eZvKYlo2C8wT9pQ4Z",
  "type": "payment_intent.succeeded",
  "api_version": "2024-11-20",
  "created": 1714000000,
  "data": {
    "object": {
      "id": "pi_3NxV5y2eZvKYlo2C0pZ4uX2A",
      "amount": 2999,
      "currency": "usd",
      "customer": "cus_O8N3K5p4lJ2mNw"
    }
  }
}

Every Stripe event includes:

  • A unique id (use this for idempotency)
  • A type like payment_intent.succeeded, customer.subscription.updated, invoice.payment_failed
  • An api_version (lock your code to a specific version)
  • A created timestamp
  • The affected object in data.object

There are over 200 event types. Subscribe only to the ones you actually use — fewer events means simpler error handling.

Stripe's signature scheme

Every Stripe webhook includes a Stripe-Signature header:

Stripe-Signature: t=1714000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

The t= is the timestamp; v1= is HMAC-SHA256 of {timestamp}.{raw_body} using your webhook signing secret.

Verification rules:

  1. Read the raw body before any JSON parsing
  2. Recompute HMAC-SHA256(secret, "t.body") and compare constant-time with v1
  3. Reject anything where now - t > 300 seconds (replay protection)

Stripe ships verifier helpers in every official SDK. Use them — don't roll your own. The classic mistake is parsing the JSON before verifying, which lets attackers smuggle invalid bytes past your check.

Stripe's retry policy

If your endpoint returns non-2xx (or doesn't respond within ~30 seconds), Stripe retries with exponential backoff for up to 3 days. After 3 days, the event is marked failed and never re-sent.

This means three things matter:

  • Acknowledge fast. Return 2xx in under a second; do real work async.
  • Be idempotent. Stripe will retry, network blips will duplicate, your replay will re-fire. Same id = same outcome.
  • Have a recovery story for events older than 3 days. Stripe has an Events API you can query for missed events, but you have to know they're missing.

DIY: minimal Stripe webhook handler in Node

The bare minimum to receive Stripe webhooks:

import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!

export async function POST(req: Request) {
  const sig = req.headers.get('stripe-signature')!
  const body = await req.text() // RAW body — required for signature

  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(body, sig, endpointSecret)
  } catch {
    return new Response('Bad signature', { status: 400 })
  }

  // Idempotency: have you seen this event.id already? If so, return 200.
  // ... your check ...

  // Acknowledge first, work async.
  queueWork(event)

  return new Response('ok', { status: 200 })
}

This works for a demo. What's missing for production:

  • A persistence layer for idempotency
  • A retry queue if queueWork fails
  • A replay path when you ship a bug
  • A delivery history when a customer asks "did you receive my payment?"
  • Signature secret rotation when an engineer leaves
  • Throttling when Stripe sends a burst (e.g. after a failed-payment retry storm)

Each of those is its own subsystem. Most teams build them; the ones that don't end up debugging payment failures at 2 AM.

Hooksbase: receive Stripe webhooks without rebuilding the rest

Hooksbase is built around this exact problem. With the verified-providers feature, Stripe is one of five pre-verified providers — after the request passes Hooksbase ingest auth, signature verification happens before the payload reaches your code.

Setup:

  1. Create a project, then a webhook with provider: stripe.
  2. Paste your Stripe webhook signing secret into the dashboard (encrypted at rest).
  3. Put a small forwarder at the Stripe webhook URL. It should preserve the raw body and Stripe-Signature header, then POST to the Hooksbase ingest URL (https://hooks.hooksbase.com/v1/ingest/wh_...) with Authorization: Bearer <ingest secret>. If your source is already a custom producer that can attach that header, it can call Hooksbase directly.
  4. Add your application URL as the destination.

Done. You now get:

  • Signature verification before the event ever reaches your endpoint
  • Idempotency for your destination — every dispatch includes a unique webhook-id header (Standard Webhooks-compatible) you can dedupe on across retries. Stripe's event.id is captured as provider.sourceId and queryable on every delivery.
  • Retries with exponential backoff to your endpoint
  • Strict FIFO ordering if you turn it on (Pro+) — useful for subscription event sequences
  • Deterministic replay with persisted dispatch snapshots, so re-runs work even after you change the transform
  • Delivery history and DLQ — every event is queryable; failed terminals are inspectable and re-drivable
  • Provider-aware fieldsprovider.eventType, provider.sourceId, provider.verified are routing conditions and queryable on every delivery

Common Stripe webhook use cases for AI agents

A few patterns that work well with this architecture:

  • Failed payment recovery agentinvoice.payment_failed triggers an agent that checks the customer's account, attempts a card update flow, and notifies the account owner if the customer churns.
  • Subscription change handlercustomer.subscription.updated triggers an agent that reconciles the change against your billing database and notifies CS if downgrade volume spikes.
  • Refund triage agentcharge.refunded triggers an agent that classifies the refund reason and routes high-value ones to a human.
  • New customer onboarding agentcustomer.created triggers an agent that enriches the customer profile and personalizes the first email sequence.

In each case, the agent is a small piece of code. The reason it stays small is that retries, idempotency, replay, and observability live in the relay layer underneath.

Where to go next

Start free at app.hooksbase.com, then use Starter+ provider verification when you're ready to receive signed Stripe webhooks through Hooksbase.

Related guides