> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# webhooks: أحداث التحقق في الوقت الفعلي وتوقيع HMAC

> استقبل أحداث التحقق من الهوية والكيانات والمعاملات من Didit في الوقت الفعلي، مع توقيع HMAC-SHA256 ووجهات متعددة وإعادة محاولات محدودة.

> [!NOTE]
>
> يُوثَّق نشاط المصادقة وتسليم webhooks المرتبط بالإصدار في [أحداث المصادقة وwebhooks](https://docs.didit.me/ar/authentication/events-and-webhooks.md). لا تسجّل أنواع أحداث المصادقة غير المدرجة مع واجهة برمجة التطبيقات الحالية للوجهات.

موجّه تكامل webhook
        نسخ الموجّه إلى الحافظة



      
      

\<pre>

\`Add a Didit webhook endpoint to my application to receive real-time identity verification, entity, and transaction events.

\## What to Build
A POST endpoint (e.g., /api/webhooks/didit) that:
1\. Reads the raw request body (do NOT parse JSON before signature verification)
2\. Verifies the HMAC-SHA256 signature (X-Signature-V2 recommended)
3\. Validates the timestamp is fresh (within 5 minutes)
4\. Parses the JSON body and processes the event by webhook_type
5\. Returns 2xx ASAP and updates state idempotently

\## Signature Headers
\- X-Signature-V2: HMAC-SHA256 over the sorted, Unicode-preserved canonical JSON (recommended).
\- X-Signature: HMAC-SHA256 over the exact raw bytes (works only if your stack does not re-encode the body).
\- X-Signature-Simple: HMAC-SHA256 over "{timestamp}:{session_id}:{status}:{webhook_type}" (DEPRECATED last-resort fallback; authenticates the envelope only, NOT the decision body — if you verify with it, re-fetch the decision from GET /v3/session/{session_id}/decision/ instead of trusting the payload).
\- X-Timestamp: Unix epoch seconds when Didit dispatched the webhook.
\- X-Signature-V2-Previous, X-Signature-Previous, X-Signature-Simple-Previous: sent only for 24 hours after a destination's signing secret is rotated in the Business Console. Same signatures, made with the replaced secret. Never sent for the legacy application webhook_url.

Verification:
1\. abs(now - X-Timestamp) <= 300 seconds
2\. Recompute the signature using your destination secret
3\. Compare with constant-time comparison against the header and, when present, its -Previous header (accept a match on either), so verification keeps working while you deploy a rotated secret

\## Webhook Event Types (use exact values in destination.subscribed_events)
\- "status.updated": session or business session status changed
\- "data.updated": session or business-session verification data was edited
\- "user.status.updated": User entity status changed ("ACTIVE" / "FLAGGED" / "BLOCKED")
\- "user.data.updated": User entity profile or aggregate data changed
\- "business.status.updated": Business entity status changed ("ACTIVE" / "FLAGGED" / "BLOCKED")
\- "business.data.updated": Business entity profile or aggregate data changed
\- "activity.created": reserved for the activity timeline (currently only sent by Try Webhook tests)
\- "transaction.created": transaction created and initial verdict ready
\- "transaction.status.updated": transaction status changed
\- "travel_rule.status.updated": a Travel Rule exchange's status changed (see /transaction-monitoring/travel-rule)
\- "workflow.compliance_changed": the compliance status of a published workflow changed after a re-evaluation
\- "transaction_rule_suggestions.created": a transaction rule suggestion run proposed new rule changes
\- "investigator.report.ready": the Investigator wrote a report on an item
\- "investigator.decision.automated": the Investigator decided an item on its own
\- "investigator.decision.overridden": a person took another action than the Investigator's report, or overrode a decision it took on its own
\- "investigator.decision.contested": the end user contested a decision and asked for a human review
\- "investigator.question.raised": the Investigator asked the team a question about its policy
\- "investigator.automation.paused": automation stopped deciding, paused by a person or at the monthly spending limit
\- "investigator.rfi.answered": the end user answered an information request
\- "investigator.rfi.overdue": an information request passed its deadline unanswered
\- "investigator.drift.detected": a queue drifted and automation stepped down a tier
\- "investigator.sla.breached": a report waited past its queue's SLA without a decision
\- "investigator.qa.failed": a decision a person took failed the Investigator's quality review
\- "investigator.external.report.ready": the report on an item you sent from outside Didit is ready

There is no wildcard; include every event family you want to receive. You can create multiple destinations to fan out to different URLs.

\## Webhook Envelope (session events)
\{
"event_id": "uuid",
"session_id": "uuid",
"status": "Approved" | "Declined" | "In Review" | "In Progress" | "Not Started" | "Abandoned" | "Expired" | "Kyc Expired" | "Resubmitted" | "Awaiting User",
"webhook_type": "status.updated",
"created_at": 1627680000,
"timestamp": 1627680000,
"application_id": "uuid",
"environment": "live" | "sandbox",
"sandbox_scenario": "decline_aml_hit" | null,  // armed scenario slug on sandbox sessions; null on live sessions or an unarmed sandbox session
"workflow_id": "uuid",
"workflow_version": 1,
"vendor_data": "your-internal-user-id",
"metadata": { "user_type": "premium" },
"decision": { ... }  // only when Approved / Declined / In Review / Abandoned
}

Status values are exact, case-sensitive strings — note "Kyc Expired" uses a single capital K.

KYB / business-session payloads additionally include "business_session_id" and "session_kind": "business".

\## Decision Object (V3)
The decision mirrors the GET /v3/session/decision/ response. Per-feature data is in plural arrays so a workflow can include multiple instances of the same feature:
\- id_verifications[], nfc_verifications[], liveness_checks[], face_matches[]
\- aml_screenings[], poa_verifications[], phone_verifications[], email_verifications[]
\- ip_analyses[], database_validations[], questionnaire_responses
\- bank_verifications[]: one item per Bank Verification step. Its method is "open_banking" (bank sign-in), "account_lookup" (account check), "statement" or "microdeposits", or "none" when the person continued without a bank before any attempt was evaluated. continued_without_bank: true says the person continued without a bank, and continuation.reason says why.
\- reviews[]
\- signatures[]: only when the workflow has an E-signature step. One item per signer; status "signed" means the documents are signed. Download signed_file and evidence_file with your API key.

Each item carries a node_id so you can correlate it with the workflow graph.
User Verification decisions also carry one biometric_consent object (not an array): required, trigger, status (not_required | pending | given | declined), reason. status "pending" with reason "BIOMETRIC_CONSENT_UNSUPPORTED_SDK" means a native SDK below 4.10.0 captured a face without the consent step, so consent was not collected.

\## Database Update Logic
switch (status):
"Approved"     -> user.verified = true; store decision data
"Declined"     -> user.verification_status = "declined"; log decision.*.warnings
"In Review"    -> user.verification_status = "pending_review"
"In Progress"  -> user.verification_status = "in_progress"
"Resubmitted"  -> reopen the listed feature nodes per resubmit_info
"Abandoned"    -> optionally trigger reminder
"Awaiting User" -> KYB parent session waiting on party verifications; no action needed
"Expired" / "Kyc Expired" -> mark session expired; optionally create a new one

\## Retry Policy
On 5xx, 404, timeout, or connection failure Didit retries up to 2 times:
\- 1st retry: ~1 minute after the initial failure
\- 2nd retry: ~4 minutes after the 1st retry
After that the delivery is dropped. Each delivery attempt is logged separately in the console's Deliveries tab.
Each attempt in a destination's delivery log has an attempt number: 1 for the first delivery of an event, then one more than the attempt it repeats for each retry or resend.
Automatic retries go to the URL the first attempt used; a manual resend goes to the destination's current URL.
Retries and resends are signed again with the destination's current secret and a fresh timestamp.

\## Error Handling
\- Return 2xx as fast as possible; do heavy work asynchronously.
\- Log the raw body for debugging signature failures.
\- Treat duplicate webhooks idempotently (key on event_id, or session_id + status + webhook_type).
\- Delivery order is not guaranteed: never let an older event overwrite newer state. Compare created_at, or re-fetch the decision from GET /v3/session/{session_id}/decision/.
\- HTTPS endpoints only. Webhook receivers behind Cloudflare must whitelist 18.203.201.92.

\## Environment Variable
DIDIT_WEBHOOK_SECRET -- from the Business Console (per-destination secret_shared_key).

\## Testing
Send test webhooks from the Business Console (API & Webhooks > Try Webhook) before going live. The console covers approved, declined, in-review, KYB, entity, activity, and transaction scenarios.

\## Docs
\- Webhooks guide: https://docs.didit.me/integration/webhooks
\- Webhook destinations API: https://docs.didit.me/management-api/webhook/list-destinations
\- Verification statuses: https://docs.didit.me/integration/verification-statuses
\- Decision schema: https://docs.didit.me/sessions-api/retrieve-session

\## Important
\- ALWAYS verify the signature before processing any webhook
\- Use constant-time comparison to prevent timing attacks
\- Store and HMAC the raw JSON string -- never re-stringify after parsing
\- Return 2xx quickly; do heavy processing asynchronously if needed`

\</pre>



      
      

الصق في

## نظرة عامةتُعد webhooks الطريقة التي تُرسل بها Didit **تحديثات فورية** إلى الخادم الخلفي لديك كلما تغيّرت حالة جلسة تحقق، أو جلسة شركة، أو مستخدم مورّد، أو شركة مورّد، أو معاملة. يتيح لك الاشتراك بوجهة واحدة أو أكثر في الأحداث التي تهمك الاستجابةَ بمجرد جاهزية النتيجة، وعادةً خلال ثوانٍ من إكمال المستخدم الخاضع للتحقق لسير عمله.تُعد webhooks نمط التكامل **الموصى به**. الاستطلاع عبر [`GET /v3/session/{id}/decision/`](https://docs.didit.me/ar/sessions-api/retrieve-session) مدعوم كخيار احتياطي، لكنه أبطأ، ويستهلك طلبات أكثر، ويتجاوز أحداثاً مثل `data.updated` و`transaction.status.updated` والتغييرات على مستوى الكيان، وهي أمور لا تُرسل إلا عبر webhooks. اعتبر تسليم webhook مصدر الحقيقة، ولا تستطلع إلا عند بدء التشغيل البارد أو لأغراض المطابقة.يبدأ هذا الفصل من درس التكامل في أكاديمية Didit من نظرة عامة على webhooks.يغطي درس الأكاديمية الخاص بالتكامل موضوع webhooks من الدقيقة 3:12: إضافة وجهة، واختيار الأحداث، والسر المشترك الذي ستستخدمه للتحقق أدناه.[نظرة عامة على webhooks · Didit Academy](https://www.youtube.com/embed/fX03-WEu_EI?start=192\&rel=0\&playsinline=1)> [!NOTE]
>
> **مستخدمو Cloudflare / WAF.** تُرسل Didit webhooks من عنوان IP الثابت `18.203.201.92` مع `User-Agent: DiditWebhook/2.0 +https://didit.me`. إذا كانت الحافة لديك تحجب العملاء غير المعروفين، فاسمح بهذا العنوان في **Security > WAF > Tools > IP Access Rules**، واختر **سماح** لاسم المضيف المستقبِل.## أنواع الأحداثاستخدم القيمة الدقيقة في مصفوفة `subscribed_events` الخاصة بالوجهة. لا يوجد حرف بدل للاختيار الشامل، لذا اذكر كل فئة تريد استلامها، ويمكنك توزيع الأحداث على وجهات متعددة.| الحدث | المورد | متى يُرسَل |
| - | - | - |
| `status.updated` | جلسة KYC أو جلسة شركة KYB | تتغيّر حالة الجلسة (لم تبدأ → قيد التنفيذ → مقبولة / مرفوضة / قيد المراجعة / متروكة / أُعيد تقديمها). تتضمّن payloads الخاصة بـ KYB القيمة `session_kind: "business"` و`business_session_id`. |
| `data.updated` | جلسة KYC أو جلسة شركة KYB | تُعدَّل بيانات التحقق بعد إنشائها (تصحيحات يدوية من المراجِع على KYC وإثبات العنوان وNFC وسجل KYB ومستندات KYB وغيرها). |
| `user.status.updated` | كيان المستخدم | تتغيّر حالة المستخدم الموحّد بين `ACTIVE` و`FLAGGED` و`BLOCKED` (إجراء من وحدة التحكم، أو إجراء بناءً على قاعدة، أو قائمة الحظر). يتضمّن `previous_status`. |
| `user.data.updated` | كيان المستخدم | تتغيّر بيانات ملف المستخدم الموحّد أو عدّاداته أو بياناته الوصفية أو معرّفاته المعتمدة. يتضمّن `changed_fields` وخريطة `changes` تحتوي على القيم `previous` و`current`. |
| `business.status.updated` | كيان الشركة | تتغيّر حالة الشركة الموحّدة بين `ACTIVE` و`FLAGGED` و`BLOCKED` (إجراء من وحدة التحكم، أو إجراء بناءً على قاعدة، أو قائمة الحظر). يتضمّن `previous_status`. |
| `business.data.updated` | كيان الشركة | تتغيّر بيانات ملف الشركة الموحّد أو بيانات تسجيلها أو عدّاداتها أو بياناتها الوصفية أو حقول التحقق المجمّعة. يتضمّن `changed_fields` و`changes`. |
| `activity.created` | الخط الزمني للنشاط | محجوز لتغذية الخط الزمني للنشاط. القيمة مقبولة في `subscribed_events` ويغطيها أداة الاختبار **Try Webhook**، لكن Didit لا تُرسل `activity.created` حاليًا في حركة البيانات الفعلية. |
| `transaction.created` | مراقبة المعاملات | تُنشأ معاملة وتكون نتيجة القواعد الأولية جاهزة. يتضمّن `transaction_id` و`txn_id` و`status` و`score` و`severity` و`amount` و`currency` و`direction` و`action_required`. |
| `transaction.status.updated` | مراقبة المعاملات | تتغيّر حالة المعاملة بعد تقييم القواعد، أو إجراء المحلّل، أو المعالجة، أو مطابقات قائمة الحظر، أو تحديثات المزوّد، أو إجراءات API أو وحدة التحكم. كما يُرسَل مرة واحدة، تغيّرت الحالة أم لم تتغيّر، عند اكتمال فحص العملات المشفرة الذي أُعيدت محاولته في الخلفية ([لم تُفهرَس تجزئات المعاملات بعد](https://docs.didit.me/ar/transaction-monitoring/aml-screening.md#هاشات-المعاملات-غير-المفهرسة-بعد)). |
| `travel_rule.status.updated` | Travel Rule | تتغيّر حالة تبادل [Travel Rule](https://docs.didit.me/ar/transaction-monitoring/travel-rule.md): عند الإنشاء (بما في ذلك الانتقال إلى `NOT_APPLICABLE` تحت الحد)، أو عند تأكيد الملكية، أو عند استجابة استلام البريد الإلكتروني، أو عند الإنهاء أو الإلغاء، أو عند انتهاء المهلة. تُرسَل للتحويلات الصادرة وللتحويلات الواردة أيضًا: نظيرة الواردة عبر المسار `INTERNAL`، وتسجيلات Sunrise، واستفسارات `TRP` الواردة. بالنسبة إلى معاملة أنشأتها Didit، تُرسَل هذه الأحداث بعد `transaction.created` الخاص بها. يتضمّن `transaction_id` و`txn_id` و`travel_rule_status` و`rail` و`direction`. |
| `workflow.compliance_changed` | امتثال سير العمل | تغيّرت حالة امتثال سير عمل منشور بعد إعادة التقييم. حدث واحد لكل سير عمل. يتضمّن `workflow_id` و`workflow_version` و`workflow_label` و`transition` (الحقلان `from` و`to`) و`severity` و`kb_version`، ولا يتضمّن `status`. |
| `transaction_rule_suggestions.created` | مراقبة المعاملات | اقترح تشغيل خاص باقتراحات القواعد تغييرات جديدة على القواعد، وإشعارات webhook مفعّلة في إعدادات اقتراحات القواعد. يتضمّن `run_id` و`open_count` و`suggestions[]`، ولا يتضمّن `status`. |
| `investigator.report.ready` | Investigator | كتب Investigator تقريرًا عن عنصر. |
| `investigator.decision.automated` | Investigator | اتخذ Investigator قرارًا بشأن عنصر بمفرده. |
| `investigator.decision.overridden` | Investigator | اتخذ شخص إجراءً مختلفًا عن تقرير Investigator، أو تجاوز قرارًا اتخذه بمفرده. |
| `investigator.decision.contested` | Investigator | اعترض المستخدم النهائي على قرار وطلب مراجعة بشرية. |
| `investigator.question.raised` | Investigator | وجّه Investigator إلى الفريق سؤالًا حول سياسته. |
| `investigator.automation.paused` | Investigator | توقّفت الأتمتة عن اتخاذ القرارات، إما بإيقاف من شخص أو بسبب بلوغ حد الإنفاق الشهري. |
| `investigator.rfi.answered` | Investigator | ردّ المستخدم النهائي على طلب معلومات. |
| `investigator.rfi.overdue` | Investigator | تجاوز طلب معلومات موعده النهائي دون رد. |
| `investigator.drift.detected` | Investigator | انحرفت قائمة الانتظار، فنزلت الأتمتة مستوى واحدًا. |
| `investigator.sla.breached` | Investigator | انتظر تقرير إلى ما بعد اتفاقية مستوى الخدمة الخاصة بقائمة الانتظار دون صدور قرار. |
| `investigator.qa.failed` | Investigator | أخفق قرار اتخذه شخص في مراجعة الجودة لدى Investigator. |
| `investigator.external.report.ready` | Investigator | أصبح التقرير عن عنصر أرسلته من خارج Didit جاهزًا. |
| `auth.signup` | المصادقة | يسجّل شخص حسابًا عبر [المصادقة](https://docs.didit.me/ar/authentication/events-and-webhooks.md). يتضمّن `auth_event` وملخّص `authentication` الخاص بالشخص. |
| `auth.login` | المصادقة | يسجّل شخص دخوله. يتضمّن `auth_event` وملخّص `authentication` الخاص بالشخص. |
| `auth.login_failed` | المصادقة | تفشل محاولة تسجيل الدخول. تكون قيمة `vendor_data` بقيمة `null` عندما لا تُسمّي المحاولة شخصًا معروفًا. يتضمّن `auth_event` وملخّص `authentication` الخاص بالشخص. |
| `auth.password_reset_requested` | المصادقة | يطلب شخص إعادة تعيين كلمة المرور. يتضمّن `auth_event` وملخّص `authentication` الخاص بالشخص. |
| `auth.password_reset_completed` | المصادقة | يُكمل شخص إعادة تعيين كلمة المرور. يتضمّن `auth_event` وملخّص `authentication` الخاص بالشخص. |
| `auth.mfa_changed` | المصادقة | يضيف شخص أداة مصادقة أو يغيّرها. يتضمّن `auth_event` وملخّص `authentication` الخاص بالشخص. |
| `auth.new_device` | المصادقة | يسجّل شخص دخوله من جهاز لم يستخدمه من قبل. يتضمّن `auth_event` وملخّص `authentication` الخاص بالشخص. |
| `auth.account_locked` | المصادقة | تؤدي سياسة أمنية إلى قفل الحساب، أو يحظر فريقك الشخص. يتضمّن `auth_event` وملخّص `authentication` الخاص بالشخص. |القيم الواردة في هذا الجدول هي المجموعة الكاملة من الأحداث القابلة للاشتراك، ولا يوجد فيها `session.status.updated` ولا `kyc.completed` أو ما شابه ذلك. أما الإرسال الوحيد الآخر الذي تُجريه Didit فهو [الاستدعاء غير الموقّع `kyb.registry_search.resolved`](#استدعاء-نتائج-بحث-سجل-kyb)، ويُرسل إلى `webhook_url` لكل طلب على حدة تمرّره إلى [`POST /v3/kyb/search/`](https://docs.didit.me/ar/standalone-apis/kyb-registry)، وليس إلى وجهة webhook.> [!TIP]
>
> يُرسل الخيار **Try Webhook** من قائمة Business Console > API & Webhooks payloads مكتملة التكوين لكل حدث من هذه الأحداث (مقبولة، ومرفوضة، وقيد المراجعة، وأسماء بترميز Unicode، وKYC/KYB، والكيان، والنشاط، والمعاملة). استخدمه للتحقق من نقطة الاستقبال (endpoint) لديك قبل الانتقال إلى الإنتاج.## بنية الحمولة### الهيكل العاميشترك كل webhook في الهيكل العام نفسه. تُسلسَل الحقول دائمًا باستخدام `sort_keys=True` وبفواصل مضغوطة (`,` / `:`)، حتى تنتج الحمولة نفسها التوقيع نفسه على الطرفين.| الحقل | النوع | ملاحظات |
| - | - | - |
| `event_id` | `uuid` | معرّف ثابت للحدث. يُرسَل **المعرّف نفسه** `event_id` إلى كل وجهة يتفرع إليها الحدث، ويُعاد استخدامه في المحاولات المتكررة، لذا اجعل idempotency يعتمد عليه. |
| `webhook_type` | نص | أحد [أنواع الأحداث](#أنواع-الأحداث) أعلاه. |
| `timestamp` | عدد صحيح (ثوانٍ Unix) | الوقت الذي أرسلت فيه Didit الـ webhook. يستخدمه التحقق من التوقيع. يُحدَّث عند كل إعادة محاولة. |
| `created_at` | عدد صحيح (ثوانٍ Unix) | الوقت الذي حُدِّث فيه السجل الأساسي آخر مرة. |
| `application_id` | `uuid` | التطبيق الذي يملك الوجهة. |
| `environment` | `"live"` \| `"sandbox"` | يحدد ما إذا كان الحدث صادرًا عن جلسة أو تطبيق في بيئة الإنتاج أو في البيئة التجريبية. |
| `status` | نص | يظهر في كل حدث جلسة وكيان ومعاملة (بما في ذلك `*.data.updated`). أحداث `investigator.*` لا تحمل `status` (انظر حمولة Investigator أدناه). تستخدم أحداث الجلسات [تسميات حالة التحقق](https://docs.didit.me/ar/integration/verification-statuses.md) بصيغتها الدقيقة (مثل `"Approved"` و`"Kyc Expired"`)، وتستخدم أحداث الكيانات `ACTIVE` / `FLAGGED` / `BLOCKED`، وتستخدم أحداث المعاملات `APPROVED` / `IN_REVIEW` / `DECLINED` / `AWAITING_USER`. |بالنسبة إلى أحداث **الجلسة** (`status.updated` و`data.updated` للجلسات العادية وجلسات الشركات)، يتضمن الهيكل العام الحقول التالية أيضًا:| الحقل | النوع | ملاحظات |
| - | - | - |
| `session_id` | `uuid` | معرّف UUID لجلسة التحقق من المستخدم أو جلسة الشركة. |
| `business_session_id` | `uuid` | يظهر فقط في KYB، ويساوي `session_id`. |
| `session_kind` | `"business"` | يظهر فقط في أحداث KYB، ويتيح للمعالج العام التفريع حسب النوع. |
| `workflow_id` | `uuid` | سير العمل المستخدم لإنشاء الجلسة، عند تحديده. |
| `workflow_version` | عدد صحيح | إصدار سير العمل. |
| `vendor_data` | نص | معرّفك الداخلي، عند تقديمه عند إنشاء الجلسة. |
| `vendor_business_id` | نص | يظهر فقط في جلسات KYB، عند تقديمه. |
| `sandbox_scenario` | نص \| null | يظهر في كل حدث جلسة (KYC وKYB). هو المعرّف النصي لسيناريو [البيئة التجريبية](https://docs.didit.me/ar/integration/sandbox-testing.md) المفعّل، مثل `"decline_aml_hit"`، في جلسة تجريبية يكون فيها سيناريو مفعّلًا. القيمة `null` في جلسة إنتاج، أو في جلسة تجريبية لم يُفعَّل فيها أي سيناريو. لا يظهر في webhook الخاصة بالكيانات أو المعاملات. |
| `metadata` | كائن | البيانات الوصفية التي أرفقتها عند إنشاء الجلسة، مع حذف مفاتيح إزالة التكرار الداخلية. |
| `trigger` | نص | اختياري. يوضح سبب إرسال الـ webhook عندما لا يكون تدفق التحقق نفسه هو سببه: `"manual_review"` (تعديلات المراجِع من الكونسول)، و`"manual_step_update"` (قام مراجِع بتغيير حالة ميزة)، و`"ongoing_monitoring"` (تحديث مراقبة AML المستمرة)، و`"webhook_reconciliation"` (إعادة إرسال `status.updated` لأن الـ webhook النهائي الأول لم يُنشأ، لذلك قد تتلقى حدثًا ثانيًا لقرار حصلت عليه مسبقًا: أزل التكرار باستخدام `session_id` و`status`)، و`"manual_webhook_recovery"` (استرداد webhook يدويًا)، و`"kyb_registry_change"` (أبلغ سجل الشركات عن تغيير)، و`"console_registry_edit"` و`"console_key_people_skip"` و`"console_key_people_regenerate"` (تعديلات تُجرى في الكونسول على جلسة شركة)، و`"biometric_retention"` (`data.updated` يُرسَل عند حذف البيانات البيومترية الحية للجلسة وفق الجدول). قد تتسع هذه المجموعة، لذا عامل أي قيمة غير معروفة على أنها معلوماتية فقط. |
| `decision` | كائن | يظهر عندما تكون `status` هي `Approved` أو `Declined` أو `In Review` أو `Abandoned`. يعكس [قرار الجلسة في V3](https://docs.didit.me/ar/sessions-api/retrieve-session.md). |
| `resubmit_info` | `{ nodes_to_resubmit, reasons }` | يظهر عندما تكون `status` هي `Resubmitted` (ولا يظهر معه `decision`). |
| `decided_by` | نص | يظهر فقط في `status.updated` عندما تكون الحالة `Approved` أو `Declined` أو `Resubmitted` ويستخدم التطبيق Investigator. القيمة `"investigator"` عندما يتخذ Investigator القرار بمفرده، والقيمة `"human"` عندما يتخذه مستخدم في الكونسول، بما في ذلك قبول تقرير Investigator. لا يظهر الحقل عندما لا يسجل أي مراجِع القرار، أو عندما يتخذه مفتاح API، أو عندما لا يكون Investigator مستخدمًا. |### مثال على الهيكل العام للجلسة```json theme={null}
{
  "event_id": "9c0c8b8a-1111-4222-9333-444444444444",
  "webhook_type": "status.updated",
  "timestamp": 1774970000,
  "created_at": 1774969994,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "sandbox_scenario": null,
  "session_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "status": "Approved",
  "workflow_id": "66666666-7777-8888-9999-000000000000",
  "workflow_version": 4,
  "vendor_data": "user_42",
  "metadata": { "tier": "premium" },
  "decision": { "...": "see /sessions-api/retrieve-session" }
}
```> [!TIP]
>
> كائن `decision` هو المخطط نفسه الذي تعيده [`GET /v3/session/{id}/decision/`](https://docs.didit.me/ar/sessions-api/retrieve-session). للاطلاع على المرجع القياسي لحقول كل ميزة (كل خاصية في `id_verifications[]` و`aml_screenings[]` وغيرها)، راجع [نماذج البيانات](https://docs.didit.me/ar/reference/data-models.md). لم نكرر جداول الحقول لكل ميزة هنا عن قصد، حتى يبقى المخطط في مكان واحد فقط.### مصفوفات V3 المتعددة (مهم)داخل `decision`، تكون كل نتيجة لكل ميزة مصفوفة تحتوي على عدة عناصر، بحيث يمكن لسير عمل واحد أن يتضمن عدة نسخ من الميزة نفسها (على سبيل المثال، عدة فحوصات للهوية عبر مستندات متعددة):```text theme={null}
id_verifications[]        nfc_verifications[]
liveness_checks[]         face_matches[]
phone_verifications[]     email_verifications[]
poa_verifications[]       aml_screenings[]
ip_analyses[]             database_validations[]
questionnaire_responses   reviews[]
bank_verifications[]
```يحمل كل عنصر `node_id` يطابق رسم سير العمل، إضافةً إلى `status` خاص بالميزة. **لا تقرأ** الحقول المفردة القديمة في V2 (`id_verification` و`nfc` و`liveness` و`face_match` و`phone` و`email` و`poa` و`aml` و`ip_analysis` و`database_validation`)؛ فهي تظهر فقط إذا كانت الوجهة مثبتة صراحةً على `webhook_version: "v2"`.تُخزَّن نتائج **التوقيع الإلكتروني** في `signatures[]`، ولا تظهر إلا عندما يتضمن سير العمل خطوة **التوقيع الإلكتروني**: عنصر واحد لكل موقّع، يتضمن الموقّع، وتأكيد الرمز، والشهادة، والطابع الزمني، وتجزئات كل مستند ومسارات تنزيله، وملف PDF الخاص بالأدلة. لا يوجد حدث توقيع منفصل: يخبرك `status.updated` بوقت قراءتها. راجع [تقرير التوقيع الإلكتروني](https://docs.didit.me/ar/core-technology/electronic-signature/report-electronic-signature.md).تُخزَّن نتائج **التحقق المصرفي** في `bank_verifications[]`، عنصر واحد لكل خطوة من خطوات **التحقق المصرفي**. يوضح `method` في كل عنصر الطريقة التي تم بها التحقق من الحساب: `open_banking` (تسجيل الدخول إلى البنك)، و`account_lookup` (فحص الحساب)، أو بديل `statement` أو `microdeposits`، ويكون `null` حتى يتم تقييم حساب. عندما يختار الشخص **المتابعة من دون استخدام بنكك**، تكون قيمة `continued_without_bank` هي `true`، ويوضح `continuation` السبب. تكون قيمة `method` عندئذٍ `none`، إلا إذا جرت محاولة تقييم سابقة: راجع [متى لا يتمكن الشخص من التحقق من حسابه المصرفي](https://docs.didit.me/ar/core-technology/bank-verification/fallbacks.md#قراءة-ذلك-في-النتيجة). لا تتم قراءة أي بيانات مالية عند التحقق من الحساب عبر فحص الحساب، لذلك يسرد `financial_data.not_collected` الأرصدة أو المعاملات أو الدخل التي طلبتها الخطوة. راجع [طرق التحقق المصرفي](https://docs.didit.me/ar/core-technology/bank-verification/verification-methods.md#قراءة-الطريقة-في-النتيجة).### قسم Investigatorيحمل `decision` من الإصدار V3 لجلسة مستخدم أو شركة أيضاً الحقل `investigator`: وهو آخر مراجعة [Investigator](https://docs.didit.me/ar/console/investigator.md) للجلسة، أو `null` إذا لم تكن لها أي مراجعة قط.| الحقل | النوع | المعنى |
| - | - | - |
| `run_id` | uuid | المراجعة. اقرأ التقرير الكامل عبر `GET /v3/investigator/runs/{run_id}/`. |
| `status` | string | `running` أو `completed` أو `failed`. |
| `stage` | string | `shadow` أو `live` عند إجراء المراجعة. |
| `queue` | string | `kyc` أو `kyb` أو `aml` أو `transactions` أو `cases`. |
| `is_open` | boolean | المراجعة ما زالت بانتظار قرار. |
| `recommendation` | string \| null | `approve` أو `decline` أو `escalate` أو `request`. |
| `recommendation_detail` | string \| null | سطر واحد يصف الإجراء الموصى به. |
| `confidence` | string \| null | `high` أو `moderate` أو `low`. |
| `summary` | string \| null | ملخص التقرير. |
| `ready_to_accept` | boolean | يمكن قبول التقرير كما هو. |
| `prepared_at` | datetime \| null | وقت جاهزية التقرير. |
| `automated` | boolean | اتخذ Investigator القرار بنفسه، حتى لو تجاوزه أحد لاحقاً. |
| `decided_by` | string \| null | `investigator` ما دام قراره الخاص قائماً، و`member` في غير ذلك، و`null` ما دام القرار لم يُتخذ بعد. |
| `decision` | object \| null | كيف خرج العنصر من قائمة الانتظار: `outcome` و`action` و`final_status` و`agreed` و`decided_at` و`billed` ومن اتخذ القرار. |ميزات جمع المستندات، وهي **مستندات KYB** و**Document AI**، ليس لها مصفوفة مخصصة بصيغة الجمع، بل تظهر في قائمة `features[]` ضمن القرار على شكل `{ "feature": "DOCUMENT_AI", "node_id": "…" }` (عنصر واحد لكل عقدة). راجع [Document AI](https://docs.didit.me/ar/core-technology/document-ai/overview.md).تحمل قرارات التحقق من المستخدمين أيضاً كائن `biometric_consent` واحداً يحدد ما إذا كانت الجلسة تتطلب موافقة بيومترية صريحة، و`trigger`، وحالة الإجابة `status`، وأدلتها. تعني الحالة `status: "pending"` مع `reason: "BIOMETRIC_CONSENT_UNSUPPORTED_SDK"` أن حزمة SDK أصلية أقل من 4.10.0 التقطت وجهاً دون خطوة الموافقة. راجع [الموافقة البيومترية](https://docs.didit.me/ar/getting-started/biometric-consent.md).### مفاتيح طريقة التحقق من الهويةيحدد كل عنصر في `id_verifications[]` [طريقة التحقق من الهوية](https://docs.didit.me/ar/core-technology/id-verification/verification-methods.md) التي أنتجته، حتى يتمكن المعالج من التفرع بحسب الدليل الذي حصل عليه فعلاً:| المفتاح | القيمة |
| - | - |
| `verification_method` | `document` أو `id_lookup` أو `wallet` |
| `assurance` | `documentary` أو `data_match` أو `cryptographic` |
| `wallet_provider` | معرّف المحفظة من الكتالوج عندما تنتجه محفظة، وإلا فالقيمة `null` |
| `id_lookup` | أدلة السجل عندما تكون الطريقة `id_lookup`، وإلا فالقيمة `null` |
| `wallet_verification` | أدلة الاعتماد عندما تكون الطريقة `wallet`، وإلا فالقيمة `null` |
| `fallback_from` | سبب عدم إنهاء خطوة الهوية بطريقة غير طريقة المستند: الطريقة والسبب، وهل عاد المستخدم إلى التقاط المستند أم رُفض |تستمر المعالجات الحالية في العمل دون أي تعديل: تضبط جلسة المستند القيمة `verification_method` على `document` وتترك باقي الحقول `null`. توجد أنواع الحقول تفصيلاً في [نماذج البيانات](https://docs.didit.me/ar/reference/data-models.md#التحقق-من-الهوية).#### جلسة المحفظة المقبولة (`status.updated`)```json theme={null}
{
  "event_id": "3a0c1b2c-4444-4555-8666-777777777777",
  "webhook_type": "status.updated",
  "timestamp": 1774970000,
  "created_at": 1774969994,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "session_id": "cccccccc-dddd-eeee-ffff-000000000000",
  "status": "Approved",
  "vendor_data": "user_77",
  "decision": {
    "session_id": "cccccccc-dddd-eeee-ffff-000000000000",
    "status": "Approved",
    "features": ["ID_VERIFICATION"],
    "id_verifications": [
      {
        "node_id": "id_verification_1",
        "status": "Approved",
        "issuing_state": "DNK",
        "first_name": "Freja",
        "last_name": "Nielsen",
        "full_name": "Freja Nielsen",
        "date_of_birth": "1988-03-02",
        "verification_method": "wallet",
        "assurance": "cryptographic",
        "wallet_provider": "mitid",
        "fallback_from": null,
        "id_lookup": null,
        "wallet_verification": {
          "provider": "mitid",
          "provider_name": "MitID",
          "issuing_authority": "Danish Agency for Digital Government",
          "issuing_country": "DNK",
          "credential_type": "person_identification",
          "level_of_assurance": "substantial",
          "verified_at": "2026-09-04T09:12:44Z",
          "signature_valid": true,
          "attributes": {
            "full_name": "Freja Nielsen",
            "date_of_birth": "1988-03-02",
            "cpr_alias": "b0f1c2d3-e4f5-4678-9abc-def012345678"
          },
          "portrait": null,
          "face_match_score": null
        },
        "warnings": []
      }
    ],
    "reviews": [],
    "created_at": "2026-09-04T09:12:20.443172Z"
  }
}
```### حمولات أحداث الكيانات```json theme={null}
{
  "event_id": "uuid",
  "webhook_type": "user.status.updated",
  "timestamp": 1774970000,
  "created_at": 1774969994,
  "application_id": "uuid",
  "environment": "live",
  "vendor_user_id": "uuid",
  "vendor_data": "user_42",
  "status": "BLOCKED",
  "previous_status": "ACTIVE"
}
```حالات الكيانات هي `ACTIVE` أو `FLAGGED` أو `BLOCKED`. يحتفظ الحدثان `user.data.updated` و`business.data.updated` بالحقل `status`، ويضيفان مصفوفة `changed_fields` بالإضافة إلى خريطة `changes` بالصيغة `{ field: { previous, current } }`. تستخدم أحداث `business.*` الحقل `vendor_business_id` بدلاً من `vendor_user_id`.### حمولات أحداث المعاملاتتعمل webhook المعاملات على البنية التحتية نفسها للتوزيع المتعدد وتستخدم الغلاف نفسه، لكنها تعرّف المعاملة بدلاً من الجلسة:```json theme={null}
{
  "event_id": "uuid",
  "webhook_type": "transaction.created",
  "timestamp": 1774970000,
  "created_at": 1774970000,
  "application_id": "uuid",
  "environment": "live",
  "transaction_id": "uuid",
  "txn_id": "finance0001",
  "status": "APPROVED",
  "score": 0,
  "severity": "UNKNOWN",
  "amount": "1200.00",
  "currency": "EUR",
  "direction": "OUTBOUND",
  "action_required": null
}
```القيمة `action_required` هي `null`، أو الإجراء المعلّق للمستخدم (`type` بقيمة `wallet_ownership` أو `verification_session`، مع `url` الخاص به) كما هو موضح في [الإجراءات المطلوبة من المستخدم](https://docs.didit.me/ar/transaction-monitoring/sdk-transaction-submission.md#الإجراءات-المطلوبة-من-المستخدم).تُعيَّن المعاملات الجديدة افتراضياً إلى `APPROVED`. إذا غيّرت قاعدة أو قائمة حظر أو سير عمل حالة المعاملة أثناء إنشائها، فإن الحمولة الأولية `transaction.created` تعكس الحالة النهائية. يحمل `transaction.status.updated` الشكل نفسه مع قيمة `status` المحدّثة. حالات المعاملة هي `APPROVED` و`IN_REVIEW` و`DECLINED` أو `AWAITING_USER`؛ و`severity` هي `UNKNOWN` / `LOW` / `MEDIUM` / `HIGH` / `CRITICAL` و`direction` هي `INBOUND` / `OUTBOUND`. لا يُرسل `transaction.created` و`transaction.status.updated` إلا للتطبيقات في **بيئة الإنتاج** فقط. أما تبادلات Travel Rule في البيئة التجريبية فتُرسل فعلاً `travel_rule.status.updated` مع `environment: sandbox`.### استدعاء نتائج بحث سجل KYBيُرسل مسار [`POST /v3/kyb/search/`](https://docs.didit.me/ar/standalone-apis/kyb-registry) غير المتزامن حمولة إضافية واحدة **ليست** حدث وجهة: عند تمرير `webhook_url` إلى طلب البحث، يرسل Didit طلب POST بالاستدعاء `kyb.registry_search.resolved` إلى ذلك الرابط بمجرد أن يُحلّ السجل. وتختلف عن webhook الوجهة في ثلاث نواحٍ:- هو **غير موقّع** ولا يحمل ترويسات `X-Signature*`؛ بدلاً من ذلك يضع Didit عليه العلامة `X-Didit-Unsigned-Callback: true`، ويُتحقق منه بمطابقة `request_id` مع المعرّف الذي أعاده طلب البحث.
- يُخزَّن اسم الحدث في `event_type` (وليس `webhook_type`)، ولا يوجد `application_id` ولا `environment`.
- `timestamp` و`created_at` أعداد صحيحة بصيغة epoch، مثل webhooks الأخرى، على خلاف `created_at` بصيغة ISO في نقطة نهاية البحث.```json theme={null}
{
  "event_id": "uuid",
  "event_type": "kyb.registry_search.resolved",
  "request_id": "uuid",
  "vendor_data": "business-123",
  "metadata": { "source": "onboarding" },
  "search_status": "resolved",
  "search_resolved": true,
  "kyb_registry": { "companies": ["..."], "pagination": { "...": "..." } },
  "timestamp": 1774970000,
  "created_at": 1774970000
}
```يتبع الاستدعاء [سياسة إعادة المحاولة](#سياسة-إعادة-المحاولة) نفسها التي تتبعها webhook الوجهة (تعيد عمليات إعادة المحاولة ختم `timestamp` وتبقى غير موقّعة). راجع [KYB Registry API](https://docs.didit.me/ar/standalone-apis/kyb-registry.md) للاطلاع على مخطط قائمة المرشحين الكامل.## التحقق من التوقيعيُوقَّع كل webhook باستخدام HMAC-SHA256 والمفتاح `secret_shared_key` الخاص بالوجهة. تُرسَل ثلاثة رؤوس توقيع حتى تختار الرأس الذي يناسب بنيتك التقنية:| الرأس | ما يُوقَّع | آمن مع الوسيط البرمجي | متى يُستخدم |
| - | - | - | - |
| `X-Signature-V2` | JSON مضغوط ومرتّب يحافظ على أحرف يونيكود (`ensure_ascii=False`) | نعم | **موصى به**، يعمل حتى لو أعاد إطارُ العمل ترميز الجسم. |
| `X-Signature` | البايتات الخام الدقيقة للطلب كما أرسلها Didit (`sort_keys`، `ensure_ascii=True`) | لا | فقط عندما تستطيع قراءة الجسم الخام قبل أن يعالجه أي محلّل. |
| `X-Signature-Simple` **(مهمَل)** | `"{timestamp}:{session_id}:{status}:{webhook_type}"` | نعم | خيار احتياطي أخير يصادق على الغلاف فقط، ولا يصادق على `decision`. اطّلع على إشعار الإهمال أدناه قبل استخدامه. |الرؤوس التي يرسلها Didit دائمًا:| الرأس | مثال | المعنى |
| - | - | - |
| `Content-Type` | `application/json` | الجسم بصيغة JSON. |
| `User-Agent` | `DiditWebhook/2.0 +https://didit.me` | يُعرّف عامل webhook الخاص بـ Didit. |
| `X-Timestamp` | `1774970000` | عدد الثواني منذ بداية Unix Epoch وقت إرسال الـ webhook. ارفض الطلب إذا كان `abs(now - X-Timestamp) > 300`. |
| `X-Signature` / `X-Signature-V2` / `X-Signature-Simple` | ملخصات hex | HMAC-SHA256 للجسم أو للسلسلة القياسية الموضحة أعلاه. |لمدة 24 ساعة بعد تدوير سر التوقيع للوجهة، تحمل عمليات التسليم أيضًا الرؤوس `X-Signature-Previous` و`X-Signature-V2-Previous` و`X-Signature-Simple-Previous` (انظر [تدوير سر التوقيع](#تدوير-سر-التوقيع)).### تدوير سر التوقيعتُدوِّر سر التوقيع لوجهة webhook من داخل Business Console.
لمدة 24 ساعة بعد التدوير، تحمل كل عملية تسليم إلى تلك الوجهة ثلاثة رؤوس إضافية، وهي موقّعة بالسر الذي تم استبداله:| الرأس | المطابق له في التوقيع | موقّع بـ |
| - | - | - |
| `X-Signature-V2-Previous` | `X-Signature-V2` | السر المستبدل |
| `X-Signature-Previous` | `X-Signature` | السر المستبدل |
| `X-Signature-Simple-Previous` | `X-Signature-Simple` (مهمَل) | السر المستبدل |المعالج الذي يقبل التطابق مع رأس التوقيع العادي أو مع رأس `-Previous` الخاص به يستمر في التحقق أثناء نشر السر الجديد، إذ يطابق السر القديم رأس `-Previous`، ويطابق السر الجديد الرأس العادي.
تطبّق نماذج الكود أدناه هذا السلوك.
انشر السر الجديد خلال 24 ساعة، لأنه بعد ذلك لا يتحقق إلا السر الجديد.
يخبرك الحقل `previous_secret_expires_at` في [`GET /v3/webhook/destinations/{destination_uuid}/`](https://docs.didit.me/ar/management-api/webhook/get-destination) بموعد انتهاء الساعات الأربع والعشرين.تُوقَّع عمليات إعادة المحاولة وإعادة الإرسال من جديد بالسر الحالي للوجهة وقت إرسالها، وتحمل رؤوس `-Previous` فقط خلال الساعات الأربع والعشرين.
إذا أُجري تدوير آخر خلال الساعات الأربع والعشرين، يُلغى أقدم سر فورًا، ولا يبقى صالحًا للتحقق عبر رؤوس `-Previous` إلا السر الذي كان حاليًا حتى التدوير الجديد، ولمدة 24 ساعة من التدوير الجديد.رؤوس `-Previous` موجودة فقط لوجهات webhook.
أما `secret_shared_key` على مستوى التطبيق، الذي يوقّع `webhook_url` القديم، فليس له تداخل: إذ يؤدي تدويره عبر `rotate_secret_key` في [`PATCH /v3/webhook/`](https://docs.didit.me/ar/sessions-api/management-api#إعدادات-webhook) إلى إبطال السر القديم فورًا.### `X-Signature-Simple` مهمَل> [!WARNING]
>
> **`X-Signature-Simple` يصادق على الغلاف، وليس على payload.** تغطي سلسلته القياسية الحقول `timestamp` و`session_id` و`status` و`webhook_type` فقط. وهي **لا** تغطي `decision`، ولا `resubmit_info` أو `metadata` أو `vendor_data` أو `event_id`. يمكن لطلب أُعيدت كتابة `decision` فيه مع ترك هذه الحقول الأربعة دون تغيير أن يجتاز التحقق عبر `X-Signature-Simple`.
>
> وهو أضعف في الأحداث التي لا تحمل أي `session_id` على الإطلاق. فأحداث المعاملات (`transaction_id` و`txn_id` و`amount` و`score`) وأحداث الكيانات (`vendor_user_id` و`vendor_business_id`) توقّع `"{timestamp}::{status}:{webhook_type}"`، وهذه السلسلة لا تحدد **أي** معاملة أو كيان يتعلق به الحدث. فمعاملتان مختلفتان أُبلغ عنهما بالحالة نفسها في الثانية نفسها تحملان `X-Signature-Simple` نفسه.
> كذلك لا تحمل أحداث `travel_rule.status.updated` و`workflow.compliance_changed` و`transaction_rule_suggestions.created` أي `status`، لذلك توقّع `"{timestamp}:::{webhook_type}"`، مثل `"{timestamp}:::travel_rule.status.updated"`.
>
> إذا كان `X-Signature-Simple` هو الرأس الوحيد الذي تتحقق منه، فاعتبر كل ما عدا تلك الحقول الأربعة **غير موثوق**، وأعد جلب السجل من الواجهة البرمجية: [`GET /v3/session/{session_id}/decision/`](https://docs.didit.me/ar/sessions-api/retrieve-session) للجلسة، و[`GET /v3/transactions/{transaction_id}/`](https://docs.didit.me/ar/management-api/transactions/get) للمعاملة، لأنها المصدر الرسمي للسجل.هذا قيد للتوافق، ولا يمكن إصلاحه في مكانه. فالسلسلة القياسية المكونة من أربعة حقول عقد على مستوى البروتوكول: كل مستقبِل يتحقق من `X-Signature-Simple` يعيد حساب هذه الحقول الأربعة بالضبط، ولذلك فإن تغيير ما يوقّعه الرأس سيكسر كل هؤلاء المستقبِلين بصمت في مسار التحقق من التوقيع. كما أن تغطية الجسم تتطلب شكلًا قياسيًا محددًا لـ JSON يعيد الطرفان إنتاجه بايتًا بايتًا، وقد وفّرت Didit هذا الشكل بالفعل عبر `X-Signature-V2`. بدلًا من إضافة متغير ثانٍ يغطي الجسم مع قواعد تطبيع خاصة به، يبقى الرأس كما هو ويُهمَل لصالح المتغيرات الموجودة التي تغطي payload كاملة.وما يعنيه ذلك لك:- **لا يتغير شيء على الشبكة.** يستمر إرسال `X-Signature-Simple` مع كل عملية تسليم، وبالطريقة نفسها. تستمر أدوات التحقق القائمة في العمل، وسنعلن أي إزالة مسبقًا بوقت كافٍ.
- **يجب أن تستخدم التكاملات الجديدة `X-Signature-V2`.** فهو آمن مع الوسيط البرمجي **ويغطي** payload كاملة.
- **إذا لم تستطع الانتقال عن `X-Signature-Simple`**، فتحقق منه، ثم اجلب السجل من الواجهة البرمجية بدلًا من قراءته من جسم الـ webhook.### لماذا ثلاثة متغيراتتُعيد بعض أطر الويب (محللات الجسم في Express، والوسيط البرمجي في Django، وبوابات الواجهات البرمجية) ترميز JSON بصمت قبل أن يقرأه المعالج الخاص بك. فإذا احتوى الجسم الأصلي على `"José"` وأعاد الوسيط كتابته إلى `"José"`، تتغير البايتات مع أن البيانات واحدة. عندئذٍ يفشل `X-Signature`. أما `X-Signature-V2` فيُحسب من شكل JSON قياسي (ترتيب المفاتيح، وفواصل بلا مسافات، و**يونيكود غير مُهرَّب**) يعيد معظم الوسطاء إنتاجه بشكل حتمي. وعندما يفشل ذلك أيضًا، يعود `X-Signature-Simple` المهمَل إلى توقيع سلسلة قصيرة لا تعتمد على المحلل، ولا يوقّع الجسم أبدًا، لذلك لا يستطيع أن يضمن سلامة الجسم.### ترتيب التحقق الموصى به1) جرّب **`X-Signature-V2`** أولاً. أعد ترميز `JSON.parse(body)` مع فرز المفاتيح والحفاظ على Unicode، ثم احسب `HMAC-SHA256(secret, canonical)` وقارن الناتج بالرأس باستخدام `timingSafeEqual`.
2) إذا فشل V2 ويمكنك قراءة البايتات الخام، جرّب **`X-Signature`** مقابل `HMAC-SHA256(secret, rawBody)`.
3) فقط إذا فشلت الطريقتان، ارجع إلى **`X-Signature-Simple`** المتقادمة مقابل `HMAC-SHA256(secret, "{timestamp}:{session_id}:{status}:{webhook_type}")`. هذه الطريقة تصادق الغلاف وحده: عامل `decision` في النص على أنه غير موثوق، وأعد جلبه من [`GET /v3/session/{session_id}/decision/`](https://docs.didit.me/ar/sessions-api/retrieve-session) قبل أن تتصرف بناءً عليه.عند كل خطوة، إذا وُجد الرأس `-Previous` المقابل، فاقبل التطابق مع أيٍّ من الرأسين (راجع [تدوير السر المشترك للتوقيع](#تدوير-سر-التوقيع)).في جميع الحالات، **ارفض** أي طلب أقدم من **5 دقائق** (`abs(now - X-Timestamp) > 300`) للحماية من إعادة التشغيل.## سياسة إعادة المحاولةإذا ردّ endpoint الخاص بك بالرمز `5xx` أو `404`، يعيد Didit المحاولة حتى **مرتين** مع تأخير متزايد:- **إعادة المحاولة الأولى**: بعد نحو **دقيقة واحدة** من الفشل الأول.
- **إعادة المحاولة الثانية**: بعد نحو **4 دقائق** من إعادة المحاولة الأولى.بعد إعادة المحاولة الثانية يُسقط التسليم. تُسجَّل كل محاولة تسليم (الأولى وإعادات المحاولة) كإدخال منفصل في Business Console ضمن تبويب **التسليمات** الخاص بالوجهة، حتى تتمكن من إعادة تشغيل أي محاولة أو فحصها.تحمل كل محاولة في سجل تسليم الوجهة رقم `attempt`: القيمة `1` لأول تسليم لحدث، وواحد زائد على رقم المحاولة التي تكرّرها في كل إعادة محاولة أو إعادة إرسال.
تُرسل إعادات المحاولة التلقائية إلى العنوان الذي أُرسلت إليه المحاولة الأولى، حتى لو غيّرت `url` الوجهة بعد ذلك.
تُرسل إعادة الإرسال اليدوية إلى عنوان URL الحالي للوجهة، لذلك فإن تصحيح العنوان يصحح إعادة الإرسال اليدوية أيضاً.قواعد أخرى للتسليم يجب معرفتها:- تنتهي مهلة طلب HTTP الصادر بعد **5 ثوانٍ**.
- يمنع Didit التسليم إلى العناوين الخاصة / localhost كحماية من SSRF، ولا يتبع إعادات التوجيه أبداً. استخدم endpoint عاماً عبر HTTPS يجيب مباشرة.
- يُرفض عنوان `url` للوجهة الذي يحتوي على شرطة مائلة عكسية خام أو محرف تحكم أو أكثر من `@` واحدة في جزء authority من عنوان URL، باعتباره غير صالح. لا يمكن إنشاء مثل هذا العنوان، ولا يمكن إعادة تسليم عنوان موجود أو إعادة تشغيله. استخدم عنوان `https://host/path` بسيطاً مع قسم معلومات مستخدم واحد على الأكثر (لعناوين webhook بأسلوب basic-auth).
- تُعامَل الاستجابة `2xx` على أنها نجاح. تُعدّ المهلات وفشل الاتصال قابلة لإعادة المحاولة (وتظهر بالرمزين `504` و`503` في سجل التسليم)، أما الاستجابات `3xx` و`4xx` باستثناء `404` فلا تُعاد محاولتها.
- عند إعادة المحاولة يعيد Didit حساب التواقيع باستخدام `X-Timestamp` جديد، فيبقى زوج الطابع الزمني والتوقيع متوافقاً دائماً.
- تُوقَّع إعادات المحاولة وإعادات الإرسال بسر التوقيع الحالي للوجهة وقت إرسالها.### ترتيب التسليملا يضمن Didit ترتيب وصول webhook إليك. يُضاف كل تسليم إلى قائمة الانتظار بشكل مستقل، وتصل إعادة المحاولة بعد دقائق من المحاولة الأولى، لذلك قد يصلك `status.updated` بالحالة `Approved` قبل حدث سابق بالحالة `In Progress`.ابنِ المستقبِل لديك وفق ذلك:- عامل webhook على أنه إشارة بأن شيئاً قد تغيّر. عندما يهم الترتيب، استدعِ [`GET /v3/session/{session_id}/decision/`](https://docs.didit.me/ar/sessions-api/retrieve-session) وتصرّف بناءً على `status` الحالي.
- لا تدع حدثاً أقدم يكتب فوق حالة أحدث: قارن `created_at` للحدث مع آخر حدث خزّنته لذلك السجل، وتجاهل الحدث الأقدم.
- اعتمد `event_id` مفتاحًا للتكرارات، فهو يبقى كما هو عبر إعادات المحاولة.يرتب Didit أحداث تحويل Travel Rule بترتيب محدد، موصوف ضمن [تغيّر حالة تبادل Travel Rule](#تغيّرت-حالة-تبادل-travel-rule-travel_rule-status-updated)، لكن إعادة المحاولة قد تغيّر ترتيب وصولها.## إعداد وجهةيمكنك إدارة الوجهات من **Business Console > API & Webhooks** أو عبر Management API.**1. إنشاء الوجهة**استدعِ [`POST /v3/webhook/destinations/`](https://docs.didit.me/ar/management-api/webhook/create-destination) (أو انقر **إضافة وجهة** في الكونسول) مع `label` و`url` عام و`webhook_version` (`"v3"`) ومصفوفة `subscribed_events` تحتوي على كل عائلة أحداث يجب أن يستقبلها هذا endpoint. مطلوب حدث واحد على الأقل، ولا يوجد أي حرف بدل.**2. خزّن secret\_shared\_key**تُعيد استجابة الإنشاء `secret_shared_key` الخاص بالوجهة. خزّنه مع أسرار خادمك الأخرى: فهو خاص بهذه الوجهة وحدها، وهو المدخل لجميع صيغ توقيع HMAC-SHA256 الثلاثة. تعيده [`GET /v3/webhook/destinations/{destination_uuid}/`](https://docs.didit.me/ar/management-api/webhook/get-destination) مرة أخرى لمستدعي API الذين يستخدمون مفتاح API ولمستخدمي الكونسول الذين يمكنهم قراءة webhook.**3. نفّذ endpoint**أتِح مسار `POST` عاماً عبر HTTPS، واقرأ الجسم الخام، وتحقق من التوقيع (مع تفضيل `X-Signature-V2`)، وتحقق من نافذة الطابع الزمني، ثم وجّه الحدث حسب `webhook_type`. أعد `2xx` بمجرد إضافة العمل إلى قائمة الانتظار، ونفّذ المعالجة الثقيلة بشكل غير متزامن.**4. أكّد الاستلام**راقب تبويب **التسليمات** الخاص بالوجهة. تُعيد الـ webhooks السليمة الاستجابة `200` خلال ثانية أو ثانيتين. يؤدي `5xx`/`404` إلى تفعيل سياسة إعادة المحاولة لدى Didit، وتُسقط الإخفاقات المستمرة بعد إعادة المحاولة الثانية.**5. كرّر الاختبار باستخدام Try Webhook**استخدم سيناريوهات [**Try Webhook**](#الاختبار) في الكونسول لتوجيه endpoint الخاص بك عبر payload مقبولة (approved)، ومرفوضة (declined)، وقيد المراجعة (in-review)، وKYB، والكيان، والنشاط، والمعاملة، قبل تفعيل حركة الإنتاج.![ميزة اختبار webhook من Didit في Business Console](https://mintcdn.com/didit-0f962782/z6T2GHM4Zh-iSj-K/images/test-webhook.png?fit=max\&auto=format\&n=z6T2GHM4Zh-iSj-K\&q=85\&s=2787ef3ed7a8016798f951e5ae898e4e)أرسل webhook اختباريًا مباشرة من Business Console للتحقق من endpoint الخاص بك قبل الإطلاق.يمكنك إنشاء أي عدد من الوجهات تحتاجه. نمط شائع هو **وجهة واحدة لكل مستهلك**: على سبيل المثال، أحداث KYC إلى خدمة المصادقة لديك، وأحداث KYB إلى خدمة عمليات الامتثال، والمعاملات إلى طابور الاحتيال. لكل وجهة سرها وإصدارها وقائمة اشتراكاتها الخاصة، لذلك فإن تدوير السر أو نشراً معيباً على مستهلك واحد لا يؤثر على الآخرين.## نماذج الكودلضمان أمان نقطة نهاية الـ webhook، تحقّق من أصالة الطلبات الواردة باستخدام `secret_shared_key` الخاص بالوجهة.> [!WARNING]
>
> **استخدم `X-Signature-V2`.** هو المتغيّر الأكثر مقاومةً لإعادة الترميز الذي تجريه البرمجيات الوسيطة، وهو يصادق على نص الطلب بالكامل (بما في ذلك `decision`).### Node.js / Express (المُوصى به: X-Signature-V2)```javascript theme={null}
const express = require("express");
const crypto = require("crypto");

const app = express();
const PORT = process.env.PORT || 1337;

// Per-destination secret from POST /v3/webhook/destinations/
const WEBHOOK_SECRET_KEY = process.env.WEBHOOK_SECRET_KEY || "YOUR_WEBHOOK_SECRET_KEY";

app.use(express.json());

/**
 * Match Didit's float normalisation: whole-valued floats are serialised as ints.
 */
function shortenFloats(data) {
  if (Array.isArray(data)) return data.map(shortenFloats);
  if (data !== null && typeof data === "object") {
    return Object.fromEntries(
      Object.entries(data).map(([key, value]) => [key, shortenFloats(value)])
    );
  }
  if (typeof data === "number" && !Number.isInteger(data) && data % 1 === 0) {
    return Math.trunc(data);
  }
  return data;
}

/** Sort object keys recursively before re-stringifying. */
function sortKeys(obj) {
  if (Array.isArray(obj)) return obj.map(sortKeys);
  if (obj !== null && typeof obj === "object") {
    return Object.keys(obj).sort().reduce((acc, key) => {
      acc[key] = sortKeys(obj[key]);
      return acc;
    }, {});
  }
  return obj;
}

function verifySignatureV2(jsonBody, signatureHeader, timestampHeader, secret) {
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestampHeader, 10)) > 300) return false;

  // Reproduce Didit's canonical JSON: sorted keys, compact, Unicode preserved.
  const canonical = JSON.stringify(sortKeys(shortenFloats(jsonBody)));
  const expected = crypto.createHmac("sha256", secret).update(canonical, "utf8").digest("hex");

  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signatureHeader, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function verifySignatureSimple(jsonBody, signatureHeader, timestampHeader, secret) {
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestampHeader, 10)) > 300) return false;

  const canonical = [
    jsonBody.timestamp ?? "",
    jsonBody.session_id ?? "",
    jsonBody.status ?? "",
    jsonBody.webhook_type ?? "",
  ].join(":");
  const expected = crypto.createHmac("sha256", secret).update(canonical).digest("hex");

  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signatureHeader, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

/**
 * The signature header and, for 24 hours after the destination's secret is
 * rotated, its "-Previous" header (the same signature made with the replaced secret).
 */
function signaturesFor(req, header) {
  return [req.get(header), req.get(`${header}-Previous`)].filter(Boolean);
}

app.post("/webhook", (req, res) => {
  const timestamp = req.get("X-Timestamp");
  const body = req.body;

  if (!timestamp || !WEBHOOK_SECRET_KEY) {
    return res.status(401).json({ message: "Missing required headers" });
  }

  // A match on either header is enough.
  const matches = (header, verify) =>
    signaturesFor(req, header).some((sig) => verify(body, sig, timestamp, WEBHOOK_SECRET_KEY));

  let verified = false;
  if (matches("X-Signature-V2", verifySignatureV2)) {
    verified = true;
  } else if (matches("X-Signature-Simple", verifySignatureSimple)) {
    // Deprecated fallback: Simple authenticates the envelope only. Do not trust
    // body.decision here — re-fetch it from GET /v3/session/{session_id}/decision/.
    verified = true;
  }
  if (!verified) return res.status(401).json({ message: "Invalid signature" });

  // Dispatch by event type. Return 2xx fast; do heavy work asynchronously.
  switch (body.webhook_type) {
    case "status.updated":
    case "data.updated":
      // session or business session
      break;
    case "user.status.updated":
    case "user.data.updated":
    case "business.status.updated":
    case "business.data.updated":
      // consolidated entity event
      break;
    case "activity.created":
      // audit timeline event
      break;
    case "transaction.created":
    case "transaction.status.updated":
      // transaction monitoring event
      break;
    case "travel_rule.status.updated":
      // travel rule exchange status event
      break;
  }
  return res.status(200).json({ ok: true });
});

app.listen(PORT, () => console.log(`Server on http://localhost:${PORT}`));
```إذا كان بإمكانك قراءة البايتات الخام قبل أن تصل إليها أي أداة تحليل، فإن متغيّر `X-Signature` الأصلي يعمل بالطريقة نفسها، لكنه يوقّع النص الخام. احتفظنا به في القسم القديم أدناه للتوثيق الكامل.### Python / FastAPI (المُوصى به: X-Signature-V2)```python theme={null}
from fastapi import FastAPI, Request, HTTPException
from time import time
import json
import hmac
import hashlib
import os

app = FastAPI()


def shorten_floats(data):
    """Match Didit: whole-valued floats serialise as ints."""
    if isinstance(data, dict):
        return {k: shorten_floats(v) for k, v in data.items()}
    if isinstance(data, list):
        return [shorten_floats(x) for x in data]
    if isinstance(data, float) and data.is_integer():
        return int(data)
    return data


def verify_signature_v2(body_json, signature_header, timestamp_header, secret):
    if abs(int(time()) - int(timestamp_header)) > 300:
        return False
    canonical = json.dumps(
        shorten_floats(body_json),
        sort_keys=True,
        separators=(",", ":"),
        ensure_ascii=False,   # unescaped Unicode -- matches Didit's V2 canonical form
    )
    expected = hmac.new(secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header, expected)


def verify_signature_simple(body_json, signature_header, timestamp_header, secret):
    if abs(int(time()) - int(timestamp_header)) > 300:
        return False
    canonical = ":".join([
        str(body_json.get("timestamp", "")),
        str(body_json.get("session_id", "")),
        str(body_json.get("status", "")),
        str(body_json.get("webhook_type", "")),
    ])
    expected = hmac.new(secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header, expected)


def signatures(request, header):
    """The signature header and, for 24 hours after the destination's secret is
    rotated, its -Previous header (the same signature made with the replaced secret)."""
    return [sig for sig in (request.headers.get(header), request.headers.get(f"{header}-previous")) if sig]


@app.post("/webhook")
async def handle_webhook(request: Request):
    body = json.loads((await request.body()).decode("utf-8"))
    secret = os.environ["WEBHOOK_SECRET_KEY"]

    ts = request.headers.get("x-timestamp")
    if not ts:
        raise HTTPException(status_code=401, detail="Missing X-Timestamp")

    # A match on either header is enough.
    if any(verify_signature_v2(body, sig, ts, secret) for sig in signatures(request, "x-signature-v2")):
        pass
    elif any(verify_signature_simple(body, sig, ts, secret) for sig in signatures(request, "x-signature-simple")):
        # Deprecated fallback: Simple authenticates the envelope only. Do not trust
        # body["decision"] here - re-fetch it from GET /v3/session/{session_id}/decision/.
        pass
    else:
        raise HTTPException(status_code=401, detail="Invalid signature")

    # Dispatch on body["webhook_type"]; return 2xx fast.
    return {"ok": True}
```### PHP / Laravel (المُوصى به: X-Signature-V2)تخالف الإعدادات الافتراضية لـ JSON في PHP الشكلَ المعياري. يجب تجاوز أربعة منها، وأي إغفال لواحد منها يغيّر البايتات الموقّعة، فلا تتطابق البصمة أبدًا:| الإعداد الافتراضي في PHP | ما يُخرجه | ما تحتاجه |
| - | - | - |
| `json_decode($raw, true)` (ترابطي) | الكائنات الفارغة `{}` تنهار إلى `[]` | فكّ الترميز بـ `$associative = false` بحيث تبقى كائنات JSON من نوع `stdClass` |
| `json_encode` يهرب الشرطات المائلة الأمامية | `https:\/\/media.didit.me` | `JSON_UNESCAPED_SLASHES` |
| `json_encode` يهرب الأحرف غير ASCII | `Jos\u00e9` بدلًا من `José` | `JSON_UNESCAPED_UNICODE` |
| `ksort` يقارن المفاتيح الشبيهة بالأرقام كأرقام | `"9"` يأتي قبل `"10"` | `ksort($props, SORT_STRING)` |فخ الكائن الفارغ هو الأشدّ إيلامًا. تحمل payloads الخاصة بـ Didit عادةً خرائط فارغة مثل `risk_view.countries.risk_scores` و`risk_view.crimes.risk_scores` و`risk_view.custom_list`، لذلك يكسر التحويل الترابطي ذهابًا وإيابًا التوقيعَ في الجلسات المقبولة العادية، لا في حالة نادرة استثنائية.```php theme={null}
<?php

use Illuminate\Http\Request;

class WebhookController extends Controller
{
    /**
     * Rebuild Didit's canonical JSON form: recursively sorted keys, JSON
     * objects preserved as objects, arrays left in order.
     */
    private function canonicalize($value)
    {
        if ($value instanceof \stdClass) {
            $props = get_object_vars($value);
            ksort($props, SORT_STRING); // code-point order, like Python's sort_keys
            $sorted = new \stdClass();
            foreach ($props as $key => $item) {
                $sorted->{$key} = $this->canonicalize($item);
            }
            return $sorted;
        }
        if (is_array($value)) { // JSON arrays keep their order
            return array_map(function ($item) { return $this->canonicalize($item); }, $value);
        }
        return $value;
    }

    private function verifySignatureV2(string $rawBody, string $sig, string $ts, string $secret): bool
    {
        if (abs(time() - (int) $ts) > 300) return false;

        // Decode to objects, not associative arrays: associative decoding turns
        // empty JSON objects ({}) into empty arrays ([]), and json_encode then
        // writes them back as [] -- different bytes than Didit signed.
        $decoded = json_decode($rawBody, false);
        if (json_last_error() !== JSON_ERROR_NONE) return false;

        $canonical = json_encode(
            $this->canonicalize($decoded),
            JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
        );
        return hash_equals(hash_hmac('sha256', $canonical, $secret), $sig);
    }

    /** X-Signature: HMAC over the exact bytes received -- never a re-serialised copy. */
    private function verifySignatureRaw(string $rawBody, string $sig, string $ts, string $secret): bool
    {
        if (abs(time() - (int) $ts) > 300) return false;
        return hash_equals(hash_hmac('sha256', $rawBody, $secret), $sig);
    }

    private function verifySignatureSimple(?array $body, string $sig, string $ts, string $secret): bool
    {
        if (abs(time() - (int) $ts) > 300) return false;
        $canonical = implode(':', [
            $body['timestamp'] ?? '',
            $body['session_id'] ?? '',
            $body['status'] ?? '',
            $body['webhook_type'] ?? '',
        ]);
        return hash_equals(hash_hmac('sha256', $canonical, $secret), $sig);
    }

    public function handle(Request $request)
    {
        $secret = env('WEBHOOK_SECRET_KEY');
        $ts = $request->header('x-timestamp');
        if (!$ts || !$secret) {
            return response()->json(['message' => 'Missing required headers'], 401);
        }

        // getContent() hands you the exact bytes Didit transmitted, before any parser runs.
        $rawBody = $request->getContent();
        $body = json_decode($rawBody, true); // associative copy, for dispatch only

        // Check the signature header and, for 24 hours after the destination's secret is
        // rotated, its -Previous header (the same signature made with the replaced secret).
        // A match on either header is enough.
        $matches = function (string $header, callable $verify) use ($request): bool {
            foreach ([$header, $header . '-previous'] as $name) {
                $sig = $request->header($name);
                if ($sig && $verify($sig)) {
                    return true;
                }
            }
            return false;
        };

        $verified = $matches('x-signature-v2', fn ($sig) => $this->verifySignatureV2($rawBody, $sig, $ts, $secret))
            || $matches('x-signature', fn ($sig) => $this->verifySignatureRaw($rawBody, $sig, $ts, $secret))
            // Deprecated fallback: Simple authenticates the envelope only. Do not trust
            // the decision body here - re-fetch it from GET /v3/session/{session_id}/decision/.
            || $matches('x-signature-simple', fn ($sig) => $this->verifySignatureSimple($body, $sig, $ts, $secret));
        if (!$verified) {
            return response()->json(['message' => 'Invalid signature'], 401);
        }

        // Dispatch on $body['webhook_type']; return 2xx fast.
        return response()->json(['ok' => true]);
    }
}
```أمران آخران يستحقان الذكر:- **الأعداد العشرية ذات القيم الصحيحة لا تحتاج إلى معالجة خاصة في PHP.** تُسلسلها Didit أعدادًا صحيحة (`100.0` → `100`)، ويكتب `json_encode` في PHP العدد العشري ذي القيمة الصحيحة بالطريقة نفسها، لذا لا تحتاج إلى خطوة `shortenFloats`، على عكس نموذج Node.js أعلاه.
- **أبقِ `serialize_precision` على `-1`** (القيمة الافتراضية منذ PHP 7.1). إذا غيّر مضيفك القيمة إلى `17`، فإن `json_encode` يكتب `87.42` بوصفه `87.420000000000002`، وتفشل كل عملية تحقق من `X-Signature-V2`.إذا تعذّر عليك جعل الشكل المعياري متطابقًا، فإن `verifySignatureRaw` أعلاه هو البديل الأبسط: تُرجع `$request->getContent()` في Laravel البايتات دون أي تعديل، فيعمل `X-Signature` طالما لم يُعِد أي شيء قبل PHP ترميزَ النص. وهذه بالضبط الحالة التي صُمّم `X-Signature-V2` لتجاوزها، ولهذا يُجرَّب V2 أولًا.## الاختبارلا تحتاج إلى **تشغيل** تحقق كامل لاختبار نقطة النهاية الخاصة بك.- من **Business Console > API & Webhooks > Try Webhook**، اختر سيناريو (مثل `approved_full_features` و`declined_face_mismatch` و`in_review_aml_hit` و`business_session_approved` و`transaction_created` و`activity_created` و`user_status_updated`)، ثم أرسل webhook مكتملة البنية إلى عنوان URL الخاص بالوجهة.
- يُوقَّع كل سيناريو بالرؤوس الثلاثة نفسها للتوقيع وبالترميز المعياري نفسه المستخدم في حركة المرور في بيئة الإنتاج، بما في ذلك بيانات اختبار Unicode مثل `approved_kyc_with_unicode`، حتى تتأكد من أن `X-Signature-V2` يعمل عبر البرمجيات الوسيطة لديك. تحمل عمليات التسليم التجريبية بيانات نموذجية، وتُعلَّم برأس إضافي هو `X-Didit-Test-Webhook: true` ولاحقة `(Test)` في `User-Agent`، حتى تستطيع استبعادها من الحالة الإنتاجية.
- أعِد تشغيل أي تسليم سابق من تبويب **التسليمات** الخاص بالوجهة إذا احتجت إلى تصحيح تراجع.لاختبارات الدخان من البداية إلى النهاية، شغّل تحققًا حقيقيًا عبر سير عمل تجريبي في تطبيقك، فالـ webhook الناتجة تستخدم الغلاف نفسه والرؤوس نفسها وسياسة إعادة المحاولة نفسها التي تستخدمها سيناريوهات **Try Webhook**.## أمثلة### جلسة KYC مقبولة (`status.updated`)```json theme={null}
{
  "event_id": "9c0c8b8a-1111-4222-9333-444444444444",
  "webhook_type": "status.updated",
  "timestamp": 1774970000,
  "created_at": 1774969994,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "sandbox_scenario": null,
  "session_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "status": "Approved",
  "workflow_id": "66666666-7777-8888-9999-000000000000",
  "workflow_version": 4,
  "vendor_data": "user_42",
  "metadata": { "tier": "premium" },
  "decision": {
    "session_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "session_number": 43762,
    "status": "Approved",
    "workflow_id": "66666666-7777-8888-9999-000000000000",
    "features": ["ID_VERIFICATION", "LIVENESS", "FACE_MATCH", "AML"],
    "vendor_data": "user_42",
    "id_verifications": [
      {
        "node_id": "id_verification_1",
        "status": "Approved",
        "document_type": "Identity Card",
        "document_number": "SAMPLE-DOC-12345",
        "first_name": "Jane",
        "last_name": "Doe",
        "date_of_birth": "1990-01-01",
        "issuing_state": "ESP",
        "verification_method": "document",
        "assurance": "documentary",
        "wallet_provider": null,
        "fallback_from": null,
        "id_lookup": null,
        "wallet_verification": null,
        "warnings": []
      }
    ],
    "liveness_checks": [
      { "node_id": "liveness_1", "status": "Approved", "method": "ACTIVE_3D", "score": 95.4, "warnings": [] }
    ],
    "face_matches": [
      { "node_id": "face_match_1", "status": "Approved", "score": 96.1, "warnings": [] }
    ],
    "aml_screenings": [
      { "node_id": "aml_1", "status": "Approved", "total_hits": 0, "entity_type": "person", "hits": [], "warnings": [] }
    ],
    "reviews": [],
    "created_at": "2026-05-17T08:54:25.443172Z"
  }
}
```### جلسة مرفوضة مع تحذيرات (`status.updated`)```json theme={null}
{
  "event_id": "9c0c8b8a-1111-4222-9333-555555555555",
  "webhook_type": "status.updated",
  "timestamp": 1774970000,
  "created_at": 1774969994,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "sandbox_scenario": null,
  "session_id": "bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
  "status": "Declined",
  "vendor_data": "user_99",
  "decision": {
    "session_id": "bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
    "status": "Declined",
    "features": ["ID_VERIFICATION", "LIVENESS", "FACE_MATCH"],
    "id_verifications": [
      {
        "node_id": "id_verification_1",
        "status": "Declined",
        "document_type": "Passport",
        "expiration_date": "2024-01-01",
        "warnings": [
          {
            "feature": "ID_VERIFICATION",
            "risk": "DOCUMENT_EXPIRED",
            "additional_data": null,
            "log_type": "error",
            "short_description": "Document expired",
            "long_description": "The document's expiration date has passed, rendering it no longer valid for use.",
            "node_id": "id_verification_1"
          }
        ]
      }
    ],
    "liveness_checks": [{ "node_id": "liveness_1", "status": "Approved", "score": 92.0, "warnings": [] }],
    "face_matches": [
      {
        "node_id": "face_match_1",
        "status": "Declined",
        "score": 32.0,
        "warnings": [
          {
            "feature": "FACEMATCH",
            "risk": "LOW_FACE_MATCH_SIMILARITY",
            "additional_data": null,
            "log_type": "error",
            "short_description": "Low face match similarity",
            "long_description": "The facial features of the provided image don't closely match the reference image, suggesting a potential identity mismatch.",
            "node_id": "face_match_1"
          }
        ]
      }
    ],
    "reviews": []
  }
}
```### جلسة شركة مقبولة (`status.updated`، KYB)```json theme={null}
{
  "event_id": "uuid",
  "webhook_type": "status.updated",
  "timestamp": 1774970000,
  "created_at": 1774969994,
  "application_id": "uuid",
  "environment": "live",
  "sandbox_scenario": null,
  "session_id": "kyb-uuid",
  "business_session_id": "kyb-uuid",
  "session_kind": "business",
  "status": "Approved",
  "vendor_data": "biz_7",
  "vendor_business_id": "acme-inc",
  "decision": {
    "session_id": "kyb-uuid",
    "session_kind": "business",
    "status": "Approved",
    "features": ["REGISTRY_CHECK", "AML", "DOCUMENT_VERIFICATION"],
    "registry_checks": [{ "node_id": "registry_1", "status": "Approved", "company_name": "Acme Corp", "...": "..." }],
    "aml_screenings": [{ "node_id": "aml_1", "status": "Approved", "entity_type": "company", "total_hits": 0, "hits": [] }],
    "document_verifications": [{ "node_id": "doc_1", "status": "Approved", "document_type": "certificate_of_incorporation" }],
    "key_people_checks": [],
    "reviews": []
  }
}
```### تم إنشاء معاملة (`transaction.created`)```json theme={null}
{
  "event_id": "uuid",
  "webhook_type": "transaction.created",
  "timestamp": 1774970000,
  "created_at": 1774970000,
  "application_id": "uuid",
  "environment": "live",
  "transaction_id": "uuid",
  "txn_id": "finance0001",
  "status": "APPROVED",
  "score": 0,
  "severity": "UNKNOWN",
  "amount": "1200.00",
  "currency": "EUR",
  "direction": "OUTBOUND",
  "action_required": null
}
```### تغيّرت حالة تبادل Travel Rule (`travel_rule.status.updated`)```json theme={null}
{
  "event_id": "9c0c8b8a-1111-4222-9333-666666666666",
  "webhook_type": "travel_rule.status.updated",
  "timestamp": 1774970000,
  "created_at": 1774970000,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "transaction_id": "22222222-3333-4444-5555-666666666666",
  "txn_id": "trv-2026-07-05-001",
  "travel_rule_status": "COMPLETED",
  "rail": "INTERNAL",
  "direction": "OUTBOUND"
}
```يحمل `travel_rule_status` إحدى حالات التبادل الثلاث عشرة الموثّقة في [Travel Rule](https://docs.didit.me/ar/transaction-monitoring/travel-rule.md#دورة-الحياة).
القيمة `rail` هي `INTERNAL` أو `TRP` أو `GTR` أو `TRUST` أو `VERIFYVASP` أو `SYGNA` أو `EMAIL` (قيمة فارغة إلى أن يُختار مسار، أو إذا تعذّر تحديد أي مسار، ودائماً في تسجيلات sunrise)، أو `TEST_VASP` لتحويل في البيئة التجريبية ردّ عليه [Didit Test VASP](https://docs.didit.me/ar/transaction-monitoring/travel-rule-sandbox.md)، وقيمة `direction` هي `INBOUND` أو `OUTBOUND`.
يحمل حدث البيئة التجريبية `"environment": "sandbox"`، ويحمل حدث `TEST_VASP` أيضاً `test_vasp: true` و`test_vasp_scenario` (السيناريو الذي قدّم الاستجابة).عندما تُنشئ Didit معاملة لتحويل Travel Rule (استدعاء [`POST /v3/transactions/`](https://docs.didit.me/ar/management-api/transactions/create)، أو [تسجيل sunrise بعد الإيداع](https://docs.didit.me/ar/transaction-monitoring/travel-rule.md#التسجيل-اللاحق-للإيداع)، أو استفسار `TRP` وارد، أو المرحلة الواردة على المسار `INTERNAL`)، فإنها ترسل حدث `transaction.created` الخاص بتلك المعاملة أولاً، ثم أحداث `travel_rule.status.updated` الخاصة بالتحويل بعدها.
قد ترسل أيضاً قاعدة تغيّر حالة المعاملة خلال تلك الخطوة الحدث `transaction.status.updated` قبل `transaction.created`، وتسجيل sunrise الذي يرتبط بمعاملة إيداع سبق أن أنشأتها لا يرسل `transaction.created`.
يُسلَّم كل webhook وتُجرى إعادة المحاولة له على حدة، لذا قد يصل حدث أُعيد إرساله بعد حدث لاحق له: اجعل معالجك يعتمد على `transaction_id`، وأعد قراءة المعاملة عندما تحتاج إلى حالتها الحالية.
لا يحصل التحويل المُرسل عبر TRUST أبداً على حدث `COMPLETED`، لأن TRUST لا يعيد أي شيء تستطيع Didit قراءته: تبقى الحالة `AWAITING_COUNTERPARTY` حتى الموعد النهائي للتأكيد ثم تنتهي إلى `EXPIRED`. أما عبر GTR وVerifyVASP وSygna Bridge، فتقرأ Didit ردّ الطرف المقابل من الشبكة ويأتي الحدث بعده (راجع [شبكات Travel Rule](https://docs.didit.me/ar/transaction-monitoring/travel-rule-networks.md)).### تغيّر امتثال سير العمل (`workflow.compliance_changed`)```json theme={null}
{
  "event_id": "9c0c8b8a-1111-4222-9333-777777777777",
  "webhook_type": "workflow.compliance_changed",
  "timestamp": 1774970000,
  "created_at": 1774970000,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "workflow_id": "22222222-3333-4444-5555-666666666666",
  "workflow_version": 4,
  "workflow_label": "KYC standard",
  "transition": { "from": "compliant", "to": "gaps_detected" },
  "severity": "actionable",
  "kb_version": "2026-10"
}
```تكون كل من `transition.from` و`transition.to` إحدى القيم التالية: `compliant` أو `gaps_detected` أو `pending_change` أو `unknown_coverage` أو `not_evaluated`.
تكون قيمة `severity` هي `actionable` عند رصد فجوات، و`indicative` في حالة تغيير معلّق، و`informative` عند حل الفجوات.
ترسل Didit حدثاً واحداً لكل سير عمل تغيّر.### أُنشئت اقتراحات قواعد المعاملات (`transaction_rule_suggestions.created`)```json theme={null}
{
  "event_id": "9c0c8b8a-1111-4222-9333-888888888888",
  "webhook_type": "transaction_rule_suggestions.created",
  "timestamp": 1774970000,
  "created_at": 1774970000,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "run_id": "33333333-4444-5555-6666-777777777777",
  "open_count": 3,
  "suggestions": [
    {
      "suggestion_id": "44444444-5555-6666-7777-888888888888",
      "kind": "TWEAK",
      "title": "Raise the review threshold on high-value transfers",
      "rule_id": "55555555-6666-7777-8888-999999999999",
      "expected_impact": {}
    }
  ]
}
```تكون قيمة `kind` هي `ADD` لقاعدة جديدة، و`TWEAK` لتعديل قاعدة موجودة، و`DISABLE` لإيقاف قاعدة. تكون قيمة `rule_id` هي `null` عند `ADD`.
`open_count` هو عدد الاقتراحات التي ما زالت مفتوحة بعد التشغيل، بما فيها الجديدة.
تتحكم إعدادات اقتراحات القواعد في تفعيل هذا webhook أو تعطيله؛ راجع [webhooks المعاملات](https://docs.didit.me/ar/transaction-monitoring/webhooks.md).### أحداث Investigator (`investigator.*`)تستخدم الأحداث الاثني عشر `investigator.*` الغلاف نفسه (`event_id`، و`webhook_type`، و`timestamp`، و`created_at`، و`application_id`، و`environment`) ورؤوس التوقيع نفسها التي تستخدمها سائر الأحداث. وهي تحمل المعرّفات والأرقام فقط: لا تتضمن أبداً اسماً، ولا سبباً كتبه المراجِع، ولا سرداً، ولا أي شيء كتبه المستخدم النهائي. ولا تحتوي على `session_id`؛ والعنصر الذي يتعلق به الحدث مذكور في `item`.```json theme={null}
{
  "event_id": "9c0c8b8a-1111-4222-9333-777777777777",
  "webhook_type": "investigator.decision.automated",
  "timestamp": 1774970000,
  "created_at": 1774970000,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "run_id": "33333333-4444-5555-6666-777777777777",
  "queue": "kyc",
  "item": { "kind": "session", "id": "cccccccc-dddd-eeee-ffff-000000000000" },
  "recommendation": "approve",
  "confidence": 0.93,
  "provenance": {
    "agent_version": "1",
    "engine": "a1b2c3d4",
    "policy_version": 4,
    "model_id": "a1b2c3d4"
  },
  "decided_by": "investigator",
  "outcome": "approved",
  "action": "approve",
  "final_status": "Approved",
  "agreed": true
}
```| الحدث | الحقول إلى جانب الغلاف |
| - | - |
| `investigator.report.ready` | `run_id`، و`queue`، و`item`، و`recommendation`، و`confidence`، و`provenance`، و`ready_to_accept`، و`questions` (عدد) |
| `investigator.decision.automated` | `run_id`، و`queue`، و`item`، و`recommendation`، و`confidence`، و`provenance`، و`decided_by` (`"investigator"`)، و`outcome`، و`action`، و`final_status`، و`agreed` |
| `investigator.decision.overridden` | مماثل لـ `investigator.decision.automated` مع `decided_by` بقيمة `"human"`، إضافةً إلى `override_category` و`overrode` (`"report"` أو `"automated"`) و`previous_status` عندما تكون قيمة `overrode` هي `"automated"` |
| `investigator.decision.contested` | `run_id`، و`queue`، و`item`، و`recommendation`، و`confidence`، و`provenance`، و`case_id`، و`contested_at` |
| `investigator.question.raised` | `question_id`، و`number`، و`queue`، و`situation`، و`situation_label`، و`origin`، و`rule_action`، و`run_id`، و`item` |
| `investigator.automation.paused` | `reason` (`"manual"` أو `"spend_limit"`)؛ وبالنسبة إلى `"spend_limit"` أيضاً `month` (`YYYY-MM`) و`currency` |
| `investigator.rfi.answered` | `request_id`، و`run_id`، و`queue`، و`item`، و`status`، و`deadline_at`، و`answered_by`، و`answered_at` |
| `investigator.rfi.overdue` | `request_id`، و`run_id`، و`queue`، و`item`، و`status`، و`deadline_at` |
| `investigator.drift.detected` | `queue`، و`reasons`، و`recent_agreement`، و`baseline_agreement`، و`recent_decisions`، و`baseline_decisions`، و`drop_points`، و`psi`، و`from_tier`، و`to_tier` |
| `investigator.sla.breached` | `run_id`، و`queue`، و`item`، و`priority`، و`sla_due_at` |
| `investigator.qa.failed` | `qa_id`، و`run_id`، و`queue`، و`item`، و`kind`، و`overall`، و`scores`، و`sop_complies`، و`missed_risks` (رموز المخاطر)، و`action`، و`decided_at` |
| `investigator.external.report.ready` | `item_id`، و`external_id`، و`queue` (`"external"`)، و`kind`، و`status`، و`recommendation`، و`confidence`، و`policy_action`، و`provenance` (`engine`، و`agent_version`، و`policy_version`، و`model_id`) |تحدد `provenance` الإصدار الذي كتب به Investigator التقرير:- `engine` هو إصدار محرّك Investigator: معرّف مبهم مكوّن من ثمانية محارف. يتغيّر كلما تغيّر المحرّك، لذا فإن أي تقريرين يحملان القيمة نفسها لـ `engine` كتبهما المحرّك نفسه.
- `agent_version` هو إصدار الوكيل.
- `policy_version` هو إصدار سياستك الخاصة التي اتّبعها التقرير.
- `model_id` اسم بديل مُهمَل يحمل القيمة نفسها التي يحملها `engine`. اقرأ `engine` بدلاً منه.
- لم يعد يُرسل `prompt_hash`.يحدّد `item.kind` نوع العنصر (جلسة، أو جلسة شركة، أو معاملة، أو حالة)، ويحدّد `item.id` معرّفه. الطوابع الزمنية التي تنتهي بـ `_at` هي سلاسل نصية بصيغة ISO 8601. اشترك في هذه الأحداث بإدراج السلاسل النصية الدقيقة في `subscribed_events`، كما في أي حدث آخر.### تغيّرت حالة كيان المستخدم (`user.status.updated`)```json theme={null}
{
  "event_id": "uuid",
  "webhook_type": "user.status.updated",
  "timestamp": 1774970000,
  "created_at": 1774969980,
  "application_id": "uuid",
  "environment": "live",
  "vendor_user_id": "uuid",
  "vendor_data": "user_42",
  "status": "BLOCKED",
  "previous_status": "ACTIVE"
}
```### الجلسة المعاد تقديمها```json theme={null}
{
  "event_id": "uuid",
  "webhook_type": "status.updated",
  "timestamp": 1774970000,
  "created_at": 1774969980,
  "application_id": "uuid",
  "environment": "live",
  "sandbox_scenario": null,
  "session_id": "uuid",
  "status": "Resubmitted",
  "vendor_data": "user_42",
  "resubmit_info": {
    "nodes_to_resubmit": [
      { "node_id": "feature_poa", "feature": "PROOF_OF_ADDRESS" }
    ],
    "reasons": { "feature_poa": "Poor document quality" }
  }
}
```## القديم: X-Signature مع الجسم الخاملا يزال `X-Signature` يُرسَل مع كل تسليم، وهو HMAC-SHA256 على **البايتات الدقيقة** التي يرسلها Didit (`json.dumps(..., sort_keys=True, separators=(",", ":"))`). استخدمه فقط عندما تستطيع قراءة الجسم الخام قبل أن يعدّله أي وسيط برمجي.> [tab] Node.js
>
> ```javascript theme={null}
> const express = require("express");
> const bodyParser = require("body-parser");
> const crypto = require("crypto");
>
> const app = express();
> const PORT = process.env.PORT || 1337;
> const WEBHOOK_SECRET_KEY = process.env.WEBHOOK_SECRET_KEY || "YOUR_WEBHOOK_SECRET_KEY";
>
> app.use(bodyParser.json({
>   verify: (req, _res, buf, encoding) => {
>     if (buf && buf.length) req.rawBody = buf.toString(encoding || "utf8");
>   },
> }));
>
> app.post("/webhook", (req, res) => {
>   const signature = req.get("X-Signature");
>   const timestamp = req.get("X-Timestamp");
>   if (!signature || !timestamp || !req.rawBody) return res.status(401).end();
>
>   if (Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp, 10)) > 300) {
>     return res.status(401).json({ message: "Stale timestamp" });
>   }
>
>   const expected = crypto.createHmac("sha256", WEBHOOK_SECRET_KEY).update(req.rawBody).digest("hex");
>   const a = Buffer.from(expected, "utf8");
>   const b = Buffer.from(signature, "utf8");
>   if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
>     return res.status(401).json({ message: "Invalid signature" });
>   }
>
>   const event = JSON.parse(req.rawBody);
>   // Handle event.webhook_type ...
>   return res.json({ ok: true });
> });
>
> app.listen(PORT);
> ```> [tab] Python
>
> ```python theme={null}
> from fastapi import FastAPI, Request, HTTPException
> from time import time
> import hmac
> import hashlib
> import os
>
> app = FastAPI()
>
>
> def verify_raw(body: str, signature: str, timestamp: str, secret: str) -> bool:
>     if abs(int(time()) - int(timestamp)) > 300:
>         return False
>     expected = hmac.new(secret.encode("utf-8"), body.encode("utf-8"), hashlib.sha256).hexdigest()
>     return hmac.compare_digest(signature, expected)
>
>
> @app.post("/webhook")
> async def handle(request: Request):
>     body = (await request.body()).decode("utf-8")
>     sig = request.headers.get("x-signature")
>     ts = request.headers.get("x-timestamp")
>     secret = os.environ["WEBHOOK_SECRET_KEY"]
>
>     if not sig or not ts or not verify_raw(body, sig, ts, secret):
>         raise HTTPException(status_code=401, detail="Unauthorized")
>     return {"ok": True}
> ```## القديم: تنسيق webhook من V2> [!WARNING]
>
> الحقول أدناه تنطبق فقط عندما تكون الوجهة مثبّتة على `webhook_version: "v2"`. يوجد أيضاً تثبيت أقدم هو `"v1"` (وهو شكل ثالث للقرار بصيغة المفرد) للوجهات التي أُنشئت قبل V2، فتواصل مع الدعم قبل الاعتماد عليه. ينبغي أن تستخدم التكاملات الجديدة **V3** (الافتراضي)، الموثّق أعلاه، والذي يستخدم المصفوفات بصيغة الجمع.| الجانب | V2 | V3 |
| - | - | - |
| ترويسات التوقيع | `X-Signature`، `X-Signature-V2`، `X-Signature-Simple` (تُرسَل الثلاثة بصرف النظر عن الإصدار، وقد أصبح Simple [مُهمَلاً](#x-signature-simple-مهمَل)) | نفسه |
| أسماء حقول القرار | كائنات بصيغة المفرد (`id_verification`، `liveness`، `face_match`، ...) | مصفوفات بصيغة الجمع (`id_verifications`، `liveness_checks`، `face_matches`، ...) |
| مثيلات متعددة من الميزة | غير مدعومة | مدعومة عبر مصفوفات لكل ميزة باستخدام `node_id` |
| درجات AML | `aml.score`، `aml.hits[].match_score`، `aml.hits[].risk_score` | القيم نفسها ضمن `aml_screenings[]` |كائنات الميزات نفسها في الإصدارين متطابقة: تتلقى الوجهة المثبّتة على V2 بالضبط `match_score` و`risk_score` و`score_breakdown` و`risk_view` و`review_status` لكل تطابق، والدرجة الإجمالية `score` كما يتلقاها V3 (انظر [تقرير AML](https://docs.didit.me/ar/core-technology/aml-screening/report-aml-screening.md#ثلاث-درجات،-وليس-حكماً-واحداً)). التثبيت على V2 يغيّر مكان تقرير AML داخل `decision`، لا محتواه.إذا كان مستهلك لاحق ما زال يحتاج إلى بنى V2، فأنشئ وجهة منفصلة مثبّتة على `webhook_version: "v2"`، وأبقِ مستهلكيك الجدد على V3.## ذات صلة- [واجهة API لوجهات webhook](https://docs.didit.me/ar/management-api/webhook/list-destinations.md): عرض الوجهات وإنشاؤها وتحديثها وحذفها وفحص سجلات التسليم.
- [إنشاء وجهة webhook](https://docs.didit.me/ar/management-api/webhook/create-destination.md): مخطط الطلب والاستجابة، ويشمل `secret_shared_key` الخاص بالوجهة.
- [نماذج البيانات](https://docs.didit.me/ar/reference/data-models.md): المخطط المرجعي لكل كائن خاص بميزة مضمَّن في `decision`.
- [مخطط القرار](https://docs.didit.me/ar/sessions-api/retrieve-session.md): قائمة الحقول الكاملة لكائن `decision` المضمَّن في webhooks الخاصة بالجلسات.
- [حالات التحقق](https://docs.didit.me/ar/integration/verification-statuses.md): قيم `status` الدقيقة والانتقالات المشار إليها أعلاه.
- [تحديد المعدل](https://docs.didit.me/ar/integration/rate-limiting.md): الحدود التي تنطبق عند الاستعلام الدوري عن `GET /v3/session/{id}/decision/` كخيار احتياطي.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.