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.

VerifierScheme
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".

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