Webhooks
Bona Fide שולחת בקשת HTTPS מסוג POST לנקודת הקצה שלכם על כל אירוע מעטפה שנרשמתם אליו. המשלוחים פועלים לפי
המפרט Standard Webhooks, כך שכל אחת מהספריות שלו יכולה לאמת אותם.
רישום נקודת קצה
Section titled “רישום נקודת קצה”בהגדרות › Webhooks, או עם מפתח API שיש לו את ההרשאה 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"] }'התגובה כוללת את secret (whsec_…), רק הפעם הזו. רשימת events ריקה נרשמת לכל האירועים.
כתובת ה-URL חייבת להיות ציבורית ולהתחיל ב-https://. נקודת קצה שייכת למצב אחד: נקודות קצה שנוצרו עם מפתח בדיקה מקבלות רק
אירועים של מעטפות במצב בדיקה, ונקודות קצה חיות מקבלות רק אירועים חיים.
PATCH /v1/webhook-endpoints/{id} משנה את כתובת ה-URL, את האירועים או את התיאור, ומשהה או מחדש את נקודת הקצה
(enabled). POST /v1/webhook-endpoints/{id}/test שולח פינג webhook.test.
אירועים
Section titled “אירועים”| סוג | מתי |
|---|---|
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: המשתנים ששלחתם, וערכי השדות של כל תפקיד לפי מזהה שדה.
כותרות וחתימה
Section titled “כותרות וחתימה”כל משלוח כולל:
| כותרת | ערך |
|---|---|
webhook-id |
מזהה האירוע (evt_…); זהה בכל ניסיון חוזר. משמש למניעת כפילויות |
webhook-timestamp |
שניות Unix ברגע שבו הניסיון הזה נחתם |
webhook-signature |
v1,<base64 HMAC-SHA256>, מופרדים ברווח כשיותר מסוד אחד חותם |
user-agent |
BonaFide-Webhooks/1 |
החתימה היא HMAC-SHA256 על {webhook-id}.{webhook-timestamp}.{raw body}, כשהמפתח הוא החלק של הסוד שאחרי 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);}ניסיונות חוזרים
Section titled “ניסיונות חוזרים”משלוח מצליח בכל תגובת 2xx תוך 10 שניות. הפניות (redirects) לא נעקבות. כל דבר אחר מנוסה שוב
עם השהיה הולכת וגדלה: אחרי 5 שניות, 5 דקות, 30 דקות, שעתיים, 5 שעות, 10 שעות, 14 שעות, 20 שעות ו-24 שעות,
לכל היותר שלושה ימים אחרי האירוע. אחר כך המשלוח מסומן כנכשל.
- עונים מהר ומעבדים באופן אסינכרוני; מונעים כפילויות לפי
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), כך שאפשר
לפרוס את הסוד החדש בלי לאבד אירועים.