Webhook
針對您訂閱的每個信封事件,Bona Fide 都會向您的端點傳送 HTTPS POST 要求。傳遞內容遵循 Standard Webhooks 規格,因此可以使用該規格的任何程式庫進行驗證。
註冊端點
標題為「註冊端點」的章節在 設定 › Webhook 中註冊,或使用具有 org:manage 權限範圍的 API 金鑰註冊:
curl https://api.bonafidesign.com/v1/webhook-endpoints \ -H "Authorization: Bearer $BONA_FIDE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.example.com/hooks/bona-fide", "description": "Production", "events": ["envelope.completed", "envelope.sealed", "envelope.declined"] }'回應中包含 secret(whsec_…),僅此一次。events 清單為空時,表示訂閱所有事件。URL 必須是可公開存取的 https:// 位址。端點只屬於一種模式:以測試金鑰建立的端點只會接收測試模式信封的事件,正式端點則只接收正式事件。
PATCH /v1/webhook-endpoints/{id} 可變更 URL、事件或描述,也可以暫停或繼續端點(enabled)。POST /v1/webhook-endpoints/{id}/test 會傳送一個 webhook.test 測試要求。
| 類型 | 觸發時機 |
|---|---|
envelope.created |
信封已建立 |
envelope.ready |
文件已轉譯,信封已準備好交給收件者 |
recipient.sent |
輪到某位收件者(若為電子郵件傳遞,其連結已寄出) |
recipient.viewed |
某位收件者第一次開啟簽署頁面 |
recipient.signed |
某位收件者已提交(或已自動副署) |
recipient.declined |
某位收件者拒絕簽署 |
envelope.completed |
所有收件者皆已完成。data.values 包含變數和所有填寫的值 |
envelope.sealed |
已封存的 PDF 可供下載 |
envelope.declined |
由於有收件者拒絕簽署,信封已終止 |
envelope.voided |
您已作廢該信封 |
envelope.expired |
在完成之前已超過 expiresAt |
signing_link.used |
有訪客透過常設簽署連結發起了信封 |
acceptance.created 已在結構描述中保留,但目前尚不會發出。
{ "id": "evt_9sKq2Lm4Tz", "type": "envelope.completed", "createdAt": "2026-10-13T14:09:52.004Z", "testMode": false, "data": { "envelope": { "id": "env_8Gm2kQ4rT1", "status": "completed", "seal": "pending", "recipients": [{ "id": "rcp_5Jw0pLs9Xe", "role": "client", "status": "completed", "…": "…" }], "externalId": "user_8841", "metadata": {}, "…": "…" }, "values": { "variables": { "client": { "name": "Acme Robotics, Inc." }, "effective_date": "2026-10-13" }, "recipients": { "client": { "client_title": "CEO", "start_date": "2026-11-01" } } } }}data.envelope 是事件發生當時完整的信封。data.values 只會出現在 envelope.completed 中:包括您傳送的變數,以及依欄位 ID 列出的每個角色的欄位值。
標頭與簽章
標題為「標頭與簽章」的章節每次傳遞都帶有:
| 標頭 | 值 |
|---|---|
webhook-id |
事件 ID(evt_…);每次重試都相同。可用來去重複 |
webhook-timestamp |
本次嘗試簽章時的 Unix 時間(秒) |
webhook-signature |
v1,<base64 HMAC-SHA256>,有多個密鑰簽章時以空格分隔 |
user-agent |
BonaFide-Webhooks/1 |
簽章是對 {webhook-id}.{webhook-timestamp}.{raw body} 計算的 HMAC-SHA256,其金鑰為密鑰中 whsec_ 之後的部分經 base64 解碼的結果。請驗證原始本文(在 JSON 剖析之前)、以固定時間進行比較,並拒絕與您的時鐘相差超過五分鐘的時間戳記。
import express from 'express';import { Webhook } from 'standardwebhooks'; // npm i standardwebhooks
const wh = new Webhook(process.env.BONA_FIDE_WEBHOOK_SECRET!); // whsec_…const app = express();
app.post('/hooks/bona-fide', express.raw({ type: 'application/json' }), (req, res) => { let event; try { event = wh.verify(req.body, req.headers as Record<string, string>); // throws if forged or too old } catch { return res.sendStatus(400); } queue.add(event); // do the work asynchronously and answer quickly res.sendStatus(204);});# pip install standardwebhooks flaskimport osfrom flask import Flask, requestfrom standardwebhooks.webhooks import Webhook
wh = Webhook(os.environ["BONA_FIDE_WEBHOOK_SECRET"]) # whsec_…app = Flask(__name__)
@app.post("/hooks/bona-fide")def bona_fide(): try: event = wh.verify(request.get_data(), dict(request.headers)) except Exception: return "", 400 enqueue(event) return "", 204import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(secret: string, headers: Record<string, string>, rawBody: string) { const id = headers['webhook-id']; const ts = Number(headers['webhook-timestamp']); if (!id || !ts || Math.abs(Date.now() / 1000 - ts) > 300) throw new Error('stale or missing headers'); const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64'); const expected = createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest(); const ok = (headers['webhook-signature'] ?? '').split(' ').some((part) => { const [version, sig] = part.split(','); const given = Buffer.from(sig ?? '', 'base64'); return version === 'v1' && given.length === expected.length && timingSafeEqual(given, expected); }); if (!ok) throw new Error('bad signature'); return JSON.parse(rawBody);}在 10 秒內傳回任何 2xx 即視為傳遞成功。不會跟隨重新導向。其他任何情況都會以退避方式重試,分別在 5 秒、5 分鐘、30 分鐘、2 小時、5 小時、10 小時、14 小時、20 小時和 24 小時後進行,最長持續到事件發生後三天。之後,該次傳遞會標示為失敗。
- 請快速回應並以非同步方式處理;依
webhook-id去重複。 - 傳遞內容可能不依順序送達:請檢查
data.envelope.status,而不要假設事件的先後順序。 GET /v1/webhook-endpoints/{id}/deliveries會列出最近的傳遞嘗試及其狀態碼、錯誤和耗時,每個端點也會顯示其 24 小時成功率(health)。POST /v1/events/{id}/redeliver會將事件重新傳送到端點,例如在服務中斷之後。
輪替密鑰
標題為「輪替密鑰」的章節POST /v1/webhook-endpoints/{id}/secret/rotate 會立即傳回新的密鑰。在接下來的 24 小時內,每次傳遞都會同時以新密鑰和舊密鑰簽章(webhook-signature 中會有兩個項目),讓您可以在不遺漏事件的情況下部署新密鑰。