Webhook
Bona Fide は、サブスクライブしたエンベロープのイベントごとに、エンドポイントへ HTTPS の POST を送信します。配信は
Standard Webhooks 仕様に従っているため、その仕様のどのライブラリでも検証できます。
エンドポイントの登録
Section titled “エンドポイントの登録”設定 › 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:// である必要があります。エンドポイントは 1 つのモードに属します。テストキーで作成したエンドポイントは
テストモードのエンベロープのイベントだけを、本番のエンドポイントは本番のイベントだけを受信します。
PATCH /v1/webhook-endpoints/{id} は、URL、イベント、説明を変更し、エンドポイントを一時停止または再開します
(enabled)。POST /v1/webhook-endpoints/{id}/test は webhook.test の ping を送信します。
| タイプ | タイミング |
|---|---|
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 ごと)が格納されます。
ヘッダーと署名
Section titled “ヘッダーと署名”各配信には次のヘッダーが含まれます。
| ヘッダー | 値 |
|---|---|
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 として解析する前の)生の本文を検証し、定数時間で比較して、
自分の時計と 5 分以上ずれているタイムスタンプは拒否してください。
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 時間後に行われ、
イベントの発生から最大 3 日間続きます。その後、配信は失敗として記録されます。
- すばやく応答し、処理は非同期で行ってください。重複排除には
webhook-idを使います。 - 配信は順不同で届くことがあります。順序を前提にせず、
data.envelope.statusを確認してください。 GET /v1/webhook-endpoints/{id}/deliveriesは、最近の試行をステータスコード、エラー、所要時間とともに一覧表示します。 また、各エンドポイントには 24 時間の成功率(health)が表示されます。POST /v1/events/{id}/redeliverは、たとえば障害の後などに、イベントをエンドポイントへ再送します。
シークレットのローテーション
Section titled “シークレットのローテーション”POST /v1/webhook-endpoints/{id}/secret/rotate は、新しいシークレットを直ちに返します。その後 24 時間は、すべての
配信が新旧両方のシークレットで署名されるため(webhook-signature に 2 つのエントリー)、イベントを取りこぼすことなく
新しいシークレットをデプロイできます。