
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 event 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.
Frequently asked questions
How do you verify a Stripe webhook signature?
Read the raw request body before any JSON parsing, recompute HMAC-SHA256 of {timestamp}.{raw_body} using your webhook signing secret, and compare it constant-time against the v1= value in the Stripe-Signature header. Reject the request if the header's t= timestamp is more than 300 seconds old. Stripe ships verifier helpers in every official SDK, and using them avoids the most common mistake, which is parsing the JSON before verifying it.
How long does Stripe retry a failed webhook?
Stripe retries with exponential backoff for up to 3 days when your endpoint returns a non-2xx status or fails to respond within about 30 seconds. After 3 days the event is marked failed and is never sent again, so anything older than that has to be recovered by querying Stripe's Events API.
Do Stripe webhooks need to be idempotent?
Yes. Stripe retries on failure, network problems duplicate deliveries, and any manual replay re-fires the event, so the same event will reach your handler more than once. Key your processing on the Stripe event id so a repeated delivery produces the same outcome as the first.
What is Hooksbase?
Hooksbase is event infrastructure for AI agents. It ingests events over four channels — HTTP, email, HTML form, and scheduled cron — verifies them, routes them by rule, runs versioned Automations in the event path, and delivers them to HTTP and cloud destinations (AWS SQS, AWS EventBridge, GCP Pub/Sub, and S3-compatible storage) with retries, strict ordering, Standard Webhooks-compatible signing, deterministic replay, and a dead-letter path. It is a hosted service, runs on Cloudflare Workers, is operated at hooksbase.com, and is not affiliated with — and shares no code or ownership with — other similarly named webhook, hook, or tunnelling tools.