Webhooks
Declare a signed webhook endpoint and verify it in the route handler.
Connectors that receive provider-emitted events declare a single webhook endpoint in the manifest. The platform mounts the endpoint, generates a per-installation URL the admin pastes into the provider’s webhook config, and routes deliveries to your handler.
Declaring the endpoint
webhooks: {
endpoint: "/webhooks/stripe",
auth: "stripe_signature",
},
| Field | Effect |
|---|---|
endpoint | URL path under the connector’s namespace. Must correspond to a route file (src/api/webhooks/stripe.ts for the example). |
auth | Signature scheme. Provider-specific (e.g. "stripe_signature"). |
The set of supported signature schemes is provider-by-provider. Plain extensions are restricted to
auth: "api_key"or"public"; webhook signature schemes are gated to connectors. Consult the platform team for what’s wired up.
The handler
// src/api/webhooks/stripe.ts
import { api, ingest } from "@backfill-io/sdk";
import {
STRIPE_EVENT_CATALOG,
KNOWN_IGNORED_EVENT_TYPES,
eventIdempotencyKey,
} from "../../lib/transforms/webhooks";
export const config = { auth: "stripe_signature" };
export const POST = api(async (request) => {
const event = request.body || {};
const object = event.data?.object || {};
const catalogEntry = STRIPE_EVENT_CATALOG[event.type];
if (!catalogEntry) {
return api.json({
ignored: true,
known: KNOWN_IGNORED_EVENT_TYPES.includes(event.type),
eventId: event.id || null,
eventType: event.type || null,
}, { status: 202 });
}
const emissions = catalogEntry.transform(event, object);
const results = emissions.map((em, i) =>
ingest.canonical(em.canonicalType, em.payload, {
stream: em.stream,
idempotencyKey: eventIdempotencyKey(event, em, i, emissions.length),
}),
);
return api.json({ ok: true, eventId: event.id, results }, { status: 202 });
});
import { ingest } from "@backfill-io/sdk" is the connector pattern — ingest is provided to connector scripts at runtime.
Signature verification
When config.auth is a signature scheme, the platform verifies the signature before invoking your handler. By the time POST(request) runs, the signature has been validated against the secret stored in the connection’s settings (e.g., webhook_secret). If verification fails, the platform rejects the delivery with 401 and your handler never runs.
This is different from a plain extension’s auth: "public" route, where verification is your responsibility entirely.
Idempotency
Providers re-deliver. Your handler will see duplicates. Always pass an idempotencyKey to ingest.canonical derived from the provider’s event ID (and emission index, if a single event splits into multiple records). The platform deduplicates based on the key.
idempotencyKey: `stripe:event:${event.id}:${emissionIndex}`
Status codes
Return 202 Accepted for valid events you’ve processed (or chosen to ignore). Reserve 4xx for deliveries you can’t process at all (malformed payload, event types you’ve never heard of). The platform’s webhook ingress retries on 5xx.
What you don’t have to do
- Tenant resolution. The platform resolves tenant from the per-installation URL and binds your handler accordingly.
- Replay storage. Verified raw bodies are persisted by the platform for replay tooling.
- Retries from the ingest side — that’s
ingest.canonical’s job, with the idempotency key.