Skip to content

Webhooks

Register an endpoint (POST /v1/webhook_endpoints) with a URL and the event types you want (see the event catalogue for the full list). Your endpoint receives a POST with a JSON body:

{
"id": "evt_01H8X...",
"type": "exchange.delivered",
"api_version": "2026-09-01",
"created": "2026-09-10T09:00:00Z",
"mode": "test",
"data": { "object": { "id": "exch_01H8X...", "state": "delivered", "...": "..." } }
}

Respond 2xx within a few seconds — anything else is treated as a delivery failure and queued for retry.

Every delivery carries a Railflo-Signature header: t=<unix-seconds>,v1=<hex-hmac-sha256>. The signed string is <timestamp>.<raw request body> — verify against the raw, unparsed body, before your framework touches it.

Always verify. An unverified webhook endpoint is a public POST target anyone can hit.

import { verifyRailfloWebhook } from '@railflo/events'; // packages/events/src/signing.ts
const valid = verifyRailfloWebhook({
rawBody: rawRequestBody, // the exact request body as a string — NOT JSON.parse()'d
signatureHeader: req.headers['railflo-signature'],
secret: process.env.RAILFLO_WEBHOOK_SECRET!, // or [oldSecret, newSecret] during rotation
});

The canonical reference implementation ships in this repo at packages/events/verify/verify.py — copy it directly:

from verify import verify_railflo_webhook
valid = verify_railflo_webhook(
raw_body=request.body, # bytes, unparsed
signature_header=request.headers.get("Railflo-Signature"),
secret=os.environ["RAILFLO_WEBHOOK_SECRET"],
)

Canonical reference: packages/events/verify/verify.cs in this repo — same algorithm, same tolerance window.

A failed delivery (non-2xx, timeout, or connection error) retries on a fixed backoff ladder: 30s, 2m, 10m, 1h, 6h, 24h. After the last attempt fails, the delivery is marked permanently failed — it will not retry again, but it remains visible (and re-deliverable on demand) via GET /v1/webhook_deliveries.

An endpoint failing more than 90% of its deliveries over 24 hours is automatically disabled — you’ll receive a webhook_endpoint.disabled event on your other endpoints, and need to re-enable it from Console once the underlying problem is fixed.

POST /v1/webhook_endpoints/{id}/rotate-secret issues a new secret while keeping the old one valid for a 24-hour overlap window — verify against both during that window, then drop the old one.

  • Verify every signature — no endpoint that skips this.
  • Return 2xx fast; do slow work (DB writes, downstream calls) asynchronously after responding.
  • Handle duplicate deliveries idempotently — at-least-once delivery means the same id can arrive twice.
  • Watch for webhook_endpoint.disabled on a secondary channel (email/Slack), not just as another webhook to the endpoint that just got disabled.
  • Request a live-mode key and re-register your endpoint under it — test and live endpoints are entirely separate.