Webhooks — إشعارات لحظية بحالة كل توقيع.
استقبل الأحداث على خادمك لحظة وقوعها. فحين يتغيّر شيء، كأن يُفتح المستند أو تُؤكَّد الهوية أو يكتمل التوقيع، ترسل «وثائق» طلب POST موقَّعًا إلى عنوانك خلال ثوانٍ، فيستجيب نظامك فورًا دون الحاجة إلى الاستعلام المتكرّر عبر واجهة برمجة التطبيقات (API).
signature_request.completed
t=1754500000,v1=6ff7d3f2b1a0c9e4d5f8a1b0c3a
أربع خطوات من الحدث إلى ردّ خادمك
نظام إشعارات الأحداث في «وثائق» بسيط ومتين: تُسجّل عنوانًا واحدًا يستقبل الأحداث التي تهمّك، ونتولّى نحن إرسالها إليه موقَّعةً كلّما تغيّر شيء. فلا حاجة إلى الاستطلاع الدوري (polling) ولا إلى الاستعلام المتكرّر، إذ يصلك الحدث فور وقوعه.
أنشئ نقطة استقبال (endpoint)
سجّل عنوان HTTPS عبر POST /v1/webhook_endpoints وحدّد الأحداث التي تشترك فيها ضمن enabled_events. يعيد الرد كائن webhook_endpoint يتضمّن السرّ whsec_ الذي ستستخدمه لاحقًا في التحقّق.
| الحقل | النوع | الوصف |
|---|---|---|
| url | string مطلوب | عنوان HTTPS يستقبل طلبات POST. يجب أن يكون متاحًا للعموم وأن يردّ بسرعة. |
| enabled_events | string[] مطلوب | قائمة أنواع الأحداث التي تُرسَل إلى هذا العنوان. أدرِج ما تحتاجه فقط. |
curl -X POST https://wthaiq.com/api/v1/webhook_endpoints \
-H "Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Wthaiq-Version: 2026-07-01" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.acme.com/hooks/wthaiq",
"enabled_events": ["signature_request.completed", "signer.signed"]
}'{
"id": "we_1a",
"object": "webhook_endpoint",
"url": "https://api.acme.com/hooks/wthaiq",
"enabled_events": ["signature_request.completed", "signer.signed"],
"status": "enabled",
"secret": "whsec_7bK2c8Vd1QpN9sR4tHmZ0xY", // يظهر مرّة واحدة — احفظه بأمان
"created_at": 1754000000
}whsec_ إلا مرّة واحدة عند الإنشاء. خزّنها في متغيّر بيئة آمن، فستحتاج إليها في كل عملية تحقّق. ولكل نقطة استقبال سرّها المستقل.استقبل طلب POST
عند وقوع أي حدث اشتركت فيه، ترسل «وثائق» طلب POST إلى عنوانك، وجسمه بصيغة JSON كائن event يضمّ الكائن المتأثّر في data.object، مع ترويسة التوقيع Wthaiq-Signature.
POST /hooks/wthaiq HTTP/1.1
Host: api.acme.com
Content-Type: application/json
Wthaiq-Version: 2026-07-01
Wthaiq-Signature: t=1754500000,v1=6ff7d3f2b1a0c9e4d5f8a1b0c3a...
{"id":"evt_2M8kQ1","object":"event","type":"signature_request.completed", ... }تحقّق من التوقيع
قبل الوثوق بأي حمولة، أعِد حساب توقيع HMAC SHA-256 على الجسم الخام (raw body) وقارِنه بقيمة v1 في الترويسة، وارفض الطلب إن كان طابعه الزمني خارج نافذة السماح. وتجد التفاصيل والتنفيذ الكامل في قسم التحقّق من التوقيع أدناه.
ردّ بـ 2xx بسرعة
بعد التحقّق، أعِد رمز حالة في النطاق 2xx (مثل 200) خلال أقل من 5 ثوانٍ. وأي رمز خارج هذا النطاق يُعَدّ فشلًا في التسليم، فتُعاد المحاولة. أمّا العمل الثقيل، كتحديث قواعد البيانات وإرسال البريد وتوليد الملفات، فأجِّله إلى مهمّة غير متزامنة بعد إرسال الردّ.
جميع أنواع الأحداث ومتى تُطلق
تنقسم الأحداث إلى مجموعات: أحداث على مستوى طلب التوقيع signature_request.*، وأحداث خاصة بكل موقّع signer.*، إلى جانب أحداث ختم المستند والتحقّق العام. اشترك في ما تحتاجه فقط عبر enabled_events.
| نوع الحدث | متى يُطلق |
|---|---|
| signature_request.* — على مستوى طلب التوقيع | |
| signature_request.created | أُنشئ طلب توقيع جديد في حسابك، وحُفظ مسودّةً قبل إرساله. |
| signature_request.sent | أُرسل الطلب إلى الموقّعين، وأصبحت روابط التوقيع مفعّلة. |
| signature_request.viewed | فتح أحد الموقّعين صفحة التوقيع الخاصّة بالطلب لأوّل مرّة، من رابط البريد أو واتساب أو جلسة التوقيع المضمّنة (signing_session). |
| signature_request.partially_signed | وقّع أحد الموقّعين، وما يزال توقيع آخرين معلّقًا (في الطلبات متعدّدة الموقّعين). |
| signature_request.completed | وقّع جميع الموقّعين واكتمل الطلب، فأصبح المستند المختوم متاحًا للتنزيل وأُسنِد إليه مرجع تحقّق عام. |
| signature_request.declined | رفض أحد الموقّعين التوقيع فتوقّف الطلب. |
| signature_request.expired | حلّ موعد انتهاء الصلاحية expires_at قبل اكتمال التوقيع. |
| signature_request.canceled | ألغيتَ الطلب عبر واجهة برمجة التطبيقات أو لوحة المطوّرين قبل اكتماله. |
| signer.* — على مستوى كل موقّع | |
| signer.sent | أُرسل رابط التوقيع إلى موقّع بعينه (ويُطلَق لكل موقّع عند حلول دوره إذا كان ترتيب التوقيع محدَّدًا). |
| signer.viewed | فتح الموقّع صفحة التوقيع الخاصّة به لأوّل مرّة، من رابط البريد أو واتساب أو جلسة التوقيع المضمّنة (signing_session). |
| signer.otp_verified | أدخل الموقّع بنجاح رمز التحقّق لمرّة واحدة (OTP) المُرسَل إلى بريده. |
| signer.identity_verified | اجتاز الموقّع تأكيد الهوية في مستوى AES، بمستند رسمي ومطابقة حيّة للوجه عبر مزوّد متخصص في التحقّق من الهوية. |
| signer.identity_in_review | أتمّ الموقّع خطوات تأكيد الهوية، وأُحيلت النتيجة إلى مراجعة بشرية، ولا يُتاح له التوقيع حتى يصدر القرار. |
| signer.identity_resubmission_requested | طلب المراجِع من الموقّع إعادة إحدى خطوات تأكيد الهوية، كتصوير المستند أو مطابقة الوجه، في الجلسة نفسها. |
| signer.identity_declined | رُفض تأكيد هوية الموقّع، آليًّا أو بعد المراجعة البشرية، ولا يُتاح له التوقيع ما لم يُعتمد تأكيد لاحق. |
| signer.identity_match_hold | اجتاز الموقّع تأكيد الهوية، لكن الاسم في وثيقته يختلف عن الاسم المرسَل في الطلب، فأُوقف توقيعه حتى يقرّر مراجع الهوية في حسابك. يحمل الحدث الكائن identity_match بالاسمين ونوع الوثيقة وآخر أربعة أرقام منها. |
| signer.identity_match_decided | صدر القرار في اختلاف الاسم من لوحة المطوّرين أو عبر POST /signers/{id}/identity_match: approved فيُستكمل التوقيع، أو rejected فيُوقَف توقيع هذا الطرف. |
| signer.signed | أتمّ الموقّع توقيعه على المستند. |
| signer.declined | رفض الموقّع التوقيع، مع إمكان ذكر السبب. |
| document.sealed و verification.created — الختم والتحقّق | |
| document.sealed | اكتملت جميع التواقيع، فخُتم للطلب محضر أدلّة موثَّق قابل للتحقّق المستقل، يحمل توقيع Ed25519 (يستطيع أي طرف التحقّق منه بالمفتاح العام في /trust) وختمًا زمنيًّا وفق RFC 3161. ولا يُضمَّن توقيع PAdES داخل ملف PDF في مسار واجهة برمجة التطبيقات. ولا يصدر لطلبات التجربة. |
| verification.created | أُنشئ سجلّ تحقّق عام برقم مرجعي (بصيغة WTQ-) يتيح التحقّق العلني من سلامة المستند. ولا يصدر لطلبات التجربة. |
غلاف الحدث والكائن المتأثّر
جسم كل إشعار يصلك هو كائن event موحَّد البنية يضمّ الكائن المتأثّر في data.object. ويدلّ الحقل type على نوع الحدث، و livemode يميّز الوضع الحيّ عن الاختبار، و created_at طابع زمني بصيغة Unix بالثواني.
{
"id": "evt_...",
"object": "event",
"type": "signature_request.completed",
"created_at": 1754500000,
"livemode": true,
"data": {
"object": { /* الكائن المتأثّر: signature_request أو signer أو … */ }
}
}مثال كامل للحدث signature_request.completed ومعه كائن signature_request بكامل حقوله في data.object:
{
"id": "evt_2M8kQ1",
"object": "event",
"type": "signature_request.completed",
"created_at": 1754500000,
"livemode": true,
"data": {
"object": {
"id": "sr_3n8Kd2Qa1V",
"object": "signature_request",
"livemode": true,
"status": "completed",
"title": "عقد عمل — أحمد م.",
"legal_level": "aes",
"format": "pades-lt",
"source": { "type": "template", "template_id": "tpl_employment" },
"signers": [
{
"id": "sgr_9fA2",
"object": "signer",
"name": "أحمد محمد",
"email": "ahmed@example.com",
"type": "individual",
"method": "draw",
"require_identity": true,
"order": 1,
"status": "signed",
"signing_url": "https://wthaiq.com/sign.php?token=uZ8..",
"viewed_at": 1754000100,
"signed_at": 1754499900,
"identity": { "status": "approved", "provider": "external", "level": "aes" },
"fields": { "job_title": "مهندس برمجيات", "start_date": "2026-08-01" }
}
],
"ordered": true,
"require_identity": true,
"reference": "WTQ-1A2B3C4D5E6F",
"reminders": { "enabled": true, "interval_hours": 48, "max": 3 },
"expires_at": 1755000000,
"completed_at": 1754500000,
"download_url": "https://wthaiq.com/api/v1/signature_requests/sr_3n8Kd2Qa1V/download",
"metadata": { "order_id": "A-1024" },
"created_at": 1754000000
}
}
}data.object بحسب type. فأحداث signer.* تحمل كائن signer، و document.sealed يحمل كائن signature_request، و verification.created يحمل كائن verification. اعتمد دائمًا على قيمة type لتحديد كيفية قراءة الحمولة.تحقّق من كل طلب قبل الوثوق به
ترسل «وثائق» مع كل طلب ترويسة Wthaiq-Signature تتيح لك التأكّد من أن الحمولة صادرة منّا وأنه لم يُعبَث بها. والتحقّق إلزامي: لا تعالِج أي حمولة قبل نجاحه.
Wthaiq-Signature: t=1754500000,v1=<hmac_sha256 hex>t = الطابع الزمني للإرسال بصيغة Unix (بالثواني). v1 = توقيع HMAC SHA-256 بالصيغة الست عشرية (hex).
خطوات التحقّق
| الخطوة | التفصيل |
|---|---|
| 1 · استخرج | افصل t و v1 من قيمة الترويسة. |
| 2 · ركّب | الحمولة الموقّعة = "{t}.{raw_body}" — أي الطابع الزمني، ثم نقطة، ثم الجسم الخام حرفيًّا. |
| 3 · احسب | احسب HMAC-SHA256 للحمولة الموقّعة، على أن يكون المفتاح سرَّ نقطة الاستقبال whsec_، وأخرِج الناتج بالصيغة الست عشرية. |
| 4 · قارِن | قارِن الناتج بـv1 مقارنةً ثابتة الزمن (hash_equals / timingSafeEqual) لتفادي هجمات التوقيت. |
| 5 · النافذة | ارفض الطلب إذا كان |now - t| > 300 ثانية (5 دقائق)، للحماية من هجمات إعادة الإرسال (replay). |
تنفيذ التحقّق
const express = require('express');
const crypto = require('crypto');
const app = express();
const SECRET = process.env.WTHAIQ_WEBHOOK_SECRET; // whsec_...
const TOLERANCE = 300; // ثوانٍ
// مهم: استقبل الجسم الخام (Buffer)، لا JSON المُحلَّل
app.post('/hooks/wthaiq',
express.raw({ type: 'application/json' }),
(req, res) => {
const raw = req.body; // Buffer خام
const header = req.get('Wthaiq-Signature') || '';
// 1) استخرج t و v1
const parts = Object.fromEntries(
header.split(',').map(p => p.split('=')));
const t = parts.t, v1 = parts.v1;
// 2) نافذة الطابع الزمني (5 دقائق)
const now = Math.floor(Date.now() / 1000);
if (!t || Math.abs(now - Number(t)) > TOLERANCE)
return res.status(400).send('timestamp out of tolerance');
// 3) أعد حساب HMAC على "{t}.{raw_body}"
const signedPayload = t + '.' + raw.toString('utf8');
const expected = crypto
.createHmac('sha256', SECRET)
.update(signedPayload)
.digest('hex');
// 4) مقارنة ثابتة الزمن
const ok = v1 && expected.length === v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
if (!ok) return res.status(400).send('invalid signature');
const event = JSON.parse(raw.toString('utf8'));
// أزل التكرار على event.id، ثم ردّ فورًا وعالِج لاحقًا
res.status(200).send('ok');
});import hmac, hashlib, time, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["WTHAIQ_WEBHOOK_SECRET"].encode() # whsec_... كبايتات
TOLERANCE = 300 # ثوانٍ
@app.post("/hooks/wthaiq")
def handle():
raw = request.get_data() # بايتات خام — لا تستخدم request.json
header = request.headers.get("Wthaiq-Signature", "")
# 1) استخرج t و v1
parts = dict(kv.split("=", 1) for kv in header.split(",") if "=" in kv)
t, v1 = parts.get("t"), parts.get("v1")
# 2) نافذة الطابع الزمني (5 دقائق)
if not t or abs(time.time() - int(t)) > TOLERANCE:
abort(400)
# 3) أعد حساب HMAC على "{t}.{raw_body}"
signed_payload = f"{t}.".encode() + raw
expected = hmac.new(SECRET, signed_payload, hashlib.sha256).hexdigest()
# 4) مقارنة ثابتة الزمن
if not v1 or not hmac.compare_digest(expected, v1):
abort(400)
event = request.get_json()
# أزل التكرار على event["id"]، ثم ردّ فورًا وعالِج لاحقًا
return "", 200<?php
$secret = getenv('WTHAIQ_WEBHOOK_SECRET'); // whsec_...
$tolerance = 300; // ثوانٍ
// اقرأ الجسم الخام — لا تستخدم $_POST مطلقًا
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_WTHAIQ_SIGNATURE'] ?? '';
// 1) استخرج t و v1
$parts = [];
foreach (explode(',', $header) as $kv) {
[$k, $v] = array_pad(explode('=', $kv, 2), 2, '');
$parts[$k] = $v;
}
$t = $parts['t'] ?? '';
$v1 = $parts['v1'] ?? '';
// 2) نافذة الطابع الزمني (5 دقائق)
if ($t === '' || abs(time() - (int)$t) > $tolerance) {
http_response_code(400);
exit('timestamp out of tolerance');
}
// 3) أعد حساب HMAC على "{t}.{raw_body}"
$signedPayload = $t . '.' . $raw;
$expected = hash_hmac('sha256', $signedPayload, $secret);
// 4) مقارنة ثابتة الزمن
if ($v1 === '' || !hash_equals($expected, $v1)) {
http_response_code(400);
exit('invalid signature');
}
$event = json_decode($raw, true);
// أزل التكرار على $event['id']، ثم ردّ فورًا وعالِج لاحقًا
http_response_code(200);
echo 'ok';hash_equals و timingSafeEqual فتُجريان المقارنة في زمن ثابت أيًّا كان موضع الاختلاف.ماذا يحدث عند الفشل، وكيف تبني معالجًا متينًا
2xx (أو لم يردّ)، نعيد الإرسال تلقائيًّا وفق تراجع أُسّي (exponential backoff) على مدى يصل إلى نحو 46 ساعة. وبعد استنفاد المحاولات التسع يُصنَّف التسليم فاشلًا. ويمكنك إعادة إرسال أي إشعار بزرّ «إعادة إرسال الإشعار» في صفحة الإشعارات ببوابة المطوّرين أو عبر POST /v1/events/{id}/redeliver، واستدراك الأحداث الفائتة عبر GET /v1/events.event.id (بصيغة evt_) وتجاهل أي معرّف سبق أن عالجته، لتبقى النتيجة واحدة مهما تكرّر التسليم.2xx فور التحقّق من التوقيع وتخزين المعرّف، ثم انقل العمل الثقيل، كتحديث قواعد البيانات وإرسال البريد وتوليد الملفات، إلى طابور أو مهمّة غير متزامنة. فالمعالجة المتزامنة الطويلة تُبطئ الردّ، فتُحسَب فشلًا وتُعاد المحاولة.created_at لترتيبها، وإذا احتجت إلى الحالة النهائية المؤكَّدة فاستعلِم عن أحدث نسخة عبر GET /v1/signature_requests/{id} بدل الاعتماد على حمولة الحدث وحدها.اختبر تكاملك قبل الإطلاق
ابنِ تكاملك بمفتاح تجربة sk_test_؛ فأحداثه تحمل livemode:false ولا يُخصم به شيء ولا تُرسَل به رسالة إلى أحد. وسجّل نقطة الاستقبال بالمفتاح التجريبي نفسه عبر POST /v1/webhook_endpoints لتصلها أحداث التجربة، فالنقاط المضافة من لوحة المطوّرين حيّة. ثم أنشئ طلبًا تجريبيًّا ببريدك وافتح رابط التوقيع بنفسك، لتصلك أحداث signature_request.* و signer.* إلى نقطة استقبالك دون التأثير على عملاء حقيقيين.
أرسِل حدثًا حقيقيًّا لنفسك
أنشئ طلب توقيع بمفتاحك sk_ واجعل بريدك الخاص هو المستلم، فتصلك أحداث signature_request.* و signer.* إلى عنوانك لتتأكّد من وصولها ونجاح التحقّق منها، دون التأثير على عملاء حقيقيين، علمًا بأن تكلفة الطلب تُحتسَب فعليًّا.
أرسل حدثًا تجريبيًّا
أرسل من لوحة المطوّرين حدثًا تجريبيًّا إلى نقطة استقبالك لتتحقّق من التوقيع والمعالجة، واستدرك الأحداث السابقة بمعرّفاتها عبر GET /v1/events، وأعِد إرسال أي إشعار سابق بعينه من صفحة الإشعارات أو عبر POST /v1/events/{id}/redeliver.
افحص عمليات التسليم
تعرض لوحة المطوّرين آخر عمليات التسليم لكل نقطة: الحالة وعدد المحاولات ورمز الاستجابة ووقتها، لتشخّص أي فشل.
# أنشئ طلبًا حقيقيًا صغيرًا (استخدم بريدك كمستلم) لتوليد أحداث حقيقية — يُحتسب إرسالًا من رصيد باقتك
curl -X POST https://wthaiq.com/api/v1/signature_requests \
-H "Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Wthaiq-Version: 2026-07-01" \
-H "Content-Type: application/json" \
-d '{
"title": "اختبار Webhook",
"source": {"type":"template","template_id":"tpl_employment"},
"legal_level": "ses",
"signers": [{"name":"مطوّر","email":"you@example.com","method":"draw"}]
}'قائمة تحقّق الإنتاج
- تحقّق من كل طلب قبل معالجته؛ لا تثق بأي حمولة غير موقّعة أو خارج نافذة الطابع الزمني.
- استخدم الجسم الخام حرفيًّا في حساب HMAC، قبل أي تحليل JSON.
- اجعل معالجتك لا تتأثّر بالتكرار بتخزين
event.idوتجاهل المكرّر. - ردّ 2xx بسرعة ثم أنجز العمل الثقيل في مهمّة غير متزامنة.
- لا تفترض ترتيبًا؛ واستعلِم عن أحدث حالة عبر واجهة برمجة التطبيقات إذا احتجت إلى التيقّن منها.
- خصّص سرًّا لكل نقطة استقبال واحفظه في متغيّر بيئة، واستبدله دوريًّا.
- افصل أحداث التجربة عن الحيّة. حقل
livemodeيساويfalseفي أحداث مفاتيح التجربة؛ فلا تُنفّذ عليها أثرًا حقيقيًّا في نظامك. - اشترك في ما تحتاجه فقط عبر
enabled_eventsلتقليل الضجيج والحمل. - راقب عمليات التسليم من لوحة المطوّرين، وأعد الإرسال عند اللزوم.
- استخدم HTTPS دائمًا ولا تكشف السرّ في السجلّات أو رسائل الخطأ.
أسئلة المطوّرين حول الـWebhooks
كيف أتحقّق من صحّة الطلب الوارد؟
أعِد حساب HMAC-SHA256 على الحمولة الموقّعة "{t}.{raw_body}" بمفتاح = سرّ نقطة الاستقبال whsec_، وقارِن الناتج hex بقيمة v1 في ترويسة Wthaiq-Signature بمقارنة ثابتة الزمن. ارفض الطلب إذا اختلف التوقيع أو كان الفارق بين الآن و t أكبر من 300 ثانية.
لماذا يجب استخدام الجسم الخام تحديدًا؟
لأن التوقيع محسوب على البايتات كما أُرسلت بالضبط. أي تحليل ثم إعادة تسلسل لـJSON قد يغيّر المسافات أو ترتيب المفاتيح أو ترميز المحارف، فتختلف البايتات ويفشل حساب HMAC ويُرفض طلب سليم. اقرأ الجسم الخام قبل أي middleware يحلّل JSON.
ماذا يحدث إذا فشل خادمي في الاستجابة؟
إذا ردّ خادمك بغير 2xx أو تجاوز المهلة، نعيد الإرسال وفق تراجع أُسّي على مدى يقارب 46 ساعة. بعد استنفاد المحاولات يُوسَم التسليم كفاشل، وتستطيع استدراك الأحداث الفائتة عبر GET /v1/events بعد إصلاح المشكلة.
هل ترتيب وصول الأحداث مضمون؟
لا. قد تصل الأحداث بترتيب مختلف عن ترتيب وقوعها، وقد يتكرّر الحدث نفسه. رتّب باستخدام created_at، وأزل التكرار على event.id، واستعلِم عن أحدث حالة عبر GET /v1/signature_requests/{id} عند الحاجة إلى اليقين.
كيف أختبر تكاملي قبل الإطلاق؟
استخدم مفتاح تجربة sk_test_ وسجّل به نقطة استقبالك عبر POST /v1/webhook_endpoints، ثم أنشئ طلبًا تجريبيًّا ببريدك وافتح رابط التوقيع بنفسك، فتصلك الأحداث بقيمة livemode: false دون أي خصم. وأرسل حدثًا تجريبيًّا من اللوحة، وافحص سجلّ عمليات التسليم لرؤية حالة كل محاولة ورمز الاستجابة.
جاهز لاستقبال أوّل حدث؟
أنشئ نقطة استقبال، وتحقّق من التوقيع، وابدأ التفاعل مع أحداث التوقيع لحظة وقوعها. ويقودك دليل البدء السريع من نقطة الصفر إلى تشغيل أوّل إشعار آلي خلال دقائق.