Webhook verification & crypto
The verify helpers for Standard Webhooks and generic HMAC signatures, plus the Crypto primitives.
Never process a webhook you haven’t verified. The SDK ships two globals for
this: verify, with helpers for reusable signature schemes, and Crypto,
the HMAC/hash primitives for custom checks. Both are available to any script —
connector webhook routes are the usual home, but a plain extension exposing a
public API route can use them the same way.
import { verify, Crypto } from "@backfill-io/sdk";
verify
Each verifier takes the raw request and the signing secret and returns a result object — it never throws:
type WebhookVerificationResult =
| { ok: true; metadata: Record<string, any> }
| { ok: false; error: string; message: string; header?: string };
Pass the raw body string exactly as received. Re-serializing parsed JSON
(JSON.stringify(request.json)) breaks signatures — key order and whitespace
matter.
| Verifier | Scheme |
|---|---|
verify.standardWebhook(request, secret, opts?) | Standard Webhooks webhook-id/webhook-timestamp/webhook-signature. |
verify.hmacSha256(request, secret, opts?) | Generic hex HMAC-SHA256 over the raw body; opts.headers lists the header names to check. |
request is { rawBody: string, headers: Record<string, string | string[] | null | undefined> }.
standardWebhook accepts { toleranceSeconds?, nowSeconds? } for timestamp
checks (tolerance defaults to a few minutes).
import { api, verify, Secrets, ingest, Log } from "@backfill-io/sdk";
export const config = { auth: "public" };
export const POST = api(async (request) => {
const result = verify.standardWebhook(
{ rawBody: request.rawBody, headers: request.headers },
Secrets.get("webhook_signing_secret"),
);
if (!result.ok) {
Log.warn("Rejected webhook", { error: result.error });
return api.error("Invalid signature", { status: 401 });
}
const event = JSON.parse(request.rawBody);
// ... ingest.canonical(...) / handle the event ...
return api.json({ received: true });
});
For declared connector webhook streams, the platform can verify signatures
before your route runs — see Webhooks. Reach for verify
when a route needs to perform signature verification in code.
Crypto
Primitives for custom schemes. All algorithms are "sha256" | "sha384" | "sha512".
| Function | Returns |
|---|---|
Crypto.hmac(algorithm, key, data) | Hex-encoded HMAC. |
Crypto.hmacBase64(algorithm, key, data) | Base64-encoded HMAC. |
Crypto.hmacBase64WithBase64Key(algorithm, key, data) | Base64 HMAC where key is itself base64-encoded (Standard Webhooks-style secrets). |
Crypto.hash(algorithm, data) | Hex-encoded digest. |
Crypto.timingSafeEqual(a, b) | Constant-time string comparison. |
Always compare signatures with Crypto.timingSafeEqual, never === — a
plain comparison leaks timing information:
const expected = Crypto.hmac("sha256", secret, request.rawBody);
const given = String(request.headers["x-signature"] ?? "");
if (!Crypto.timingSafeEqual(expected, given)) {
return api.error("Invalid signature", { status: 401 });
}
Testing
The connector test runtime includes Crypto and verify
implementations, so signature handling is unit-testable — sign a fixture
body with the real algorithm in your test, then assert your route accepts it
and rejects a tampered copy.