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

# Verify signatures

> Authenticate webhook requests using the endpoint signing secret.

Every webhook request is signed with the secret returned when you create the endpoint. Verify the signature before parsing or acting on the JSON body.

## Signed headers

| Header              | Format         | Description                                                          |
| ------------------- | -------------- | -------------------------------------------------------------------- |
| `webhook-id`        | UUID           | Delivery ID. It remains unchanged when the same delivery is retried. |
| `webhook-timestamp` | Unix timestamp | Time the delivery attempt was signed, in whole seconds.              |
| `webhook-signature` | `v1,{base64}`  | Versioned HMAC-SHA256 signature.                                     |

Header names are case-insensitive. Your framework may expose them in lowercase.

## Signing input

Wircle signs the following UTF-8 string:

```text theme={null}
{webhook-id}.{webhook-timestamp}.{raw-request-body}
```

The HMAC key is the Base64-decoded value after the `whsec_` prefix in your endpoint secret. The resulting SHA-256 digest is Base64-encoded and returned with a `v1,` prefix.

Do not parse and re-serialize the body before verification. Whitespace, property order, and character encoding are part of the signed value, so verification must use the exact raw bytes received over HTTP.

## Verification procedure

1. Read `webhook-id`, `webhook-timestamp`, and `webhook-signature` from the request headers.
2. Reject missing headers and unrecognized secret or signature prefixes.
3. Parse the timestamp and reject requests outside a short tolerance. Five minutes is recommended.
4. Construct the signing input using the raw request body.
5. Calculate the HMAC-SHA256 digest using the decoded endpoint secret.
6. Compare the expected and received signatures with a constant-time comparison.
7. Parse and process the JSON only after verification succeeds.

## Node.js example

```ts theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

type VerificationInput = {
  rawBody: string;
  webhookId: string | undefined;
  webhookTimestamp: string | undefined;
  webhookSignature: string | undefined;
  secret: string;
};

export function verifyWircleWebhook({
  rawBody,
  webhookId,
  webhookTimestamp,
  webhookSignature,
  secret,
}: VerificationInput) {
  if (
    !webhookId
    || !webhookTimestamp
    || !webhookSignature?.startsWith('v1,')
    || !secret.startsWith('whsec_')
  ) {
    return false;
  }

  const timestamp = Number(webhookTimestamp);
  const toleranceSeconds = 5 * 60;

  if (
    !Number.isInteger(timestamp)
    || Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds
  ) {
    return false;
  }

  const encodedSecret = secret.slice('whsec_'.length);
  const signingKey = Buffer.from(encodedSecret, 'base64');

  // Wircle endpoint secrets decode to exactly 32 bytes.
  if (
    signingKey.length !== 32
    || signingKey.toString('base64') !== encodedSecret
  ) {
    return false;
  }

  const signingInput = `${webhookId}.${webhookTimestamp}.${rawBody}`;
  const expected = `v1,${createHmac('sha256', signingKey)
    .update(signingInput)
    .digest('base64')}`;
  const expectedBytes = Buffer.from(expected);
  const receivedBytes = Buffer.from(webhookSignature);

  return expectedBytes.length === receivedBytes.length
    && timingSafeEqual(expectedBytes, receivedBytes);
}
```

Call the verifier with the raw body supplied by your HTTP framework. If it returns `false`, respond with `400` or `401` and do not process the event.

## Replay protection and idempotency

Signature verification proves that Wircle signed the request, but it does not prevent an already valid request from being replayed within your timestamp tolerance.

After the signature is valid, use `webhook-id` as an idempotency key:

* Store the ID before performing side effects.
* If the ID has already been processed, return a `2xx` response without repeating those side effects.
* Keep processed IDs for at least as long as the [delivery retry window](/webhooks/delivery).

## Retries

The body and `webhook-id` stay the same across retries. Each attempt has a new `webhook-timestamp`, so it also has a new `webhook-signature`. Always verify the headers on the current request instead of storing an earlier signature.

## Secret handling

* Store signing secrets only in server-side secret storage.
* Never expose a secret in browser code, logs, or error responses.
* Each endpoint has its own secret. Select the secret using trusted endpoint configuration, not an unsigned body field.
* If a secret may be compromised, create a replacement endpoint, switch traffic to it, and revoke the old endpoint.
