Verifying signatures

Every signed webhook carries:

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

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

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 — treat unsigned deliveries as untrusted hints, never as facts.