---
{
  "id": "platform.webhook.signing",
  "topic": "webhook",
  "title": "Inbound Webhook 簽章",
  "locale": "zh-TW",
  "version": "2026-08-09",
  "summary": "Sender 以三個必要 header 與 timestamp、nonce、原始 body 的 HMAC-SHA256 驗證 inbound 請求。",
  "content": "每次 inbound POST 都必須帶 `X-Timestamp`、`X-Nonce`、`X-Signature`。timestamp 使用 Unix seconds，與伺服器時間的差距不可超過正負 300 秒。nonce 必須是 1 到 128 個可見、無空白的 ASCII 字元，建議使用 UUID；同一 webhook 的 nonce 會保留 600 秒，重複使用會被拒絕。\n\n使用只顯示一次的 signing secret，針對下列「完全相同的原始 bytes」計算小寫十六進位 HMAC-SHA256：\n\n`timestamp + \":\" + nonce + \":\" + raw_request_body`\n\n簽完後不可再 parse 並重新序列化 JSON；空白或 key 順序改變都會讓簽章失效。\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 簽章",
    "HMAC",
    "X-Signature",
    "X-Timestamp",
    "X-Nonce",
    "發送端整合"
  ],
  "tags": [
    "authentication",
    "hmac",
    "replay-protection",
    "security"
  ],
  "relatedActions": [
    "arinova.webhook.create_webhook",
    "arinova.webhook.get_webhook"
  ],
  "relatedActionPrefixes": [],
  "url": "https://docs.arinova.ai/zh-tw/kb/webhook/signing/"
}
---

每次 inbound POST 都必須帶 `X-Timestamp`、`X-Nonce`、`X-Signature`。timestamp 使用 Unix seconds，與伺服器時間的差距不可超過正負 300 秒。nonce 必須是 1 到 128 個可見、無空白的 ASCII 字元，建議使用 UUID；同一 webhook 的 nonce 會保留 600 秒，重複使用會被拒絕。

使用只顯示一次的 signing secret，針對下列「完全相同的原始 bytes」計算小寫十六進位 HMAC-SHA256：

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

簽完後不可再 parse 並重新序列化 JSON；空白或 key 順序改變都會讓簽章失效。

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()
```
