Skip to main content
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.
Do not parse and re-serialise the JSON before verification. That can change whitespace or field order and break the signature check.

Example verifier

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.