
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
typelikepayment_intent.succeeded,customer.subscription.updated,invoice.payment_failed - An
api_version(lock your code to a specific version) - A
createdtimestamp - 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:
- Read the raw body before any JSON parsing
- Recompute
HMAC-SHA256(secret, "t.body")and compare constant-time withv1 - Reject anything where
now - t > 300seconds (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
queueWorkfails - 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:
- Create a project, then a webhook with
provider: stripe. - Paste your Stripe webhook signing secret into the dashboard (encrypted at rest).
- Put a small forwarder at the Stripe webhook URL. It should preserve the raw body and
Stripe-Signatureheader, then POST to the Hooksbase ingest URL (https://hooks.hooksbase.com/v1/ingest/wh_...) withAuthorization: Bearer <ingest secret>. If your source is already a custom producer that can attach that header, it can call Hooksbase directly. - 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-idheader (Standard Webhooks-compatible) you can dedupe on across retries. Stripe'sevent.idis captured asprovider.sourceIdand 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 fields —
provider.eventType,provider.sourceId,provider.verifiedare 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 agent —
invoice.payment_failedtriggers an agent that checks the customer's account, attempts a card update flow, and notifies the account owner if the customer churns. - Subscription change handler —
customer.subscription.updatedtriggers an agent that reconciles the change against your billing database and notifies CS if downgrade volume spikes. - Refund triage agent —
charge.refundedtriggers an agent that classifies the refund reason and routes high-value ones to a human. - New customer onboarding agent —
customer.createdtriggers 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
- Verify provider webhooks for the verification model in detail
- Recover failed agent events for DLQ and bulk replay patterns
- What is a webhook? if you want the broader webhook primer
- How to build an AI agent for the full agent build path
Start free at app.hooksbase.com, then use Starter+ provider verification when you're ready to receive signed Stripe webhooks through Hooksbase.