Developers
Check a webhook's signature
Every webhook we send carries a UQT-Signature header. Check it before you act
on the event, so you know the message came from us and was not changed or replayed.
The header
The header has a timestamp t (Unix time, in seconds) and one or more
signatures v1, each 64 hexadecimal characters:
UQT-Signature: t=TIMESTAMP,v1=SIGNATURE. While a signing secret is being
replaced, the header carries one v1 for each secret; a match with any one is
enough.
The steps
- Read
tand everyv1from the header. -
Join the timestamp, a full stop and the exact body you received, byte for byte:
TIMESTAMP.BODY. Do not parse and re-encode the body first. - Make the HMAC-SHA256 of that text with your endpoint's signing secret, written in lower-case hexadecimal.
-
Compare it with each
v1using a constant-time comparison. Accept the message if one matches. -
Refuse it if
tand your clock are further apart than 300 seconds, so an old message cannot be sent again later.
A worked example
With these made-up values, your check should accept the message. The example body leaves out the amount a real event carries, because this website shows no money figures.
- Signing secret:
example_signing_secret - Timestamp:
1790000000 -
Body:
{"id":"7d3c0f2e-1b4a-4c8e-9f21-5a6b7c8d9e01","type":"token.redeemed","created_at":"2026-09-28T10:00:00.000Z","data":{"order_ref":"your-order-123","unit":"UQT.USD"}} -
UQT-Signatureheader:t=1790000000,v1=4329603348ece7518cbffec54e941dfa0a58cd3fccc689990afad3f1ca238c05
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, body, header, nowSeconds = Date.now() / 1000) {
const m = /^t=(\d{1,12})((?:,v1=[0-9a-f]{64}){1,5})$/.exec(header ?? '');
if (!m) return false;
const t = Number(m[1]);
if (Math.abs(nowSeconds - t) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${body}`, 'utf8').digest();
return m[2].split(',v1=').slice(1)
.some((sig) => timingSafeEqual(expected, Buffer.from(sig, 'hex')));
}
Never put your signing secret in a web page or share it. If you lose it, remove the endpoint and add it again to get a new one.