Add a webhook consumer
Create an API-managed endpoint, verify a signed test delivery, and pause it safely on failure.
Create an API-managed endpoint, verify a signed test delivery, and pause it safely on failure.
Webhook endpoint lifecycle is platform-owned. Receiver code, raw-body signature verification, idempotency, and asynchronous processing belong in your backend repository.
The endpoint accepts a signed test delivery, records it idempotently, and can be paused without changing the Business contract.
Use the Backend SDK's dedicated receiver. Webhooks have their own signing
secret; they do not carry gateway request signatures and do not require
FS_RUNTIME_TOKEN.
import express from "express";
import { createWebhookHandler } from "@farthershore/backend/webhooks";
const app = express();
const secret = process.env.FS_WEBHOOK_SECRET;
if (!secret) throw new Error("FS_WEBHOOK_SECRET is required");
const webhooks = createWebhookHandler({
secret,
on: {
"subscription.created": async (event) => {
await enqueueIdempotently(event); // your durable transaction/queue adapter
},
"payment.failed": async (event) => {
await enqueueIdempotently(event);
},
},
});
// Mount before gateway verification and before any JSON body parser.
app.post(
"/webhooks/farthershore",
express.raw({ type: "*/*" }),
webhooks.express(),
);
For a Fetch framework export webhooks.fetch as the handler. The receiver
verifies raw bytes and timestamp, parses the envelope, and dispatches known
events. Handler failures return a retriable failure. Unknown future event types
are acknowledged; use onUnknown for observability. A successful handler must
mean the event is durably recorded or completed, not merely scheduled in memory.
The default deduplication store is in-process. For multiple replicas or crash
recovery, implement the WebhookNonceStore
claim/settle contract and retain application-level idempotency in your durable
write. Do not substitute a check-then-insert cache; concurrent deliveries and
lease expiry can otherwise run side effects twice. Use
signWebhookForTesting to test modified bytes,
expired signatures, duplicates, and a handler that fails once then succeeds.
While the receiver still runs on your machine, let the CLI tunnel to it instead of deploying to test:
farthershore webhook listen acme \
--forward-to http://localhost:3000/webhooks/farthershore \
--print-secret --trigger subscription.created
Export the printed FS_WEBHOOK_SECRET= line into the receiver's environment,
then watch the tail (time type status responseStatus id) while you fire
more samples from a second shell with
farthershore webhook trigger acme <webhookId> --type payment.failed --idempotency-key <persisted-attempt-key>. Ctrl-C
deletes the temporary endpoint; if the listener was killed instead, remove the
leftover *.trycloudflare.com endpoint with webhook delete --yes.
farthershore webhook create acme \
--url https://api.example.com/webhooks/farthershore \
--events subscription.created,payment.failed \
--idempotency-key primary-webhook \
--format json
Capture any one-time secret without logging or committing it. Verify the
signature against the raw request bytes before JSON parsing, deduplicate by the
event identifier, return a 2xx promptly, and queue slow work.
farthershore webhook test acme <webhookId> --idempotency-key <persisted-test-attempt-key> --format json
farthershore webhook trigger acme <webhookId> --type payment.failed --idempotency-key <persisted-trigger-attempt-key> --format json
farthershore webhook deliveries acme <webhookId> --limit 20 --format json
webhook test sends the plain webhook.test ping; webhook trigger --type
sends a signed, realistic sample of one catalog event so each handler branch is
exercised and logged under its own event type. A successful send is not proof
the receiver processed it; inspect the delivery response and your receiver's
durable record.
Pause deliveries while repairing a failing receiver:
farthershore webhook update acme <webhookId> --disable --format json
farthershore webhook deliveries acme <webhookId> --limit 20 --format json
Rotate the signing secret when it may have leaked or on a schedule. The new secret is returned once; deliveries carry both signatures for 24 hours so the receiver can switch without a gap:
farthershore webhook rotate acme <webhookId> --format json --idempotency-key <persisted-webhook-rotate-attempt-key>
Re-enable only after a signed test succeeds. Deletion is destructive and
requires --yes:
farthershore webhook delete acme <webhookId> --yes --format json
Webhook delivery is independent of a person's notification preferences. See Notifications.
Require a successful test delivery, a matching receiver-side durable record,
and a recent delivery row with the expected response. A 2xx alone does not
prove downstream work completed.
Disable the endpoint while repairing repeated failures. Re-enable only after a
new signed test succeeds. Use delete --yes only when permanent removal is the
reviewed intent; deletion has no restore command.
Create the webhook for only the listed events, capture the one-time secret
without logging it, send a signed test, and verify both the platform delivery
record and the receiver's durable deduplication record. Pause on failure.