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 محجوز في المخطط ولا يُطلَق بعد.
الحمولة
Section titled “الحمولة”{ "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}، ومفتاحه هو الجزء المفكوك من base64
من السر بعد whsec_. تحقّق من الجسم الخام (قبل تحليل 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 ثوانٍ. ولا تُتبع عمليات إعادة التوجيه. وأي شيء آخر تُعاد محاولته
بتأخير متزايد، بعد 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 سرًا جديدًا على الفور. وخلال الساعات الأربع والعشرين التالية يُوقَّع كل
تسليم بكل من السر الجديد والقديم (إدخالان في webhook-signature)، لكي تتمكن من
نشر السر الجديد دون فقدان أي أحداث.