---
{
  "id": "platform.webhook.signing",
  "topic": "webhook",
  "title": "Sign inbound webhook requests",
  "locale": "en",
  "version": "2026-08-09",
  "summary": "Senders authenticate the exact request body with HMAC-SHA256 over timestamp, nonce, and body using three required headers.",
  "content": "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.\n\nCompute lowercase hex HMAC-SHA256 with the display-once signing secret over the exact bytes:\n\n`timestamp + \":\" + nonce + \":\" + raw_request_body`\n\nDo not parse and reserialize JSON between signing and sending. Even harmless whitespace or key-order changes alter the signed bytes.\n\nCurl:\n```sh\nTS=$(date +%s); NONCE=$(uuidgen); BODY='{\"event\":\"example\"}'\nSIG=$(printf '%s:%s:%s' \"$TS\" \"$NONCE\" \"$BODY\" | openssl dgst -sha256 -hmac \"$WEBHOOK_SECRET\" -hex | awk '{print $2}')\ncurl -X POST \"$WEBHOOK_URL\" -H \"Content-Type: application/json\" -H \"X-Timestamp: $TS\" -H \"X-Nonce: $NONCE\" -H \"X-Signature: $SIG\" -d \"$BODY\"\n```\n\nNode:\n```js\nconst body = JSON.stringify({ event: \"example\" });\nconst ts = Math.floor(Date.now() / 1000).toString();\nconst nonce = crypto.randomUUID();\nconst signature = crypto.createHmac(\"sha256\", secret).update(`${ts}:${nonce}:${body}`).digest(\"hex\");\n```\n\nPython:\n```python\nbody = b'{\"event\":\"example\"}'\nsigned = timestamp.encode() + b\":\" + nonce.encode() + b\":\" + body\nsignature = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()\n```\n",
  "aliases": [
    "webhook signing",
    "HMAC",
    "X-Signature",
    "X-Timestamp",
    "X-Nonce",
    "sender integration"
  ],
  "tags": [
    "authentication",
    "hmac",
    "replay-protection",
    "security"
  ],
  "relatedActions": [
    "arinova.webhook.create_webhook",
    "arinova.webhook.get_webhook"
  ],
  "relatedActionPrefixes": [],
  "url": "https://docs.arinova.ai/en/kb/webhook/signing/"
}
---

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:
```sh
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:
```js
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:
```python
body = b'{"event":"example"}'
signed = timestamp.encode() + b":" + nonce.encode() + b":" + body
signature = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
```
