Skip to content

Verify the signature

Verify the signature of every delivery before processing it. Without it, anyone who discovers your URL can forge a transaction.paid.

  1. Read the raw body, byte for byte, before any JSON parsing. Re-serializing the JSON changes spacing and order and invalidates the signature.
  2. From the X-Auryon-Signature header, take t (Unix seconds) and every v1.
  3. Build "<t>.<raw body>" and compute HMAC-SHA256 with the webhook key, in hexadecimal.
  4. Accept if any v1 matches, comparing in constant time.
  5. Reject if t is 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.

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);
import hashlib
import hmac
import 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 "", 200

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.

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.