Verify the signature
Verify the signature of every delivery before processing it. Without it, anyone who discovers
your URL can forge a transaction.paid.
The algorithm
Section titled “The algorithm”- Read the raw body, byte for byte, before any JSON parsing. Re-serializing the JSON changes spacing and order and invalidates the signature.
- From the
X-Auryon-Signatureheader, taket(Unix seconds) and everyv1. - Build
"<t>.<raw body>"and computeHMAC-SHA256with the webhook key, in hexadecimal. - Accept if any
v1matches, comparing in constant time. - Reject if
tis more than 5 minutes away from your clock: that is the protection against replaying a captured delivery.
The webhook key (whsec_…) is shown only once, when the webhook is created or the key is rotated.
Node.js
Section titled “Node.js”import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300;
/** * Verifies the X-Auryon-Signature header of a webhook delivery. * rawBody: the request body exactly as received (string or Buffer), before any JSON parsing. */export function verifyAuryon(rawBody, header, secret, nowSeconds = Math.floor(Date.now() / 1000)) { let timestamp = null; const signatures = []; for (const part of String(header ?? '').split(',')) { const [key, value = ''] = part.trim().split('=', 2); if (key === 't' && /^\d+$/.test(value)) timestamp = Number(value); if (key === 'v1') signatures.push(value); } if (timestamp === null || Math.abs(nowSeconds - timestamp) > TOLERANCE_SECONDS) return false;
const expected = createHmac('sha256', secret) .update(`${timestamp}.`) .update(rawBody) .digest('hex'); return signatures.some( (s) => s.length === expected.length && timingSafeEqual(Buffer.from(s), Buffer.from(expected)), );}With Express, read the raw body on this route only:
app.post('/webhooks/auryon', express.raw({ type: 'application/json' }), (req, res) => { if (!verifyAuryon(req.body, req.get('X-Auryon-Signature'), process.env.AURYON_WEBHOOK_SECRET)) { return res.sendStatus(400); } const event = JSON.parse(req.body); // enqueue `event` and respond right away res.sendStatus(200);});<?php
const AURYON_TOLERANCE_SECONDS = 300;
/** * Verifies the X-Auryon-Signature header of a webhook delivery. * $rawBody: the request body exactly as received — file_get_contents('php://input'). */function verify_auryon(string $rawBody, string $header, string $secret, ?int $now = null): bool{ $now = $now ?? time(); $timestamp = null; $signatures = []; foreach (explode(',', $header) as $part) { [$key, $value] = array_pad(explode('=', trim($part), 2), 2, ''); if ($key === 't' && ctype_digit($value)) { $timestamp = (int) $value; } if ($key === 'v1') { $signatures[] = $value; } } if ($timestamp === null || abs($now - $timestamp) > AURYON_TOLERANCE_SECONDS) { return false; }
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret); foreach ($signatures as $signature) { if (hash_equals($expected, $signature)) { return true; } } return false;}$raw = file_get_contents('php://input');if (!verify_auryon($raw, $_SERVER['HTTP_X_AURYON_SIGNATURE'] ?? '', getenv('AURYON_WEBHOOK_SECRET'))) { http_response_code(400); exit;}$event = json_decode($raw, true);http_response_code(200);Python
Section titled “Python”import hashlibimport hmacimport time
TOLERANCE_SECONDS = 300
def verify_auryon(raw_body: bytes, header: str, secret: str, now: int | None = None) -> bool: """Verifies the X-Auryon-Signature header of a webhook delivery.
raw_body: the request body exactly as received (bytes), before any JSON parsing. """ now = int(time.time()) if now is None else now timestamp = None signatures = [] for part in header.split(","): key, _, value = part.strip().partition("=") if key == "t" and value.isdigit(): timestamp = int(value) elif key == "v1": signatures.append(value) if timestamp is None or abs(now - timestamp) > TOLERANCE_SECONDS: return False
expected = hmac.new( secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256 ).hexdigest() return any(hmac.compare_digest(expected, s) for s in signatures)With Flask:
@app.post("/webhooks/auryon")def auryon(): raw = request.get_data() if not verify_auryon(raw, request.headers.get("X-Auryon-Signature", ""), os.environ["AURYON_WEBHOOK_SECRET"]): abort(400) event = json.loads(raw) return "", 200Key rotation
Section titled “Key rotation”When you rotate in the dashboard, for 24 hours Auryon signs with the new key and the previous
one: the header carries two v1=, the new key’s first. That is why the rule is to accept any
v1 that matches. Switch the key on your server within the 24 hours, without losing any event.
The legacy format has no such window: the token goes in the body, and from the rotation on only the new one is sent.
Test vector
Section titled “Test vector”Use these values to test your implementation. With now equal to timestamp, verifying the body
below with secret must return true; with any byte of the body changed, or with now more than
300 seconds later, false. With signature_header_during_rotation, both keys (secret and
previous_secret) must pass.
{ "secret": "whsec_exemplo", "previous_secret": "whsec_anterior", "timestamp": 1781524800, "body": "{\"id\":\"evt_01JXEXEMPLO0000000000000000\",\"api_version\":\"2026-06-15\",\"type\":\"transaction.paid\",\"created\":1781524800,\"livemode\":true,\"data\":{\"object\":{\"id\":\"txn_exemplo_123\",\"status\":\"APROVADA\",\"payment_method\":\"PIX\",\"payment_method_detail\":null,\"amount\":\"19900\",\"shipping_amount\":\"0\",\"currency\":\"BRL\",\"installments\":1,\"identifier\":\"SEU-PEDIDO-123\",\"metadata\":{\"origem\":\"exemplo\"},\"offer_code\":\"ABC12345\",\"product\":{\"name\":\"Produto de exemplo\"},\"offer\":{\"name\":\"Oferta principal\"},\"customer\":{\"name\":\"Comprador de Exemplo\",\"email\":\"comprador@exemplo.test\"},\"created_at\":\"2026-06-15T12:00:00.000Z\",\"paid_at\":\"2026-06-15T12:00:00.000Z\"}}}", "signature_header": "t=1781524800,v1=18e77f9767e224aec6941555b5ce2ee0ab482b88e63b0e9b85375077f6977aea", "signature_header_during_rotation": "t=1781524800,v1=18e77f9767e224aec6941555b5ce2ee0ab482b88e63b0e9b85375077f6977aea,v1=71b3f6f314abc7cbc9d041d8903492f2ab6b0f62cbbb88faedefc460d816a1df"}The three examples on this page are tested against this vector every time the documentation is built.

