استقبل الأحداث لحظيًا على خادمك بدل الاستعلام المتكرر. حين يتغيّر شيء — فُتح المستند، أُكِّدت الهوية، اكتمل التوقيع — ترسل «وثائق» طلب POST موقّعًا إلى عنوانك خلال ثوانٍ، فيتفاعل نظامك فورًا بدل سؤال الـAPI مرارًا.
signature_request.completed
t=1754500000,v1=6ff7d3f2b1a0c9e4d5f8a1b0c3a
نظام الـWebhooks عند «وثائق» بسيط ومتين: تُسجّل عنوانًا واحدًا يستقبل الأحداث التي تهمّك، ونتكفّل نحن بإرسالها موقّعةً كلّما تغيّر شيء. لا استطلاع (polling)، ولا استعلام متكرر — يصلك الحدث فور وقوعه.
سجّل عنوان 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 إلى عنوانك، جسمه 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 على الجسم الخام وقارِنه بقيمة v1 في الترويسة، وارفض إن كان الطابع الزمني خارج نافذة السماح. التفاصيل والتنفيذ الكامل في قسم التحقق من التوقيع أدناه.
بعد التحقق، أعِد رمز حالة في النطاق 2xx (مثل 200) خلال أقل من 5 ثوانٍ. أي رمز خارج النطاق يُعامَل كفشل تسليم فتُعاد المحاولة. أجّل العمل الثقيل — قواعد البيانات، البريد، توليد الملفات — إلى مهمّة غير متزامنة بعد إرسال الرد.
تنقسم الأحداث إلى مجموعات: أحداث على مستوى طلب التوقيع signature_request.*، وأحداث لكل موقّع signer.*، بالإضافة إلى ختم المستند والتحقق العام. اشترك في ما تحتاجه فقط عبر enabled_events.
| نوع الحدث | متى يُطلق |
|---|---|
| signature_request.* — على مستوى طلب التوقيع | |
| signature_request.created | أُنشئ طلب توقيع جديد كمسودّة في حسابك قبل إرساله. |
| signature_request.sent | أُرسل الطلب إلى الموقّعين وأصبحت روابط التوقيع فعّالة. |
| signature_request.viewed | فتح أوّل موقّع صفحة التوقيع الخاصّة بالطلب لأوّل مرّة. |
| signature_request.partially_signed | وقّع أحد الموقّعين بينما ما يزال آخرون معلّقين (في الطلبات متعدّدة الموقّعين). |
| signature_request.completed | وقّع جميع الموقّعين واكتمل الطلب؛ يصبح المستند المختوم متاحًا للتنزيل ويُسنَد له مرجع تحقّق عام. |
| signature_request.declined | رفض أحد الموقّعين التوقيع فتوقّف الطلب. |
| signature_request.expired | انقضى موعد الصلاحية expires_at قبل اكتمال التوقيع. |
| signature_request.canceled | ألغيتَ الطلب عبر الـAPI أو اللوحة قبل اكتماله. |
| signer.* — على مستوى كل موقّع | |
| signer.sent | أُرسل رابط التوقيع إلى موقّع بعينه (يُطلق لكل موقّع بدوره في الطلبات المرتّبة). |
| signer.viewed | فتح الموقّع صفحة التوقيع الخاصّة به. |
| signer.otp_verified | أدخل الموقّع رمز التحقق لمرّة واحدة (OTP) المُرسل إلى بريده بنجاح. |
| signer.identity_verified | اجتاز الموقّع تأكيد الهوية — مستند رسمي ومطابقة وجه حيّة عبر Didit — في مستوى AES. |
| signer.signed | أتمّ الموقّع توقيعه على المستند. |
| signer.declined | رفض الموقّع التوقيع مع سبب اختياري. |
| document.sealed و verification.created — الختم والتحقق | |
| document.sealed | اكتملت جميع التواقيع فخُتم للطلب محضر أدلّة موثّق قابل للتحقّق المستقل — يحمل توقيع Ed25519 (يتحقّق منه أي طرف بالمفتاح العام على /trust) وختمًا زمنيًا RFC 3161. لا يُضمَّن توقيع PAdES داخل ملف الـPDF عبر مسار الـAPI. |
| verification.created | أُنشئ سجلّ تحقق عام بمرجع (بصيغة WTQ-) يتيح التأكّد من سلامة المستند علنًا. |
جسم كل طلب Webhook هو كائن 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://sign.wthaiq.com/s/uZ8..",
"viewed_at": 1754000100,
"signed_at": 1754499900,
"identity": { "status": "approved", "provider": "didit", "level": "aes" },
"fields": { "job_title": "مهندس برمجيات", "start_date": "2026-08-01" }
}
],
"ordered": true,
"require_identity": true,
"reference": "WTQ-000123",
"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 يحمل كائن document، وverification.created يحمل كائن verification. اعتمد دائمًا على قيمة type لتحديد كيفية قراءة الحمولة.ترسل «وثائق» مع كل طلب ترويسة Wthaiq-Signature تتيح لك إثبات أن الحمولة صادرة منّا ولم تُعبَث بها. التحقق إلزامي: لا تعالِج أي حمولة قبل نجاح التحقق.
Wthaiq-Signature: t=1754500000,v1=<hmac_sha256 hex>| الخطوة | التفصيل |
|---|---|
| 1 · استخرج | افصل t وv1 من قيمة الترويسة. |
| 2 · ركّب | الحمولة الموقّعة = "{t}.{raw_body}" — أي الطابع الزمني، ثم نقطة، ثم الجسم الخام حرفيًّا. |
| 3 · احسب | احسب HMAC-SHA256 للحمولة الموقّعة بمفتاح = سرّ نقطة الاستقبال whsec_، وأخرِجه hex. |
| 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) على مدى يصل إلى 24 ساعة. بعد استنفاد المحاولات يُوسَم التسليم كفاشل، ويمكنك إعادة إرساله يدويًّا من اللوحة.event.id (بصيغة evt_) وتجاهل أي معرّف عالجته من قبل — لتكون النتيجة واحدة مهما تكرّر التسليم.2xx بمجرّد التحقق وتخزين المعرّف، ثم ادفع العمل الثقيل — تحديث قواعد البيانات، إرسال البريد، توليد الملفات — إلى طابور/مهمّة غير متزامنة. المعالجة المتزامنة الطويلة تُبطّئ الردّ فتُحسَب فشلًا وتُعاد المحاولة.created_at للترتيب، وعند الحاجة إلى الحالة القاطعة استعلِم عن أحدث نسخة عبر GET /v1/signature_requests/{id} بدل الاعتماد على حمولة الحدث وحدها.لا يوجد وضع اختبار منفصل — كل مفتاح sk_ حيّ ويُطلق أحداثًا حقيقية بوضع livemode:true دائمًا، وأي طلب توقيع تنشئه يُرسل بريدًا فعليًا ويُفوتَر. للاختبار الآمن أنشئ طلبًا حقيقيًا صغيرًا واجعل نفسك الموقّع (بريدك الخاص) لتصلك أحداث signature_request.* وsigner.* إلى نقطة استقبالك دون التأثير على عملاء حقيقيين.
أنشئ طلب توقيع بمفتاحك sk_ وبريدك الخاص كمستلم فتصلك أحداث signature_request.* وsigner.* إلى عنوانك للتأكّد من الوصول والتحقق — دون التأثير على عملاء حقيقيين، مع الأخذ في الاعتبار أن الطلب يُفوتَر فعليًا.
أعد إرسال أي حدث سابق بالمعرّف نفسه من اللوحة لاختبار منطق إزالة التكرار ومعالجة الأخطاء دون انتظار حدث جديد.
تعرض اللوحة سجلّ كل محاولة تسليم: رمز الاستجابة، الترويسات، الجسم، وزمن الردّ — لتشخيص أي فشل بدقّة.
# أنشئ طلبًا حقيقيًا صغيرًا (استخدم بريدك كمستلم) لتوليد أحداث حقيقية — سيُخصم من رصيدك
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"}]
}'event.id وتجاهل المكرّر.livemode ثابت على true دائمًا — لا يوجد وضع اختبار يُميَّز به الحدث.enabled_events لتقليل الضجيج والحمل.أعِد حساب HMAC-SHA256 على الحمولة الموقّعة "{t}.{raw_body}" بمفتاح = سرّ نقطة الاستقبال whsec_، وقارِن الناتج hex بقيمة v1 في ترويسة Wthaiq-Signature بمقارنة ثابتة الزمن. ارفض الطلب إذا اختلف التوقيع أو كان الفارق بين الآن وt أكبر من 300 ثانية.
لأن التوقيع محسوب على البايتات كما أُرسلت بالضبط. أي تحليل ثم إعادة تسلسل لـJSON قد يغيّر المسافات أو ترتيب المفاتيح أو ترميز المحارف، فتختلف البايتات ويفشل حساب HMAC ويُرفض طلب سليم. اقرأ الجسم الخام قبل أي middleware يحلّل JSON.
إذا ردّ خادمك بغير 2xx أو تجاوز المهلة، نعيد الإرسال وفق تراجع أُسّي على مدى يصل إلى 24 ساعة. بعد استنفاد المحاولات يُوسَم التسليم كفاشل، ويمكنك إعادة إرساله يدويًّا من لوحة عمليات التسليم بعد إصلاح المشكلة.
لا. قد تصل الأحداث بترتيب مختلف عن ترتيب وقوعها، وقد يتكرّر الحدث نفسه. رتّب باستخدام created_at، وأزل التكرار على event.id، واستعلِم عن أحدث حالة عبر GET /v1/signature_requests/{id} عند الحاجة إلى اليقين.
لا يوجد وضع اختبار منفصل — استخدم مفتاحك الحقيقي sk_ لإنشاء طلب توقيع صغير وبريدك الخاص كمستلم، فتصلك الأحداث الحقيقية إلى نقطة استقبالك (وتُخصم تكلفة الطلب فعليًا). أعد تشغيل الأحداث السابقة من اللوحة لاختبار إزالة التكرار، وافحص سجلّ عمليات التسليم لرؤية رمز الاستجابة والترويسات والجسم وزمن الردّ.
أنشئ نقطة استقبال، تحقّق من التوقيع، وابدأ في التفاعل مع أحداث التوقيع لحظيًا. الدليل السريع يأخذك من الصفر إلى أوّل Webhook يعمل في دقائق.