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 delivery flow through FastHook source URL, request record, connection filters, billing API destination, and replay controls.
Stripe sends billing events to a FastHook source URL. FastHook captures the request, filters by event type, delivers to your billing API, and keeps evidence for retry and replay.

Stripe webhook headers

Header or keyExampleUse
Stripe-Signaturet=1713600000,v1=<signature>Use Stripe's endpoint secret and the exact raw body.
Content-Typeapplication/jsonStripe Event objects are delivered as JSON.
User-AgentStripe/1.0 (+https://stripe.com/docs/webhooks)Useful for debugging, not a security boundary.
Idempotency keyevent.idUse 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

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

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

Create filtered connection
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

Stripe CLI smoke test
# 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.paid

Stripe 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

Stripe 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

Stripe event JSON
{
  "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.

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

Verify FastHook signature
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>
Stripe webhook recovery map showing accepted requests, rejected requests, failed events, destination attempts, idempotency, and replay.
A reliable Stripe workflow separates ingress problems from destination failures. Rejected requests explain why Stripe never became a routed event; failed attempts explain why the billing API did not accept delivery.

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

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"

Inspect rejected Stripe requests

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

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 a failed event after the 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 with stored Stripe data

Accepted request JSON
{
  "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-Signature with the exact raw body before changing billing state.
  • Use event.id as the receiver-side idempotency key.
  • Return 2xx quickly 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.

Related guides