Webhook Guides
Stripe Webhooks Guide
Stripe webhooks notify your application when billing events happen. They are used for subscription state, invoice payment, checkout completion, charge outcomes, customer updates, refunds, and entitlement changes. Because these events touch revenue and account access, the receiver must be secure, idempotent, and easy to recover when delivery fails.
FastHook gives Stripe a stable source URL, records the inbound request, routes accepted events through connection rules, delivers to your billing API, and keeps request, event, attempt, retry, and replay evidence together. The examples below match current FastHook behavior for sources, connections, request inspection, destination signatures, and retries.
How Stripe webhook delivery works
Stripe sends an HTTP POST request with a JSON Event object and a Stripe-Signature header. Your receiver should verify the signature using the endpoint signing secret and the exact raw request body, then handle the event by type.
Stripe delivery is retry-aware and ordering is not guaranteed. Your handler should return a 2xx response quickly after the event is accepted, use event.id as an idempotency key, and fetch the current Stripe object when the latest billing state matters.
Stripe webhook headers
| Header or key | Example | Use |
|---|---|---|
Stripe-Signature | t=1713600000,v1=<signature> | Use Stripe's endpoint secret and the exact raw body. |
Content-Type | application/json | Stripe Event objects are delivered as JSON. |
User-Agent | Stripe/1.0 (+https://stripe.com/docs/webhooks) | Useful for debugging, not a security boundary. |
Idempotency key | event.id | Use the Stripe event id before changing billing state. |
FastHook setup for Stripe webhooks
The recommended FastHook pattern for Stripe is simple: create a dedicated Stripe source with allowed_http_methods set to ["POST"], register that source URL in Stripe, route only the billing events your application needs, and verify Stripe-Signature in the destination receiver before mutating billing state.
FastHook source HMAC auth is designed for configurable raw-body HMAC headers. Stripe uses its own Stripe-Signature format, so do not treat source HMAC as a drop-in Stripe signature verifier. Keep the Stripe secret in your receiver, or use FastHook destination signatures when you intentionally transform payloads before delivery.
Create a Stripe source URL
curl -X POST "https://api.fasthook.io/v1/sources" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
-H "Content-Type: application/json" \
-d '{
"name": "Stripe billing",
"description": "Stripe billing webhook source",
"type": "WEBHOOK",
"status": "enabled",
"config": {
"auth_type": null,
"auth": null,
"allowed_http_methods": ["POST"],
"custom_response": {
"content_type": "json",
"body": "{\"received\":true}"
}
}
}'Create the billing API destination
curl -X POST "https://api.fasthook.io/v1/destinations" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
-H "Content-Type: application/json" \
-d '{
"name": "Billing API",
"type": "HTTP",
"config": {
"url": "https://api.example.com/webhooks/stripe",
"http_method": "POST",
"path_forwarding_disabled": false,
"auth_type": "FASTHOOK_SIGNATURE",
"auth": {}
}
}'Route only the Stripe events your app consumes
curl -X POST "https://api.fasthook.io/v1/connections" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
-H "Content-Type: application/json" \
-d '{
"name": "stripe-billing-to-api",
"source_id": "src_q6z62b6py5o79b",
"destination_id": "des_TU9ioCk5EHUU",
"rules": [
{
"type": "filter",
"headers": {
"stripe-signature": { "$exists": true }
},
"body": {
"type": {
"$in": [
"invoice.paid",
"invoice.payment_failed",
"checkout.session.completed",
"payment_intent.succeeded"
]
}
}
},
{
"type": "retry",
"strategy": "exponential",
"count": 5,
"interval": 60000,
"response_status_codes": ["429", "500-599"]
}
]
}'Send a Stripe CLI test event
# Forward Stripe test events to the FastHook source URL.
stripe listen --forward-to https://hook-xxxxxx.fasthook.io/
# Generate a representative test event.
stripe trigger invoice.paidStripe payload and signature examples
A real Stripe signature must be produced by Stripe for the exact raw body. The cURL example below is useful for documenting request shape, but use Stripe CLI, Dashboard test events, or a real Stripe event to test signature verification.
Stripe-style request example
curl -X POST "https://hook-xxxxxx.fasthook.io/" \
-H "Content-Type: application/json" \
-H "Stripe-Signature: t=1713600000,v1=***redacted***" \
-d '{
"id": "evt_1QxYz",
"object": "event",
"type": "invoice.paid",
"created": 1713600000,
"data": {
"object": {
"id": "in_123",
"object": "invoice",
"customer": "cus_123",
"status": "paid",
"amount_paid": 4900,
"currency": "usd"
}
}
}'Stripe Event payload example
{
"id": "evt_1QxYz",
"object": "event",
"type": "invoice.paid",
"created": 1713600000,
"livemode": false,
"data": {
"object": {
"id": "in_123",
"object": "invoice",
"customer": "cus_123",
"status": "paid",
"amount_paid": 4900,
"currency": "usd",
"subscription": "sub_123"
}
}
}Verify Stripe-Signature in your destination app
This snippet is receiver code for your billing API, not FastHook backend code. FastHook does not read STRIPE_API_KEY or STRIPE_WEBHOOK_SECRET; it forwards the Stripe request so your application can verify Stripe-Signature against the raw body.
import express from "express";
import Stripe from "stripe";
const app = express();
// This runs in your billing API destination, not inside FastHook.
// FastHook forwards the Stripe request; your app owns these Stripe secrets.
const stripe = new Stripe(process.env.STRIPE_API_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
const signature = req.headers["stripe-signature"];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, signature, endpointSecret);
} catch (error) {
res.status(400).send("Webhook signature verification failed");
return;
}
if (event.type === "invoice.paid") {
const invoice = event.data.object;
await markInvoicePaidOnce({
stripeEventId: event.id,
invoiceId: invoice.id,
customerId: invoice.customer
});
}
res.json({ received: true });
});Verify FastHook delivery signatures when using transformed payloads
import crypto from "node:crypto";
function verifyFastHookSignature({ rawBody, timestamp, signature, signingSecret }) {
const timestampSeconds = Number(timestamp);
const nowSeconds = Math.floor(Date.now() / 1000);
if (!Number.isSafeInteger(timestampSeconds)) return false;
if (Math.abs(nowSeconds - timestampSeconds) > 5 * 60) return false;
if (!signature?.startsWith("v1=")) return false;
const expected = "v1=" + crypto
.createHmac("sha256", signingSecret)
.update(timestamp + "." + rawBody)
.digest("hex");
const actualBytes = Buffer.from(signature || "");
const expectedBytes = Buffer.from(expected);
return actualBytes.length === expectedBytes.length &&
crypto.timingSafeEqual(actualBytes, expectedBytes);
}
// FastHook sends:
// x-fasthook-timestamp: Unix timestamp in seconds
// x-fasthook-signature: v1=<hex-hmac-sha256>Inspect Stripe webhook traffic in FastHook
FastHook request records are useful when Stripe reports a delivery attempt but your billing service does not show a processed event. Query accepted requests with include=data to inspect the stored method, path, query, headers, and payload. Query rejected requests when method or source state problems stop the request before event routing.
Inspect accepted Stripe requests
curl "https://api.fasthook.io/v1/requests?source_id=src_q6z62b6py5o79b&status=accepted&from=now-24h&to=now&limit=20&include=data" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"Inspect rejected Stripe requests
curl "https://api.fasthook.io/v1/requests?source_id=src_q6z62b6py5o79b&status=rejected&from=now-24h&to=now&limit=20&include=data" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"Inspect failed destination events
curl "https://api.fasthook.io/v1/events?source_id=src_q6z62b6py5o79b&status=FAILED&limit=20" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"Retry a failed event after the receiver is fixed
curl -X POST "https://api.fasthook.io/v1/events/evt_01JYSTRIPE/retry" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"Accepted request with stored Stripe data
{
"id": "req_8bd1e3f2a4c94201",
"source_id": "src_q6z62b6py5o79b",
"status": "accepted",
"verified": false,
"rejection_cause": null,
"event_count": 1,
"data": {
"method": "POST",
"path": "/",
"headers": {
"content-type": "application/json",
"stripe-signature": "t=1713600000,v1=***redacted***"
},
"body": {
"id": "evt_1QxYz",
"type": "invoice.paid",
"data": {
"object": {
"id": "in_123",
"customer": "cus_123",
"status": "paid"
}
}
}
}
}Stripe webhook production checklist
- Use separate Stripe source URLs and signing secrets for local, staging, and production.
- Subscribe only to events your application consumes, such as invoice and checkout events.
- Verify
Stripe-Signaturewith the exact raw body before changing billing state. - Use
event.idas the receiver-side idempotency key. - Return
2xxquickly after accepted work is durably recorded. - Do not depend on Stripe event ordering; fetch current invoices, subscriptions, or payment intents when needed.
- Use FastHook event attempts and retries for destination failures after the billing receiver is healthy.
- Use FastHook destination signatures when the receiver needs to verify traffic from FastHook.
Stripe webhook FAQ
How do Stripe webhooks work?
Stripe sends signed HTTP POST requests to a webhook endpoint when events such as invoice.paid, invoice.payment_failed, checkout.session.completed, or payment_intent.succeeded occur. The receiver verifies the Stripe-Signature header against the raw request body and then processes the event idempotently.
Where should Stripe-Signature be verified when using FastHook?
For Stripe traffic, verify Stripe-Signature at the application destination using the raw body and the Stripe endpoint secret. FastHook forwards provider headers and body for normal untransformed deliveries; if you transform the payload, verify FastHook's destination signature instead for the transformed delivery.
Can FastHook source HMAC auth verify Stripe-Signature directly?
FastHook source HMAC auth verifies a configurable HMAC header over the raw body, optionally with a separate timestamp header and prefix. Stripe-Signature uses Stripe's own header format, so the practical Stripe pattern is to accept Stripe at the source and verify the Stripe signature in your receiver with Stripe's library.
How should duplicate Stripe webhook events be handled?
Use the Stripe event id, such as evt_123, as a receiver-side idempotency key before changing billing state. Stripe retries and manual resends can deliver the same event more than once.
Try FastHook with Stripe
Create a Stripe source, register the source URL in Stripe, connect it to your billing destination, send a test event, inspect the stored request, and replay failed deliveries after your billing receiver is fixed.