Skip to content

Sign inbound webhook requests

Every inbound POST requires X-Timestamp, X-Nonce, and X-Signature. The timestamp is Unix seconds and must be within plus or minus 300 seconds of server time. The nonce must contain 1 to 128 visible non-whitespace ASCII characters; a UUID is recommended. A nonce is reserved per webhook for 600 seconds, so replaying the same nonce is rejected.

Compute lowercase hex HMAC-SHA256 with the display-once signing secret over the exact bytes:

timestamp + ":" + nonce + ":" + raw_request_body

Do not parse and reserialize JSON between signing and sending. Even harmless whitespace or key-order changes alter the signed bytes.

Curl:

Terminal window
TS=$(date +%s); NONCE=$(uuidgen); BODY='{"event":"example"}'
SIG=$(printf '%s:%s:%s' "$TS" "$NONCE" "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | awk '{print $2}')
curl -X POST "$WEBHOOK_URL" -H "Content-Type: application/json" -H "X-Timestamp: $TS" -H "X-Nonce: $NONCE" -H "X-Signature: $SIG" -d "$BODY"

Node:

const body = JSON.stringify({ event: "example" });
const ts = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomUUID();
const signature = crypto.createHmac("sha256", secret).update(`${ts}:${nonce}:${body}`).digest("hex");

Python:

body = b'{"event":"example"}'
signed = timestamp.encode() + b":" + nonce.encode() + b":" + body
signature = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()

arinova.webhook.create_webhook, arinova.webhook.get_webhook

Also known as

webhook signing, HMAC, X-Signature, X-Timestamp, X-Nonce, sender integration


Machine-readable Markdown · Knowledge index

Build a7f47a5ca54ddcf7806cd48b81ce1b9827042766