Labs · webhook signatures
Raw bytes, or it didn’t happen.
A webhook signature proves who sent the bytes — the exact bytes. Change one space and the proof no longer holds. Sign a synthetic delivery event, choose what your handler receives, and watch the receiver decide.
Synthetic secret and payload — nothing is sent or received
1 · The sender signs Oveyon, simulated
- x-oveyon-event
- delivered
- x-oveyon-timestamp
- 1793196000
- x-oveyon-signature
- sha256=b9d35141aa1c55df7c50541170092c37c9f13d462c721f46d82c8d5f760c3409
HMAC-SHA256 over timestamp + "." + body with the fixture secret synthetic-fixture-only-not-a-production-secret.
2 · Your handler receives The transport
Bytes handed to verify 222 bytes
{
"event": "delivered",
"messageId": "mock-message-0001",
"deliveryId": "mock-recipient-0001",
"recipient": "[email protected]",
"status": "delivered",
"detail": {},
"timestamp": "2026-10-28T12:00:00.000Z"
}3 · The receiver decides Your application
Signature verified
The handler hashed the exact signed bytes inside the 300-second window. Store the event, then deduplicate it.
- received
- sha256=b9d35141aa1c55df7c50541170092c37c9f13d462c721f46d82c8d5f760c3409
- computed
- sha256=b9d35141aa1c55df7c50541170092c37c9f13d462c721f46d82c8d5f760c3409
- age
- 0 s of 300 s
Precomputed at build time. Change the body or the transport to verify in your browser.
Verify before you parse
Most frameworks parse JSON before your handler runs. Ask for the raw body on the webhook route, verify it, and only then call JSON.parse. Compare signatures in constant time and reject stale timestamps; a valid signature on an old request is still a replay.
This is the receiver used by the signed delivery webhook recipe. It also stores each event once and keeps the newest state per recipient.
export function verifySignature(raw: Buffer, headers: Headers, secret: string, nowSeconds: number) {
if (raw.byteLength > 65_536 || secret.length < 16 || !/^\d{10,12}$/.test(headers.timestamp) || !/^sha256=[a-f0-9]{64}$/.test(headers.signature)) return false;
const timestamp = Number(headers.timestamp);
if (!Number.isSafeInteger(timestamp) || !Number.isFinite(nowSeconds) || Math.abs(nowSeconds - timestamp) > 300) return false;
const expected = createHmac('sha256', secret).update(`${headers.timestamp}.`).update(raw).digest();
const given = Buffer.from(headers.signature.slice(7), 'hex');
return given.length === expected.length && timingSafeEqual(expected, given);
}
What this lab shows
- The header format follows the Oveyon API contract:
x-oveyon-signature: sha256=…overtimestamp.body. - The 300-second window is this recipe’s choice. Pick yours deliberately.
- The secret is a published fixture. Never paste a real webhook secret into a web page — this one does not ask.
- Your browser’s Web Crypto computes every signature. The unit tests prove it matches
node:cryptoand the recipe receiver.
Source: Oveyon API reference — webhooks.