Verify webhook signatures

Verify incoming webhooks to check that they were sent by Superchat and that the payload has not been modified

Each webhook includes the header x-superchat-signature.


To verify the signature you'll need your webhook's signing secret. Use the secret exactly as provided. You can find it either via the UI in your Workspace Settings where the context menu of each listed Webhook provides you with a "Copy Secret" option. Alternatively you can get it via the Public API Create or Get Webhook Endpoint

Verify the signature as follows:

  1. Read the raw request body before parsing the JSON. Changes to whitespace or key order will cause verification to fail.
  2. Calculate the HMAC-SHA256 digest of the raw body using your signing secret, encoded as hexadecimal.
  3. Remove v0= from x-superchat-signature and split the remaining value on commas.
  4. Compare each signature with your calculated digest using a constant-time comparison. If none matches, or the header is missing or unsupported, respond with 401 Unauthorized.
    Multiple signatures may be included during secret rotation. A match with any signature is sufficient.

Example Code

const { createHmac, timingSafeEqual } = await import("node:crypto");

const secret = "05BHcfRtIWe3yKwvcYEKJv25+8NEQe9lFeTyIFVBxwk=";
const headers = { "x-superchat-signature": "v0=215af6e32689ca0f15599ed7d6fd441632373c1272f2f295155350aeb56b2e47,441632373c1272f2f295155350aeb56b2e47215af6e32689ca0f15599ed7d6fd" }
const rawBody = Buffer.from(`{"id":"pe_ADghlKJYWcG0zIQSukOG3","operation":"removed","conversation":{"id":"cv_MFJSKWwK4RIYK9c8wUtOX","url":"/conversations/cv_MFJSKWwK4RIYK9c8wUtOX"},"users":[{"id":"us_SOnTUvM5IDVfVNUEsoBEc","first_name":"Timon","last_name":"Christiansen","email":"[email protected]","phone":null,"role":"admin","url":"/users/us_SOnTUvM5IDVfVNUEsoBEc"}],"timestamp":"2026-11-10T15:38:52.668616186Z","event":"conversation_assignments_changed"}`);

function sign(body, signingSecret) {
  return createHmac("sha256", signingSecret).update(body).digest("hex");
}

function verify(body, signatureHeader, signingSecret) {
  if (!signatureHeader?.startsWith("v0=")) return false;

  const expected = Buffer.from(sign(body, signingSecret), "hex");

  return signatureHeader.slice(3).split(",").some((candidate) => {
    if (!/^[a-f0-9]{64}$/.test(candidate)) return false;
    return timingSafeEqual(Buffer.from(candidate, "hex"), expected);
  });
}

verify(rawBody, headers["x-superchat-signature"], secret) // returns "true"

How to mitigate Replay Attacks

The signature confirms that the request came from Superchat, but it doesn't protect against replay attacks. Use the id field in the webhook payload as an idempotency key to avoid processing the same event more than once.