> ## Documentation Index
> Fetch the complete documentation index at: https://sendmux.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify webhook signatures

> Verify the HMAC-SHA256 signature on every Sendmux webhook delivery.

Every webhook POST includes `X-Sendmux-Signature` in the form `sha256=<hex>`. Verify it before you process the event.

## What to sign

* Use the raw request body bytes.
* Use the webhook signing secret returned when the subscription was created or rotated.
* Compute `HMAC-SHA256` and compare the full `sha256=<hex>` value.
* Compare in constant time.

<Warning>
  Do not parse and re-serialise the JSON before verification. That can change
  whitespace or field order and break the signature check.
</Warning>

## Example verifier

```js theme={null}
const encoder = new TextEncoder();

function toHex(buffer) {
  return [...new Uint8Array(buffer)]
    .map((byte) => byte.toString(16).padStart(2, "0"))
    .join("");
}

function constantTimeEqual(a, b) {
  if (a.length !== b.length) return false;

  let result = 0;
  for (let i = 0; i < a.length; i += 1) {
    result |= a.charCodeAt(i) ^ b.charCodeAt(i);
  }
  return result === 0;
}

export async function verifySendmuxWebhook(request, secret) {
  const rawBody = await request.text();
  const key = await crypto.subtle.importKey(
    "raw",
    encoder.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );

  const digest = await crypto.subtle.sign("HMAC", key, encoder.encode(rawBody));
  const expected = `sha256=${toHex(digest)}`;
  const received = request.headers.get("X-Sendmux-Signature") ?? "";

  if (!constantTimeEqual(received, expected)) {
    return { ok: false };
  }

  return {
    ok: true,
    body: JSON.parse(rawBody),
    eventId: request.headers.get("X-Sendmux-Event-Id"),
    eventType: request.headers.get("X-Sendmux-Event-Type"),
  };
}
```

## Common mistakes

* Reading parsed JSON instead of the raw body.
* Comparing without the `sha256=` prefix.
* Using a normal string comparison for secrets.
* Processing retries without deduping on `X-Sendmux-Event-Id`.

## Endpoint behaviour

Return a `2xx` status once the event is accepted. For longer processing, store the event first, return quickly, and process it after the response.

Sendmux retries non-`2xx` responses and timeouts. See [Webhooks setup](/docs/guides/webhooks-setup#retries).
