# Verifying signatures

Every **signed** webhook carries:

```http
content-type: application/json
x-newinstance-timestamp: <unix ms>
x-newinstance-signature: sha256=<hex HMAC-SHA256(secret, raw request body)>
```

Verify against the **raw body bytes**, before any JSON parsing or re-serialisation, and compare in constant time. Reject payloads whose embedded `timestamp` is older than a few minutes to blunt replays.

**Node**
```js
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(secret, rawBody, signatureHeader) {
  const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`;
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader ?? '');
  return a.length === b.length && timingSafeEqual(a, b);
}
```

[PHP](/php)
```php
function verify(string $secret, string $rawBody, ?string $signatureHeader): bool {
    $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $signatureHeader ?? '');
}
```

If no secret is configured for the destination, the signature headers are **omitted entirely**, so treat unsigned deliveries as untrusted hints, never as facts.
