Webhook
Bona Fide gửi một yêu cầu HTTPS POST đến endpoint của bạn cho mọi sự kiện phong bì mà bạn đăng ký nhận. Các lần gửi tuân theo
đặc tả Standard Webhooks, nên bất kỳ thư viện nào của đặc tả này cũng có thể xác minh chúng.
Đăng ký endpoint
Phần tiêu đề “Đăng ký endpoint”Tại Settings › Webhooks, hoặc bằng một khóa API có phạm vi quyền org:manage:
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"] }'Phản hồi chứa secret (whsec_…), chỉ duy nhất lần này. Danh sách events rỗng nghĩa là đăng ký nhận mọi sự kiện.
URL phải là https:// công khai. Một endpoint thuộc về một chế độ: endpoint tạo bằng khóa thử nghiệm chỉ nhận
sự kiện của phong bì ở chế độ thử nghiệm, endpoint trực tiếp chỉ nhận sự kiện trực tiếp.
PATCH /v1/webhook-endpoints/{id} thay đổi URL, sự kiện hoặc mô tả, và tạm dừng hoặc tiếp tục endpoint
(enabled). POST /v1/webhook-endpoints/{id}/test gửi một ping webhook.test.
Sự kiện
Phần tiêu đề “Sự kiện”| Loại | Khi nào |
|---|---|
envelope.created |
Phong bì đã được tạo |
envelope.ready |
Tài liệu đã được kết xuất và phong bì sẵn sàng cho người nhận |
recipient.sent |
Đã đến lượt một người nhận (và, với phương thức gửi qua email, liên kết của họ đã được gửi) |
recipient.viewed |
Một người nhận mở trang ký lần đầu tiên |
recipient.signed |
Một người nhận đã gửi (hoặc được ký đối ứng tự động) |
recipient.declined |
Một người nhận đã từ chối |
envelope.completed |
Mọi người nhận đã xong. data.values chứa các biến và mọi giá trị đã nhập |
envelope.sealed |
Tệp PDF đã niêm phong sẵn sàng để tải xuống |
envelope.declined |
Phong bì kết thúc vì một người nhận đã từ chối |
envelope.voided |
Bạn đã hủy phong bì |
envelope.expired |
expiresAt đã qua trước khi hoàn tất |
signing_link.used |
Một khách truy cập đã bắt đầu phong bì từ liên kết ký luôn hoạt động |
acceptance.created được dành sẵn trong schema và hiện chưa được phát.
Payload
Phần tiêu đề “Payload”{ "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 là toàn bộ phong bì tại thời điểm xảy ra sự kiện. data.values chỉ có trên
envelope.completed: các biến bạn đã gửi, và giá trị các trường của từng vai trò theo id trường.
Header và chữ ký
Phần tiêu đề “Header và chữ ký”Mỗi lần gửi đều mang:
| Header | Giá trị |
|---|---|
webhook-id |
Id sự kiện (evt_…); giống nhau ở mọi lần thử lại. Dùng để chống trùng lặp |
webhook-timestamp |
Số giây Unix khi lần thử này được ký |
webhook-signature |
v1,<base64 HMAC-SHA256>, phân tách bằng dấu cách khi có nhiều secret cùng ký |
user-agent |
BonaFide-Webhooks/1 |
Chữ ký là HMAC-SHA256 trên {webhook-id}.{webhook-timestamp}.{raw body}, với khóa là phần đã giải mã base64
của secret sau whsec_. Hãy xác minh nội dung thô (trước khi phân tích JSON), so sánh trong thời gian hằng định, và từ chối
dấu thời gian lệch quá năm phút so với đồng hồ của bạn.
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);}Thử lại
Phần tiêu đề “Thử lại”Một lần gửi thành công khi nhận được bất kỳ mã 2xx nào trong vòng 10 giây. Chuyển hướng không được theo. Mọi trường hợp khác được thử lại với
thời gian chờ tăng dần, sau 5 giây, 5 phút, 30 phút, 2 giờ, 5 giờ, 10 giờ, 14 giờ, 20 giờ và 24 giờ,
tối đa ba ngày sau sự kiện. Sau đó lần gửi được đánh dấu là thất bại.
- Phản hồi nhanh và xử lý bất đồng bộ; chống trùng lặp dựa trên
webhook-id. - Các lần gửi có thể đến không theo thứ tự: hãy kiểm tra
data.envelope.statusthay vì giả định một trình tự. GET /v1/webhook-endpoints/{id}/deliveriesliệt kê các lần thử gần đây kèm mã trạng thái, lỗi và thời gian, và mỗi endpoint hiển thị tỷ lệ thành công trong 24 giờ (health).POST /v1/events/{id}/redelivergửi lại một sự kiện đến một endpoint, chẳng hạn sau một sự cố gián đoạn.
Xoay vòng secret
Phần tiêu đề “Xoay vòng secret”POST /v1/webhook-endpoints/{id}/secret/rotate trả về secret mới ngay lập tức. Trong 24 giờ tiếp theo, mọi
lần gửi được ký bằng cả secret mới và secret cũ (hai mục trong webhook-signature), nhờ đó bạn có thể
triển khai secret mới mà không bỏ lỡ sự kiện nào.