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

# Verifying signatures

> Check the IC-Signature header so you only trust real events.

Every delivery carries three headers:

| Header         | Value                                                               |
| -------------- | ------------------------------------------------------------------- |
| `IC-Signature` | Lowercase hex HMAC-SHA256 over `{timestamp}.{raw body}`.            |
| `IC-Timestamp` | Unix time in **seconds** when we signed the request.                |
| `IC-Event-Id`  | The event's unique id (matches `id` in the body). Use it to dedupe. |

## The algorithm

1. Read the **raw** request body as bytes/text — do **not** re-serialise
   the parsed JSON, or whitespace/key-order differences will break the
   signature.
2. Reject the request if `now - IC-Timestamp` is greater than **300
   seconds** (5-minute replay window).
3. Compute `HMAC_SHA256(secret, "{IC-Timestamp}.{raw body}")` as hex.
4. Compare it to `IC-Signature` with a **constant-time** comparison.
5. Only if it matches, parse the body and dedupe on `IC-Event-Id`.

The `secret` is the `whsec_…` value shown once when you created the
endpoint.

## Node.js example

```javascript theme={null}
import crypto from 'node:crypto';

// `rawBody` is the exact bytes/string we POSTed — capture it before JSON parsing.
export function verifyWebhook(rawBody, headers, secret) {
  const signature = headers['ic-signature'];
  const timestamp = headers['ic-timestamp'];
  if (!signature || !timestamp) return false;

  // 5-minute replay window.
  const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(ageSeconds) || ageSeconds > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  // Constant-time compare — bail if lengths differ (timingSafeEqual throws otherwise).
  const a = Buffer.from(signature, 'hex');
  const b = Buffer.from(expected, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

<Warning>
  Verify the signature on the **raw** body, before parsing. Frameworks
  that auto-parse JSON (Express `express.json()`, Next.js route handlers)
  may not expose the raw bytes by default — configure a raw-body reader
  for your webhook route.
</Warning>

## If verification fails

Return a `4xx` and do nothing else. A failed signature means the request
did not come from us (or was tampered with) — never process it. If
legitimate events start failing verification, confirm you're using the
right endpoint's secret and signing over the raw body.
