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

# Verifying signatures

> Confirm a webhook really came from Kua.

Every webhook Kua sends carries an `X-Kua-Signature` header. Verify it with your
endpoint's signing secret (`whsec_…`) before trusting the payload.

## Headers

| Header              | Description                               |
| ------------------- | ----------------------------------------- |
| `X-Kua-Signature`   | `t={unix_seconds},v1={hex_hmac}`          |
| `X-Kua-Event-Type`  | The event type, e.g. `payroll.disbursed`. |
| `X-Kua-Event-Id`    | The event's unique id (use it to dedupe). |
| `X-Kua-Delivery-Id` | This delivery attempt's id.               |

## How the signature is computed

Kua computes `HMAC-SHA256(secret, "{t}.{raw_body}")` and hex-encodes it as the
`v1` value, where `t` is the timestamp in the header and `raw_body` is the exact
bytes of the request body.

## Verify (Node.js)

```js theme={null}
import crypto from "node:crypto";

export function verifyKuaSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  // constant-time compare
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

<Warning>
  Verify against the **raw request body**, before any JSON parsing/re-serialization
  — re-encoding changes the bytes and breaks the signature.
</Warning>

## Guard against replays

The `t` timestamp lets you reject stale deliveries — e.g. ignore anything older
than a few minutes. Combined with deduping on `X-Kua-Event-Id`, this makes your
handler safe against retries and replays.
