> ## 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.

# webhook: रीयल-टाइम KYC और KYB इवेंट, HMAC साइनिंग

> Didit से रीयल-टाइम सत्यापन, इकाई और लेन-देन webhook इवेंट पाएँ। HMAC-SHA256 साइनिंग, कई webhook गंतव्यों पर भेजना और सीमित पुनः प्रयास।

> [!NOTE]
>
> प्रमाणीकरण गतिविधि और उससे जुड़ी रिलीज़-निर्भर webhook डिलीवरी [Authentication events and webhooks](https://docs.didit.me/hi/authentication/events-and-webhooks.md) में दस्तावेज़ित है। मौजूदा गंतव्य API के साथ सूची में न दिए गए auth इवेंट प्रकार रजिस्टर न करें।

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 जब भी कोई verification session, business session, vendor user, vendor business या transaction अपनी स्थिति बदलता है, तो आपके backend को **real-time अपडेट** भेजता है। एक या अधिक webhook गंतव्यों को उन इवेंट के लिए सब्सक्राइब करने से आप परिणाम तैयार होते ही प्रतिक्रिया दे सकते हैं, आमतौर पर उस क्षण से कुछ सेकंड के भीतर जब सत्यापन करने वाला उपयोगकर्ता अपना flow पूरा करता है।Webhooks **अनुशंसित** इंटीग्रेशन पैटर्न हैं। [`GET /v3/session/{id}/decision/`](https://docs.didit.me/hi/sessions-api/retrieve-session) पर polling एक fallback के रूप में समर्थित है, लेकिन यह धीमा है, इसमें अधिक अनुरोध करने पड़ते हैं, और `data.updated`, `transaction.status.updated` तथा इकाई-स्तरीय बदलावों जैसे इवेंट छूट जाते हैं, जो केवल webhooks के माध्यम से भेजे जाते हैं। Webhook delivery को source of truth मानें और polling केवल cold start के समय या reconciliation के लिए करें।Didit Academy के इंटीग्रेशन पाठ का यह अध्याय webhooks के अवलोकन से आगे बढ़ता है।इंटीग्रेशन Academy का यह पाठ 3:12 से webhooks को कवर करता है: webhook गंतव्य जोड़ना, इवेंट चुनना, और वह साइनिंग सीक्रेट जिसे आप नीचे सत्यापित करेंगे।[Webhooks अवलोकन — Didit Academy](https://www.youtube.com/embed/fX03-WEu_EI?start=192\&rel=0\&playsinline=1)> [!NOTE]
>
> **Cloudflare / WAF उपयोगकर्ता।** Didit स्थिर IP `18.203.201.92` से `User-Agent: DiditWebhook/2.0 +https://didit.me` के साथ webhooks भेजता है। यदि आपका edge अज्ञात क्लाइंट को ब्लॉक करता है, तो इस IP को **Security > WAF > Tools > IP Access Rules** में अनुमति दें और webhook प्राप्त करने वाले होस्टनेम के लिए **Allow** चुनें।## इवेंट प्रकारकिसी webhook गंतव्य के `subscribed_events` array में सटीक मान का उपयोग करें। यहाँ कोई वाइल्डकार्ड नहीं है। आप जिन भी परिवारों को प्राप्त करना चाहते हैं, उन सभी को सूचीबद्ध करें। आप इवेंट्स को कई webhook गंतव्यों में बाँट भी सकते हैं।| इवेंट | संसाधन | कब ट्रिगर होता है |
| - | - | - |
| `status.updated` | KYC सत्र या KYB व्यवसाय सत्र | जब किसी सत्र की स्थिति बदलती है (Not Started → In Progress → Approved / Declined / In Review / Abandoned / Resubmitted)। KYB payload में `session_kind: "business"` और `business_session_id` शामिल होते हैं। |
| `data.updated` | KYC सत्र या KYB व्यवसाय सत्र | सत्यापन डेटा बनने के बाद संपादित होता है (KYC, POA, NFC, KYB रजिस्ट्री, KYB दस्तावेज़ आदि में मैन्युअल समीक्षक द्वारा किए गए सुधार)। |
| `user.status.updated` | उपयोगकर्ता इकाई | एकीकृत उपयोगकर्ता की स्थिति `ACTIVE`, `FLAGGED` और `BLOCKED` के बीच बदलती है (कंसोल कार्रवाई, नियम कार्रवाई, ब्लॉकलिस्ट)। इसमें `previous_status` शामिल है। |
| `user.data.updated` | उपयोगकर्ता इकाई | एकीकृत उपयोगकर्ता की प्रोफ़ाइल, काउंटर, मेटाडेटा या स्वीकृत पहचानकर्ता बदलते हैं। इसमें `changed_fields` और `previous` / `current` मानों वाला `changes` map शामिल है। |
| `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/hi/transaction-monitoring/aml-screening.md#लेन-देन-हैश-जो-अभी-इंडेक्स-नहीं-हुए-हैं))। |
| `travel_rule.status.updated` | Travel Rule | [Travel Rule](https://docs.didit.me/hi/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 | कोई रिपोर्ट निर्णय के बिना अपनी कतार के SLA से अधिक समय तक रुकी रही। |
| `investigator.qa.failed` | Investigator | किसी व्यक्ति द्वारा लिया गया निर्णय Investigator की गुणवत्ता समीक्षा में विफल रहा। |
| `investigator.external.report.ready` | Investigator | Didit के बाहर से भेजे गए आइटम की रिपोर्ट तैयार है। |
| `auth.signup` | प्रमाणीकरण | कोई व्यक्ति [प्रमाणीकरण](https://docs.didit.me/hi/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/hi/standalone-apis/kyb-registry) को भेजते हैं।> [!TIP]
>
> Business Console > API & Webhooks > **Try Webhook** मेनू इन सभी इवेंट के लिए पूरी तरह से बने पेलोड भेजता है, जिनमें स्वीकृत, अस्वीकृत, समीक्षाधीन, Unicode नाम, KYC/KYB, entity, activity और लेन-देन इवेंट शामिल हैं। लाइव होने से पहले अपने endpoint की जाँच करने के लिए इसका उपयोग करें।## payload संरचना### एन्वेलपहर webhook का एक ही एन्वेलप होता है। फ़ील्ड हमेशा `sort_keys=True` के साथ और संक्षिप्त विभाजकों (`,` / `:`) का उपयोग करके सीरियलाइज़ किए जाते हैं, ताकि एक ही payload दोनों ओर एक ही हस्ताक्षर दे।| फ़ील्ड | प्रकार | टिप्पणियाँ |
| - | - | - |
| `event_id` | `uuid` | इवेंट का स्थायी पहचानकर्ता। जिन सभी गंतव्यों पर यह इवेंट भेजा जाता है, उन सभी को **वही** `event_id` भेजा जाता है और पुनः प्रयास पर भी इसे दोहराया जाता है। अपनी आइडेम्पोटेंसी इसी पर आधारित करें। |
| `webhook_type` | string | ऊपर दिए गए [इवेंट प्रकारों](#इवेंट-प्रकार) में से एक। |
| `timestamp` | int (Unix सेकंड) | वह समय जब Didit ने webhook भेजा। हस्ताक्षर सत्यापन इसका उपयोग करता है। हर पुनः प्रयास पर ताज़ा किया जाता है। |
| `created_at` | int (Unix सेकंड) | वह समय जब अंतर्निहित रिकॉर्ड अंतिम बार अपडेट हुआ था। |
| `application_id` | `uuid` | वह एप्लिकेशन जिसका यह गंतव्य है। |
| `environment` | `"live"` \| `"sandbox"` | इवेंट लाइव या सैंडबॉक्स सत्र/एप्लिकेशन से आया है या नहीं। |
| `status` | string | हर सत्र, एंटिटी और लेन-देन इवेंट पर मौजूद होता है (जिसमें `*.data.updated` भी शामिल है); `investigator.*` इवेंट में कोई `status` नहीं होता (नीचे Investigator payload देखें)। सत्र इवेंट [सत्यापन स्थिति लेबल](https://docs.didit.me/hi/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` | int | वर्कफ़्लो का संस्करण। |
| `vendor_data` | string | आपका आंतरिक पहचानकर्ता, जब सत्र बनाते समय दिया गया हो। |
| `vendor_business_id` | string | केवल KYB सत्रों पर, जब दिया गया हो। |
| `sandbox_scenario` | string \| null | हर सत्र इवेंट पर (KYC और KYB दोनों)। सैंडबॉक्स सत्र पर जब कोई सिनेरियो सक्रिय किया गया हो, तब सक्रिय [सैंडबॉक्स](https://docs.didit.me/hi/integration/sandbox-testing.md) सिनेरियो का स्लग (जैसे `"decline_aml_hit"`)। लाइव सत्र पर, या ऐसे सैंडबॉक्स सत्र पर जिस पर कोई सिनेरियो सक्रिय न हो, `null`। एंटिटी या लेन-देन webhooks पर मौजूद नहीं होता। |
| `metadata` | object | सत्र बनाते समय आपने जो भी metadata जोड़ा था, उसमें से आंतरिक डुप्लिकेट-पहचान कुंजियाँ हटाकर। |
| `trigger` | string | वैकल्पिक। यह बताता है कि 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` | object | जब `status` `Approved`, `Declined`, `In Review` या `Abandoned` हो, तब मौजूद। [V3 सत्र निर्णय](https://docs.didit.me/hi/sessions-api/retrieve-session.md) के समान है। |
| `resubmit_info` | `{ nodes_to_resubmit, reasons }` | जब `status` `Resubmitted` हो, तब मौजूद (कोई `decision` नहीं)। |
| `decided_by` | string | केवल `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/hi/sessions-api/retrieve-session) लौटाता है। हर सुविधा के फ़ील्ड का मानक संदर्भ (`id_verifications[]`, `aml_screenings[]` आदि पर हर प्रॉपर्टी) [डेटा मॉडल](https://docs.didit.me/hi/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/hi/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/hi/core-technology/bank-verification/fallbacks.md#परिणाम-में-इसे-पढ़ें)। खाता जाँच से सत्यापित खाते से कोई वित्तीय डेटा नहीं पढ़ा जाता, इसलिए `financial_data.not_collected` उन बैलेंस, लेन-देन या आय को सूचीबद्ध करता है जो चरण ने माँगे थे। देखें [बैंक सत्यापन विधियाँ](https://docs.didit.me/hi/core-technology/bank-verification/verification-methods.md#परिणाम-में-विधि-पढ़ें)।### Investigator ब्लॉकउपयोगकर्ता या व्यवसाय सत्र के लिए V3 `decision` में `investigator` भी शामिल होता है: सत्र की नवीनतम [Investigator](https://docs.didit.me/hi/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 का अपना निर्णय कायम है तब `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/hi/core-technology/document-ai/overview.md)।उपयोगकर्ता सत्यापन के निर्णयों में एक एकल `biometric_consent` ऑब्जेक्ट भी होता है: यह बताता है कि सत्र को स्पष्ट बायोमेट्रिक सहमति चाहिए थी या नहीं, साथ में `trigger`, उत्तर की `status` और उसका साक्ष्य। `status: "pending"` के साथ `reason: "BIOMETRIC_CONSENT_UNSUPPORTED_SDK"` का अर्थ है कि 4.10.0 से पहले के नेटिव SDK ने सहमति चरण के बिना चेहरा कैप्चर किया। देखें [बायोमेट्रिक सहमति](https://docs.didit.me/hi/getting-started/biometric-consent.md)।### पहचान सत्यापन विधि कुंजियाँहर `id_verifications[]` आइटम बताता है कि उसे कौन सी [पहचान सत्यापन विधि](https://docs.didit.me/hi/core-technology/id-verification/verification-methods.md) ने बनाया, ताकि हैंडलर को मिले साक्ष्य के आधार पर शाखा बनाई जा सके:| कुंजी | मान |
| - | - |
| `verification_method` | `document`, `id_lookup` या `wallet` |
| `assurance` | `documentary`, `data_match` या `cryptographic` |
| `wallet_provider` | जब वॉलेट ने इसे बनाया हो तो कैटलॉग वॉलेट id, अन्यथा `null` |
| `id_lookup` | जब विधि `id_lookup` थी तो रजिस्ट्री साक्ष्य, अन्यथा `null` |
| `wallet_verification` | जब विधि `wallet` थी तो क्रेडेंशियल साक्ष्य, अन्यथा `null` |
| `fallback_from` | गैर-दस्तावेज़ विधि ने ID चरण क्यों समाप्त नहीं किया: विधि, कारण, और क्या उपयोगकर्ता दस्तावेज़ कैप्चर पर लौटा या उसे अस्वीकार किया गया |मौजूदा हैंडलर बिना बदलाव के काम करते रहते हैं: दस्तावेज़ सत्र `verification_method` को `document` पर सेट करता है और बाकी `null` छोड़ता है। फ़ील्ड-दर-फ़ील्ड प्रकार [Data models](https://docs.didit.me/hi/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"
  }
}
```### एंटिटी इवेंट payloads```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` ऐरे तथा `{ field: { previous, current } }` के रूप का `changes` मैप जुड़ता है। `business.*` इवेंट `vendor_user_id` के बजाय `vendor_business_id` का उपयोग करते हैं।### लेन-देन इवेंट payloadsलेन-देन webhooks उसी फ़ैन-आउट इन्फ्रास्ट्रक्चर पर चलते हैं और वही एनवेलप इस्तेमाल करते हैं, लेकिन सत्र के बजाय लेन-देन की पहचान करते हैं:```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/hi/transaction-monitoring/sdk-transaction-submission.md#आवश्यक-उपयोगकर्ता-क्रियाएँ) में है।नए लेन-देन डिफ़ॉल्ट रूप से `APPROVED` होते हैं। यदि कोई नियम, ब्लॉकलिस्ट या वर्कफ़्लो बनाने के दौरान status बदलता है, तो प्रारंभिक `transaction.created` payload यही अंतिम status दिखाता है। `transaction.status.updated` में वही संरचना होती है, जिसमें अद्यतन `status` होता है। लेन-देन की स्थितियाँ हैं `APPROVED`, `IN_REVIEW`, `DECLINED` या `AWAITING_USER`; `severity` है `UNKNOWN` / `LOW` / `MEDIUM` / `HIGH` / `CRITICAL` और `direction` है `INBOUND` / `OUTBOUND`। `transaction.created` और `transaction.status.updated` केवल **live** एप्लिकेशन के लिए भेजे जाते हैं। सैंडबॉक्स Travel Rule एक्सचेंज `environment: sandbox` के साथ `travel_rule.status.updated` भेजते हैं।### KYB रजिस्ट्री खोज कॉलबैकअसिंक्रोनस [`POST /v3/kyb/search/`](https://docs.didit.me/hi/standalone-apis/kyb-registry) प्रवाह एक अतिरिक्त payload भेजता है जो webhook गंतव्य इवेंट **नहीं** है: जब आप खोज अनुरोध को `webhook_url` देते हैं, तो रजिस्ट्री का उत्तर मिलते ही Didit उस URL पर `kyb.registry_search.resolved` कॉलबैक POST करता है। यह webhook गंतव्य से तीन तरह से अलग है:- यह **अहस्ताक्षरित** है: इसमें कोई `X-Signature*` हेडर नहीं होता। इसके बजाय Didit इसे `X-Didit-Unsigned-Callback: true` से चिह्नित करता है; `request_id` को खोज अनुरोध द्वारा लौटाए गए id से मिलाकर इसकी पुष्टि करें।
- इवेंट का नाम `event_type` में रहता है (`webhook_type` में नहीं), और इसमें `application_id` या `environment` नहीं होता।
- `timestamp` और `created_at` epoch पूर्णांक हैं, जैसे अन्य webhooks में, लेकिन खोज endpoint के ISO `created_at` से अलग।```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/hi/standalone-apis/kyb-registry.md) देखें।## हस्ताक्षर सत्यापनहर webhook, webhook गंतव्य के `secret_shared_key` का उपयोग करके HMAC-SHA256 से हस्ताक्षरित होता है। तीन हस्ताक्षर हेडर भेजे जाते हैं, ताकि आप अपने स्टैक के लिए वह हेडर चुन सकें जो सही तरह काम करे:| हेडर | हस्ताक्षर में क्या शामिल है | मिडलवेयर-सुरक्षित | कब उपयोग करें |
| - | - | - | - |
| `X-Signature-V2` | क्रमबद्ध, Unicode-संरक्षित कॉम्पैक्ट JSON (`ensure_ascii=False`) | हाँ | **अनुशंसित**: तब भी काम करता है जब आपका फ़्रेमवर्क बॉडी को दोबारा एन्कोड करे। |
| `X-Signature` | Didit द्वारा भेजे गए अनुरोध के सटीक रॉ बाइट्स (`sort_keys`, `ensure_ascii=True`) | नहीं | केवल तब, जब आप किसी भी parser के छूने से पहले रॉ बॉडी पढ़ सकें। |
| `X-Signature-Simple` **(अप्रचलित)** | `"{timestamp}:{session_id}:{status}:{webhook_type}"` | हाँ | अंतिम विकल्प के तौर पर बैकअप, जो केवल **आवरण** को प्रमाणित करता है और `decision` को प्रमाणित **नहीं** करता। इसे उपयोग करने से पहले नीचे दी गई अप्रचलन सूचना पढ़ें। |Didit हमेशा ये हेडर भेजता है:| हेडर | उदाहरण | अर्थ |
| - | - | - |
| `Content-Type` | `application/json` | बॉडी JSON है। |
| `User-Agent` | `DiditWebhook/2.0 +https://didit.me` | Didit के webhook worker की पहचान करता है। |
| `X-Timestamp` | `1774970000` | वह Unix epoch सेकंड, जब webhook भेजा गया था। यदि `abs(now - X-Timestamp) > 300` है तो अनुरोध अस्वीकार करें। |
| `X-Signature` / `X-Signature-V2` / `X-Signature-Simple` | हेक्स डाइजेस्ट | ऊपर बताई गई बॉडी या कैनोनिकल स्ट्रिंग का HMAC-SHA256। |किसी webhook गंतव्य का साइनिंग सीक्रेट रोटेट करने के बाद 24 घंटों तक डिलीवरी में `X-Signature-Previous`, `X-Signature-V2-Previous` और `X-Signature-Simple-Previous` भी आते हैं (देखें [साइनिंग सीक्रेट रोटेशन](#साइनिंग-सीक्रेट-रोटेशन))।### साइनिंग सीक्रेट रोटेशनआप Business Console में webhook गंतव्य का साइनिंग सीक्रेट रोटेट करते हैं।
रोटेशन के बाद 24 घंटों तक, उस गंतव्य की हर डिलीवरी में तीन और हेडर आते हैं, जो बदले गए सीक्रेट से बनाए जाते हैं:| हेडर | इसके समान हस्ताक्षर | किससे साइन किया गया |
| - | - | - |
| `X-Signature-V2-Previous` | `X-Signature-V2` | बदला गया सीक्रेट |
| `X-Signature-Previous` | `X-Signature` | बदला गया सीक्रेट |
| `X-Signature-Simple-Previous` | `X-Signature-Simple` (अप्रचलित) | बदला गया सीक्रेट |जो हैंडलर किसी सामान्य हस्ताक्षर हेडर या उसके `-Previous` हेडर में से किसी एक के मिलान को स्वीकार करता है, वह नया सीक्रेट डिप्लॉय करते समय भी सत्यापन जारी रखता है: पुराना सीक्रेट `-Previous` हेडर से मेल खाता है और नया सीक्रेट सामान्य हेडर से।
नीचे दिए गए कोड सैंपल यही करते हैं।
नया सीक्रेट 24 घंटों के भीतर डिप्लॉय करें, क्योंकि उसके बाद केवल नया सीक्रेट ही सत्यापित होता है।
[`GET /v3/webhook/destinations/{destination_uuid}/`](https://docs.didit.me/hi/management-api/webhook/get-destination) पर `previous_secret_expires_at` बताता है कि 24 घंटे कब खत्म होते हैं।रिट्राई और रीसेंड, भेजे जाने के समय गंतव्य के वर्तमान सीक्रेट से फिर से साइन किए जाते हैं, और `-Previous` हेडर केवल तब तक रहते हैं जब तक 24 घंटे चलते हैं।
24 घंटों के भीतर फिर से रोटेट करने पर सबसे पुराना सीक्रेट तुरंत रद्द हो जाता है: `-Previous` हेडर के माध्यम से नए रोटेशन से ठीक पहले जो सीक्रेट वर्तमान था, केवल उसी से सत्यापन होता रहता है, और यह नए रोटेशन से 24 घंटों तक चलता है।`-Previous` हेडर केवल webhook गंतव्यों के लिए होते हैं।
लीगेसी `webhook_url` पर हस्ताक्षर करने वाला एप्लिकेशन-स्तरीय `secret_shared_key` कोई ओवरलैप नहीं देता: [`PATCH /v3/webhook/`](https://docs.didit.me/hi/sessions-api/management-api#webhook-कॉन्फ़िगरेशन) पर `rotate_secret_key` से इसे रोटेट करने पर पुराना सीक्रेट तुरंत अमान्य हो जाता है।### `X-Signature-Simple` अप्रचलित है> [!WARNING]
>
> **`X-Signature-Simple` envelope को प्रमाणित करता है, 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}"` पर हस्ताक्षर करते हैं, जो यह नहीं बताता कि इवेंट **किस** लेन-देन या entity से जुड़ा है। एक ही सेकंड में, एक ही status के साथ रिपोर्ट किए गए दो अलग लेन-देन का `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` ही एकमात्र हेडर है जिसकी आप जाँच करते हैं, तो उन चार फ़ील्ड्स के बाहर की हर चीज़ को **अविश्वसनीय** मानें और रिकॉर्ड API से फिर से प्राप्त करें। सेशन के लिए [`GET /v3/session/{session_id}/decision/`](https://docs.didit.me/hi/sessions-api/retrieve-session) और लेन-देन के लिए [`GET /v3/transactions/{transaction_id}/`](https://docs.didit.me/hi/management-api/transactions/get) का उपयोग करें, क्योंकि वही आधिकारिक स्रोत है।यह एक संगतता सीमा है, जिसे हम जगह पर पैच नहीं कर सकते। चार-फ़ील्ड वाली कैनोनिकल स्ट्रिंग एक वायर अनुबंध है: `X-Signature-Simple` सत्यापित करने वाला हर रिसीवर ठीक इन्हीं चार फ़ील्ड्स की पुनर्गणना करता है, इसलिए हेडर क्या साइन करता है, यह बदलने से उन सभी का सत्यापन चुपचाप टूट जाएगा। बॉडी को कवर करने के लिए JSON के एक ऐसे निर्दिष्ट कैनोनिकल रूप की भी ज़रूरत है, जिसे दोनों पक्ष बाइट-दर-बाइट दोहरा सकें, और Didit यह रूप `X-Signature-V2` के रूप में पहले से देता है। अपने कैनोनिकलाइज़ेशन नियमों वाला दूसरा बॉडी-कवरिंग वेरिएंट जोड़ने के बजाय यह हेडर जैसा है वैसा ही रहता है, और मौजूदा पूर्ण-payload वेरिएंट्स के पक्ष में अप्रचलित घोषित किया गया है।इसका आपके लिए अर्थ है:- **वायर पर कुछ नहीं बदलता।** `X-Signature-Simple` हर डिलीवरी में पहले की तरह उसी तरीके से गणना करके भेजा जाता रहेगा। मौजूदा सत्यापनकर्ता काम करते रहेंगे, और किसी भी हटाने की घोषणा हम पहले से बहुत पहले करेंगे।
- **नए इंटीग्रेशन को `X-Signature-V2` का उपयोग करना चाहिए।** यह मिडलवेयर-सुरक्षित है *और* पूरे payload को कवर करता है।
- **यदि आप `X-Signature-Simple` से हट नहीं सकते**, तो उसे सत्यापित करें, और फिर webhook बॉडी से पढ़ने के बजाय रिकॉर्ड API से प्राप्त करें।### तीन वेरिएंट क्यों?कुछ वेब फ़्रेमवर्क (Express body parsers, Django middleware, API gateways) आपके handler द्वारा JSON पढ़ने से पहले उसे चुपचाप दोबारा एन्कोड कर देते हैं। यदि मूल बॉडी में `"José"` था और आपका middleware उसे दोबारा लिखकर `"José"` कर देता है, तो डेटा एक जैसा होने पर भी बाइट्स बदल जाते हैं। इससे `X-Signature` विफल हो जाता है। `X-Signature-V2` एक कैनोनिकल JSON रूप से बनता है (कुंजियाँ क्रमबद्ध, कॉम्पैक्ट separators, **बिना escape किया गया Unicode**), जिसे लगभग हर middleware निर्धारित रूप से दोहरा देता है। और अप्रचलित `X-Signature-Simple`, जब वह भी विफल हो जाए, तब केवल एक छोटी, parser से स्वतंत्र स्ट्रिंग पर हस्ताक्षर करने पर निर्भर रहता है। चूँकि वह केवल उस छोटी स्ट्रिंग पर हस्ताक्षर करता है और कभी बॉडी पर नहीं, इसलिए वह बॉडी की पुष्टि नहीं कर सकता।### अनुशंसित सत्यापन क्रम1) सबसे पहले **`X-Signature-V2`** आज़माएँ। `JSON.parse(body)` को sorted keys और Unicode को यथावत रखते हुए फिर से encode करें, फिर `HMAC-SHA256(secret, canonical)` निकालें और हेडर से `timingSafeEqual` द्वारा तुलना करें।
2) अगर V2 विफल हो और आप raw bytes पढ़ सकते हैं, तो `HMAC-SHA256(secret, rawBody)` के विरुद्ध **`X-Signature`** आज़माएँ।
3) केवल तभी जब दोनों विफल हों, अप्रचलित **`X-Signature-Simple`** का सहारा लें, जो `HMAC-SHA256(secret, "{timestamp}:{session_id}:{status}:{webhook_type}")` के विरुद्ध जाँचा जाता है। यह केवल आवरण को प्रमाणित करता है, इसलिए body के `decision` को अविश्वसनीय मानें और कार्रवाई से पहले [`GET /v3/session/{session_id}/decision/`](https://docs.didit.me/hi/sessions-api/retrieve-session) से उसे फिर से प्राप्त करें।हर चरण पर, जब मेल खाने वाला `-Previous` हेडर मौजूद हो, तो किसी भी हेडर पर मिलान स्वीकार करें (देखें [साइनिंग सीक्रेट रोटेशन](#साइनिंग-सीक्रेट-रोटेशन))।सभी मामलों में, replay हमलों से बचाव के लिए ऐसे किसी भी अनुरोध को **अस्वीकार करें** जिसका टाइमस्टैम्प वर्तमान समय से **5 मिनट** से अधिक अलग हो (`abs(now - X-Timestamp) > 300`), चाहे वह अनुरोध बहुत पुराना हो या भविष्य का हो।## पुनः प्रयास नीतिअगर आपका endpoint `5xx` या `404` से जवाब देता है, तो Didit क्रमशः बढ़ते अंतराल के साथ **2 बार तक** पुनः प्रयास करता है:- **पहला पुनः प्रयास**: पहली विफलता के लगभग **1 मिनट** बाद।
- **दूसरा पुनः प्रयास**: पहले पुनः प्रयास के लगभग **4 मिनट** बाद।दूसरे पुनः प्रयास के बाद डिलीवरी छोड़ दी जाती है। हर डिलीवरी प्रयास (पहला और पुनः प्रयास) Business Console में गंतव्य के **डिलीवरी** टैब के अंतर्गत अलग प्रविष्टि के रूप में लॉग होता है, ताकि आप किसी भी प्रयास को दोबारा चला या जाँच सकें।गंतव्य के डिलीवरी लॉग में हर प्रयास का `attempt` नंबर होता है: किसी इवेंट की पहली डिलीवरी के लिए `1`, और प्रत्येक पुनः प्रयास या पुनः भेजने के लिए, दोहराए जा रहे प्रयास के नंबर से एक अधिक।
स्वचालित पुनः प्रयास हमेशा उसी URL पर जाते हैं जिस पर पहला प्रयास भेजा गया था, भले ही उसके बाद आपने गंतव्य का `url` बदल दिया हो।
मैनुअल पुनः भेजना गंतव्य के वर्तमान URL पर जाता है, इसलिए URL ठीक करने से पुनः भेजना भी ठीक हो जाता है।डिलीवरी से जुड़े अन्य नियम जो जानना ज़रूरी है:- बाहरी HTTP अनुरोध **5 सेकंड** में timeout हो जाता है।
- Didit SSRF सुरक्षा के रूप में private या localhost URLs पर डिलीवरी ब्लॉक करता है और redirects का कभी पालन नहीं करता। ऐसा सार्वजनिक HTTPS endpoint उपयोग करें जो सीधे जवाब दे।
- गंतव्य का `url` जिसमें कच्चा बैकस्लैश, नियंत्रण वर्ण, या प्राधिकरण भाग में एक से अधिक `@` हो, malformed मानकर अस्वीकार किया जाता है। ऐसा URL न बनाया जा सकता है, और न ही मौजूदा URL को फिर से डिलीवर या replay किया जा सकता है। सरल `https://host/path` URL उपयोग करें, जिसमें अधिकतम एक उपयोगकर्ता-सूचना भाग हो (basic-auth शैली वाले webhook URLs के लिए)।
- `2xx` को सफलता माना जाता है। Timeouts और connection failures पुनः प्रयास योग्य माने जाते हैं (डिलीवरी लॉग में ये `504`/`503` के रूप में दिखते हैं)। `3xx` और `404` के अलावा `4xx` responses का **पुनः प्रयास नहीं** होता।
- पुनः प्रयास पर Didit नए `X-Timestamp` के साथ हस्ताक्षरों की फिर से गणना करता है, इसलिए timestamp और signature की जोड़ी हमेशा मेल खाती है।
- पुनः प्रयास और पुनः भेजे गए अनुरोध भेजे जाने के समय गंतव्य के वर्तमान signing secret से signed होते हैं।### डिलीवरी का क्रमDidit यह गारंटी नहीं देता कि webhooks आप तक किस क्रम में पहुँचेंगे।
हर डिलीवरी अलग कतार में रखी जाती है, और पुनः प्रयास पहले प्रयास के कई मिनट बाद पहुँचता है। इसलिए `Approved` के लिए `status.updated` आपके पास `In Progress` के पहले वाले इवेंट से पहले पहुँच सकता है।अपना प्राप्तकर्ता इसी के अनुसार बनाएँ:- webhook को इस संकेत के रूप में लें कि कुछ बदला है। जब क्रम मायने रखता है, तो [`GET /v3/session/{session_id}/decision/`](https://docs.didit.me/hi/sessions-api/retrieve-session) कॉल करें और वर्तमान `status` के आधार पर कार्य करें।
- पुराने इवेंट को नई स्थिति पर अधिलेखित न करने दें: इवेंट के `created_at` की तुलना उस रिकॉर्ड के लिए अंतिम संग्रहीत इवेंट से करें, और पुराने इवेंट को अनदेखा करें।
- दोहराव की पहचान `event_id` से करें, जो पुनः प्रयासों में भी वही रहता है।Didit Travel Rule transfer के इवेंट एक निश्चित क्रम में कतार में रखता है, जिसका वर्णन [Travel Rule exchange status changed](#travel-rule-एक्सचेंज-स्थिति-बदली-गई-travel_rule-status-updated) में है, लेकिन पुनः प्रयास फिर भी उस क्रम को बदल सकता है जिसमें वे पहुँचते हैं।## गंतव्य सेट अप करनाआप गंतव्य **Business Console > API & Webhooks** से या Management API के माध्यम से प्रबंधित कर सकते हैं।**1. गंतव्य बनाएँ**`label`, सार्वजनिक `url`, `webhook_version` (`"v3"`), और `subscribed_events` array के साथ [`POST /v3/webhook/destinations/`](https://docs.didit.me/hi/management-api/webhook/create-destination) कॉल करें (या कंसोल में **गंतव्य जोड़ें** पर क्लिक करें)। इस array में वे सभी इवेंट परिवार शामिल हों जिन्हें यह endpoint प्राप्त करना चाहिए। कम से कम एक इवेंट आवश्यक है, और कोई wildcard नहीं है।**2. secret\_shared\_key सहेजें**बनाने की प्रतिक्रिया में गंतव्य की `secret_shared_key` मिलती है। इसे अपने अन्य सर्वर secrets के साथ सहेजें: यह केवल इसी गंतव्य का है और तीनों HMAC-SHA256 हस्ताक्षर वैरिएंट का इनपुट है। [`GET /v3/webhook/destinations/{destination_uuid}/`](https://docs.didit.me/hi/management-api/webhook/get-destination) इसे API कुंजी वाले कॉलर और उन कंसोल उपयोगकर्ताओं को फिर से लौटाता है जो webhooks पढ़ सकते हैं।**3. endpoint लागू करें**एक सार्वजनिक HTTPS `POST` route बनाएँ, raw body पढ़ें, signature सत्यापित करें (`X-Signature-V2` को प्राथमिकता दें), timestamp window जाँचें, फिर `webhook_type` के आधार पर dispatch करें। काम कतार में डालते ही `2xx` लौटाएँ, और भारी प्रोसेसिंग asynchronously करें।**4. प्राप्ति की पुष्टि करें**गंतव्य का **डिलीवरी** टैब देखें। स्वस्थ webhooks एक-दो सेकंड के भीतर `200` लौटाते हैं। `5xx`/`404` प्रतिक्रियाएँ Didit की पुनः प्रयास नीति को ट्रिगर करती हैं, और लगातार विफलताएँ दूसरे पुनः प्रयास के बाद छोड़ दी जाती हैं।**5. Try Webhook से दोहराएँ**लाइव ट्रैफ़िक चालू करने से पहले, [**Try Webhook**](#टेस्टिंग) कंसोल परिदृश्यों का उपयोग करके अपने endpoint को स्वीकृत, अस्वीकृत, समीक्षाधीन, KYB, entity, activity, और transaction payloads से गुज़ारें।![Business Console में Didit webhook testing सुविधा](https://mintcdn.com/didit-0f962782/z6T2GHM4Zh-iSj-K/images/test-webhook.png?fit=max\&auto=format\&n=z6T2GHM4Zh-iSj-K\&q=85\&s=2787ef3ed7a8016798f951e5ae898e4e)Business Console से सीधे परीक्षण webhook भेजें, ताकि लाइव जाने से पहले अपने endpoint को सत्यापित कर सकें।आप जितने चाहें गंतव्य बना सकते हैं। एक आम पैटर्न है **हर उपभोक्ता के लिए एक गंतव्य**: जैसे KYC इवेंट → आपकी प्रमाणीकरण सेवा, KYB इवेंट → आपकी अनुपालन संचालन सेवा, और लेन-देन → आपकी फ़्रॉड कतार। हर गंतव्य का अपना secret, version और सब्सक्रिप्शन सूची होता है, इसलिए किसी एक उपभोक्ता पर बदली हुई सीक्रेट कुंजी या खराब परिनियोजन का असर बाकी पर नहीं पड़ता।## कोड उदाहरणअपने webhook endpoint की सुरक्षा सुनिश्चित करने के लिए, गंतव्य की `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}`));
```यदि आप किसी भी parser के छूने से पहले raw bytes पढ़ सकते हैं, तो मूल `X-Signature` वैरिएंट भी इसी तरह काम करता है, लेकिन वह raw body पर हस्ताक्षर करता है। पूर्णता के लिए हमने इसे नीचे legacy सेक्शन में रखा है।### 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)PHP के JSON डिफ़ॉल्ट canonical form को **नहीं** दोहराते। उनमें से चार को ओवरराइड करना पड़ता है, और इनमें से किसी एक को भी छोड़ने से हस्ताक्षर वाले bytes बदल जाते हैं और digest कभी मेल नहीं खाता:| PHP डिफ़ॉल्ट | यह क्या उत्पन्न करता है | आपको क्या चाहिए |
| - | - | - |
| `json_decode($raw, true)` (associative) | खाली ऑब्जेक्ट `{}` `[]` बन जाते हैं | `$associative = false` के साथ decode करें, ताकि JSON ऑब्जेक्ट `stdClass` बने रहें |
| `json_encode` forward slashes को escape करता है | `https:\/\/media.didit.me` | `JSON_UNESCAPED_SLASHES` |
| `json_encode` non-ASCII को escape करता है | `Jos\u00e9` के बजाय `José` | `JSON_UNESCAPED_UNICODE` |
| `ksort` संख्या जैसी दिखने वाली keys की तुलना संख्याओं के रूप में करता है | `"9"` `"10"` से पहले क्रम में आता है | `ksort($props, SORT_STRING)` |खाली-ऑब्जेक्ट वाला जाल सबसे ज़्यादा परेशान करता है। Didit payloads में नियमित रूप से खाली maps होते हैं, जैसे `risk_view.countries.risk_scores`, `risk_view.crimes.risk_scores` और `risk_view.custom_list`, इसलिए associative round-trip किसी दुर्लभ अपवाद में नहीं, बल्कि सामान्य स्वीकृत सत्रों पर भी हस्ताक्षर तोड़ देता है।```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 में पूर्ण-मान वाले floats को किसी विशेष प्रबंधन की आवश्यकता नहीं होती।** Didit उन्हें integers के रूप में serialize करता है (`100.0` → `100`), और PHP का `json_encode` पूर्ण float को उसी तरह लिखता है, इसलिए ऊपर Node.js उदाहरण वाले `shortenFloats` चरण की ज़रूरत नहीं है।
- **`serialize_precision` को `-1` पर रखें** (PHP 7.1 से यही डिफ़ॉल्ट है)। यदि आपका होस्ट इसे `17` पर सेट करता है, तो `json_encode` `87.42` को `87.420000000000002` लिखता है और हर `X-Signature-V2` जाँच विफल हो जाती है।यदि आप canonical form को मेल नहीं खिला पा रहे, तो ऊपर दिया `verifySignatureRaw` सरल fallback है: Laravel का `$request->getContent()` बिना छेड़े bytes लौटाता है, इसलिए `X-Signature` तब तक काम करता है जब तक PHP के सामने वाली कोई चीज़ बॉडी को री-एन्कोड नहीं करती। `X-Signature-V2` इसी स्थिति में टिके रहने के लिए डिज़ाइन किया गया है, और इसी कारण V2 को पहले आज़माया जाता है।## टेस्टिंगअपने endpoint को परखने के लिए आपको पूरा सत्यापन चलाने की **आवश्यकता नहीं** है।- **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`) और अपने गंतव्य URL पर एक पूर्ण webhook भेजें।
- हर परिदृश्य उन्हीं तीन हस्ताक्षर हेडरों और canonical encoding के साथ हस्ताक्षरित होता है, जैसी प्रोडक्शन ट्रैफ़िक में होती है, जिसमें यूनिकोड टेस्ट डेटा (`approved_kyc_with_unicode`) भी शामिल है, ताकि आप पुष्टि कर सकें कि आपके मिडलवेयर के माध्यम से `X-Signature-V2` काम करता है। टेस्ट डिलीवरी में नमूना डेटा होता है और उन्हें एक अतिरिक्त `X-Didit-Test-Webhook: true` हेडर से चिह्नित किया जाता है (और `User-Agent` पर `(Test)` प्रत्यय लगा होता है), ताकि आप उन्हें प्रोडक्शन स्थिति से बाहर रख सकें।
- किसी regression को डीबग करने के लिए, गंतव्य के **Deliveries** टैब से किसी भी पिछली डिलीवरी को रीप्ले करें।एंड-टू-एंड स्मोक टेस्ट के लिए, अपने एप्लिकेशन में किसी टेस्ट वर्कफ़्लो के माध्यम से एक वास्तविक सत्यापन चलाएँ। इसके परिणामस्वरूप बने webhooks वही एनवेलप, हेडर और पुनः प्रयास नीति इस्तेमाल करते हैं जो **Try Webhook** परिदृश्यों में होती है।## Examples### स्वीकृत 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/hi/transaction-monitoring/travel-rule.md#जीवनचक्र) में दर्ज 13 एक्सचेंज स्थितियों में से कोई एक लेता है।
`rail` `INTERNAL`, `TRP`, `GTR`, `TRUST`, `VERIFYVASP`, `SYGNA` या `EMAIL` होता है (कोई रेल चुने जाने से पहले, या यदि कोई रेल तय नहीं हो सकी हो तो खाली स्ट्रिंग, और सनराइज़ पंजीकरण में हमेशा खाली स्ट्रिंग)। [Didit Test VASP](https://docs.didit.me/hi/transaction-monitoring/travel-rule-sandbox.md) द्वारा उत्तर दिए गए सैंडबॉक्स ट्रांसफ़र के लिए यह `TEST_VASP` होता है। `direction` `INBOUND` या `OUTBOUND` होता है।
सैंडबॉक्स इवेंट में `"environment": "sandbox"` आता है, और `TEST_VASP` इवेंट में `test_vasp: true` और `test_vasp_scenario` (वह परिदृश्य जिसने उत्तर दिया) भी आते हैं।जब Didit किसी Travel Rule ट्रांसफ़र के लिए लेन-देन बनाता है ([`POST /v3/transactions/`](https://docs.didit.me/hi/management-api/transactions/create) कॉल, [जमा के बाद का सनराइज़ पंजीकरण](https://docs.didit.me/hi/transaction-monitoring/travel-rule.md#डिपॉज़िट-के-बाद-सनराइज़-पंजीकरण), आने वाली `TRP` पूछताछ, या `INTERNAL` रेल का इनबाउंड चरण), तो वह उस लेन-देन के लिए पहले `transaction.created` इवेंट भेजता है और उसके बाद ट्रांसफ़र के `travel_rule.status.updated` इवेंट।
उस चरण के दौरान कोई नियम लेन-देन की स्थिति बदले, तो वह `transaction.created` से पहले `transaction.status.updated` भी भेज सकता है। और यदि सनराइज़ पंजीकरण आपके पहले से बनाए गए जमा लेन-देन से जुड़ता है, तो उसके लिए `transaction.created` नहीं भेजा जाता।
हर webhook अलग से भेजा और दोबारा भेजा जाता है, इसलिए दोबारा भेजा गया इवेंट बाद वाले इवेंट के बाद भी पहुँच सकता है: अपना हैंडलर `transaction_id` पर रखें, और जब आपको लेन-देन की मौजूदा स्थिति चाहिए हो, तो उसे दोबारा पढ़ें।
TRUST के ज़रिए भेजे गए ट्रांसफ़र को कभी `COMPLETED` इवेंट नहीं मिलता, क्योंकि TRUST कोई ऐसा जवाब नहीं लौटाता जिसे Didit पढ़ सके: वह पुष्टि की समय-सीमा तक `AWAITING_COUNTERPARTY` पर रहता है और फिर `EXPIRED` पर समाप्त हो जाता है। GTR, VerifyVASP और Sygna Bridge पर Didit नेटवर्क से काउंटरपार्टी का जवाब पढ़ता है, और इवेंट उसके बाद आता है (देखें [Travel Rule नेटवर्क](https://docs.didit.me/hi/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` होता है। `ADD` के लिए `rule_id` `null` होता है।
`open_count` रन के बाद भी खुले सुझावों की संख्या है, जिनमें नए सुझाव भी शामिल हैं।
नियम-सुझाव सेटिंग्स से यह webhook चालू या बंद होता है; देखें [लेन-देन webhook](https://docs.didit.me/hi/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"`) और, जब `overrode` `"automated"` हो, `previous_status` |
| `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` हर डिलीवरी में अब भी भेजा जाता है; यह Didit द्वारा भेजे गए **ठीक बाइट्स** पर HMAC-SHA256 है (`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}
> ```## पुराना: V2 webhook प्रारूप> [!WARNING]
>
> नीचे दिए गए फ़ील्ड केवल तभी लागू होते हैं जब कोई webhook गंतव्य `webhook_version: "v2"` पर पिन किया गया हो। V2 से पहले बनाए गए गंतव्यों के लिए एक और भी पुराना `"v1"` पिन मौजूद है (एक तीसरी, एकवचन निर्णय संरचना), इस पर भरोसा करने से पहले सहायता से संपर्क करें। नए इंटीग्रेशन को **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/hi/core-technology/aml-screening/report-aml-screening.md#एक-नहीं-तीन-स्कोर))। V2 पर पिन करने से बदलता केवल यह है कि `decision` में AML रिपोर्ट कहाँ स्थित है, उसमें क्या है वह नहीं।यदि कोई डाउनस्ट्रीम उपभोक्ता अब भी V2 संरचना चाहता है, तो `webhook_version: "v2"` पर पिन किया हुआ एक अलग गंतव्य बनाएँ और अपने नए उपभोक्ताओं को V3 पर रखें।## संबंधित- [Webhook गंतव्य API](https://docs.didit.me/hi/management-api/webhook/list-destinations.md): गंतव्यों की सूची देखें, बनाएँ, अपडेट करें, हटाएँ और डिलीवरी लॉग देखें।
- [Webhook गंतव्य बनाएँ](https://docs.didit.me/hi/management-api/webhook/create-destination.md): अनुरोध/रिस्पॉन्स स्कीमा, जिसमें गंतव्य का `secret_shared_key` शामिल है।
- [डेटा मॉडल](https://docs.didit.me/hi/reference/data-models.md): `decision` में एम्बेड किए गए हर प्रति-सुविधा ऑब्जेक्ट की कैनोनिकल स्कीमा।
- [निर्णय स्कीमा](https://docs.didit.me/hi/sessions-api/retrieve-session.md): सत्र webhook में एम्बेड किए गए `decision` ऑब्जेक्ट की पूरी फ़ील्ड सूची।
- [सत्यापन स्थितियाँ](https://docs.didit.me/hi/integration/verification-statuses.md): ऊपर संदर्भित सटीक `status` मान और उनके बदलाव।
- [रेट लिमिटिंग](https://docs.didit.me/hi/integration/rate-limiting.md): वे सीमाएँ जो फ़ॉलबैक के रूप में `GET /v3/session/{id}/decision/` पोलिंग करते समय लागू होती हैं।


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