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.

Stripe webhook example contract showing endpoint URL, headers, payload, FastHook request record, and billing API receiver.
A good Stripe webhook example documents both sides of the contract: what Stripe sends and what your receiver must verify before billing side effects.

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

Endpoint URL
https://hook-xxxxxx.fasthook.io/

Stripe endpoint registration notes

Endpoint 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

Create Stripe 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

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

Sample headers
{
  "content-type": "application/json",
  "stripe-signature": "t=1713600000,v1=***redacted***",
  "user-agent": "Stripe/1.0 (+https://stripe.com/docs/webhooks)"
}

Sample payload

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

Stripe CLI test
# Use this to create a real Stripe-signed local test event.
stripe listen --forward-to https://hook-xxxxxx.fasthook.io/
stripe trigger invoice.paid

Receiver 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

Receiver Stripe signature
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

Idempotency guard
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 }
    });
  });
}
Stripe webhook example debugging flow showing accepted request, receiver signature verification, idempotency, failed attempts, and replay.
Debug the example from left to right: request captured, receiver verified, idempotency checked, destination attempt inspected, then replayed only after the receiver is fixed.

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

Inspect accepted 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

Retrieve request
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

Inspect request events
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

Inspect failed 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 after the billing receiver is fixed

Retry failed event
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

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 POST requests with JSON Event payloads.
  • The original Stripe-Signature header 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.

Related guides