Skip to content
AM

Verifying signatures

Studio outbound form webhooks optionally sign the exact request body with HMAC-SHA256.

X-InBlack-Signature: sha256=<64 lowercase hex characters>

There is no timestamp component in this header. The digest input is the raw body bytes only:

hex(HMAC-SHA256(signing_secret, raw_request_body))

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyInBlackWebhook(
rawBody: Buffer,
header: string,
secret: string,
): boolean {
if (!header.startsWith("sha256=")) return false;
const supplied = Buffer.from(header.slice("sha256=".length), "hex");
const expected = createHmac("sha256", secret).update(rawBody).digest();
return supplied.length === expected.length && timingSafeEqual(supplied, expected);
}

Configure your framework to retain the raw body for this route. Re-serializing parsed JSON can change bytes and invalidate the digest.

Verification order

  1. Require the header when your target has a signing secret.
  2. Require the sha256= prefix and valid 32-byte hex digest.
  3. Compute HMAC-SHA256 over the raw body.
  4. Compare decoded bytes in constant time.
  5. Parse JSON only after verification succeeds.
  6. Deduplicate work using X-InBlack-Delivery-ID or the payload’s submissionId.

Because the current signature has no timestamp, replay-window enforcement is not part of the sender contract. Receiver-side deduplication is required for retry and replay resistance.

Provider inbound webhooks use their provider’s scheme. In particular, verify Stripe with Stripe’s library and endpoint secret; do not apply the Studio algorithm merely because both use HMAC-SHA256.