Webhook Guides
Stripe Webhook Example
This Stripe webhook example shows the full request contract for a billing event: endpoint URL, HTTP method, provider headers, JSON payload, receiver signature verification, idempotency handling, and FastHook debugging queries.
The example uses the public hook /hooks/hook-stripe-test and a FastHook source URL. Stripe sends the event to the source, FastHook records the request and forwards the request body and provider headers for normal untransformed delivery, and your billing API verifies Stripe-Signature with its own Stripe endpoint secret.
Example endpoint contract
Use a dedicated source URL per environment. For this public example, the source URL is shown with a placeholder host so it is safe to copy into documentation without exposing a production secret.
Endpoint URL
https://hook-xxxxxx.fasthook.io/Stripe endpoint registration notes
Endpoint URL:
https://hook-xxxxxx.fasthook.io/
Stripe event types to subscribe to:
- invoice.paid
- invoice.payment_failed
- payment_intent.succeeded
Receiver rule:
- Verify Stripe-Signature in your billing API before changing billing state.
- Use event.id as the idempotency key.Create a matching FastHook source
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 example source",
"type": "WEBHOOK",
"status": "enabled",
"config": {
"auth_type": null,
"auth": null,
"allowed_http_methods": ["POST"],
"custom_response": {
"content_type": "json",
"body": "{\"received\":true}"
}
}
}'Example Stripe request
The sample request uses a redacted Stripe-Signature. It is useful for shape and documentation, but it is not a valid signed request. To test signature verification, use a real Stripe test event or the Stripe CLI so Stripe signs the exact body it sends.
Sample cURL
curl -X POST "https://hook-xxxxxx.fasthook.io/" \
-H "Content-Type: application/json" \
-H "Stripe-Signature: t=1713600000,v1=***redacted***" \
-d '{"id":"evt_1QxYz","type":"invoice.paid","data":{"object":{"id":"in_123","customer":"cus_123"}}}'Sample headers
{
"content-type": "application/json",
"stripe-signature": "t=1713600000,v1=***redacted***",
"user-agent": "Stripe/1.0 (+https://stripe.com/docs/webhooks)"
}Sample payload
{
"id": "evt_1QxYz",
"type": "invoice.paid",
"created": 1713600000,
"data": {
"object": {
"id": "in_123",
"customer": "cus_123",
"status": "paid",
"amount_paid": 4900,
"currency": "usd"
}
}
}Stripe CLI signed test event
# Use this to create a real Stripe-signed local test event.
stripe listen --forward-to https://hook-xxxxxx.fasthook.io/
stripe trigger invoice.paidReceiver validation example
The following code runs in your billing API destination. It is not FastHook backend code. FastHook does not store your STRIPE_API_KEY or STRIPE_WEBHOOK_SECRET; your application owns those secrets and verifies Stripe with the raw body.
Verify Stripe-Signature in your billing API
import express from "express";
import Stripe from "stripe";
const app = express();
// This code runs in your billing API destination, not inside FastHook.
// FastHook does not read STRIPE_API_KEY or STRIPE_WEBHOOK_SECRET.
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 });
});Apply receiver-side idempotency
async function markInvoicePaidOnce({ stripeEventId, invoiceId, customerId }) {
const alreadyProcessed = await db.webhookEvents.findUnique({
where: { provider_event_id: stripeEventId }
});
if (alreadyProcessed) return;
await db.transaction(async (tx) => {
await tx.webhookEvents.create({
data: {
provider: "stripe",
provider_event_id: stripeEventId,
invoice_id: invoiceId
}
});
await tx.invoices.update({
where: { stripe_invoice_id: invoiceId },
data: { status: "paid", customer_id: customerId }
});
});
}Inspect the example in FastHook
FastHook request data helps confirm whether Stripe reached the source and whether the request became a routed event. Query with include=data to inspect stored headers and payload. Query request events and failed events when the billing API returns an error or times out.
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"Retrieve one request with stored data
curl "https://api.fasthook.io/v1/requests/req_8bd1e3f2a4c94201" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"Inspect events created from the request
curl "https://api.fasthook.io/v1/requests/req_8bd1e3f2a4c94201/events?limit=20" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"Inspect failed Stripe deliveries
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 after the billing 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 JSON
{
"id": "req_8bd1e3f2a4c94201",
"source_id": "src_q6z62b6py5o79b",
"status": "accepted",
"verified": false,
"rejection_cause": null,
"events_count": 1,
"data": {
"method": "POST",
"path": "/",
"query": "",
"headers": {
"content-type": "application/json",
"stripe-signature": "t=1713600000,v1=***redacted***",
"user-agent": "Stripe/1.0 (+https://stripe.com/docs/webhooks)"
},
"body": {
"id": "evt_1QxYz",
"type": "invoice.paid",
"created": 1713600000,
"data": {
"object": {
"id": "in_123",
"customer": "cus_123",
"status": "paid",
"amount_paid": 4900,
"currency": "usd"
}
}
}
}
}What this example proves
- The endpoint accepts Stripe
POSTrequests with JSON Event payloads. - The original
Stripe-Signatureheader is visible for receiver-side verification. - Your billing API verifies Stripe using the exact raw body and its own endpoint secret.
- FastHook records accepted requests with headers, body, method, path, and query when you use
include=data. - Failed destination deliveries can be inspected and retried after idempotency is active.
Stripe webhook example FAQ
Is the Stripe-Signature value in this cURL example valid?
No. The cURL sample uses a redacted Stripe-Signature to document request shape. For a valid signature, use a real Stripe test delivery, Stripe Dashboard resend, or Stripe CLI forwarding so Stripe signs the exact raw body.
Does FastHook verify Stripe-Signature with STRIPE_WEBHOOK_SECRET?
No. FastHook receives and records the Stripe request, then forwards the body and provider headers for normal untransformed delivery. Your billing API owns STRIPE_WEBHOOK_SECRET and verifies Stripe-Signature against the raw body.
What should a Stripe receiver use for idempotency?
Use the Stripe event id, such as evt_1QxYz, as the receiver-side idempotency key before mutating invoices, subscriptions, entitlements, or fulfillment state.
When should I use FastHook destination signatures?
Use FastHook destination signatures when your receiver needs to verify that a delivery came from FastHook. That is separate from Stripe-Signature, which proves the original Stripe request.