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

# Prompt de integración para los tres productos de Didit

> Un prompt reutilizable de Didit para verificación de identidad, monitorización de transacciones y autenticación, unidos por una referencia de persona estable.

Usa un solo prompt para integrar **Verificación de identidad**, **Monitoreo de transacciones**, **Autenticación** o cualquier combinación de ellos. Selecciona los productos en el bloque de contexto de la aplicación; tu agente implementa solo esos flujos.> [!WARNING]
>
> Autenticación es nueva. Su flujo alojado, el emisor de JWT ES256, los eventos de autenticación y la protección contra bots están documentados; sus paquetes de SDK aún no están publicados. El prompt exige que el agente compruebe la disponibilidad y mantenga desactivados los adaptadores no compatibles. No debe inventar un SDK funcional ni afirmar que una integración no disponible ha superado las pruebas.## Usar el prompt1) Haz clic en **Copiar** y pega el prompt en tu agente de programación.
2) Indica tu stack, tu plataforma, los productos seleccionados y la configuración actual de tu aplicación.
3) Elige un identificador estable para la persona en `vendor_data`. Tu backend lo obtiene a partir del usuario autenticado.
4) Revisa la implementación generada y los resultados de sus pruebas antes de activarla para tus clientes.

Prompt de integración de Didit
        Copiar el prompt al portapapeles



      
      

\<pre>

\````# Integrate Didit into my application

Integrate the Didit products I select into my application. Identity verification checks people, Transaction monitoring evaluates transactions and application events, and Authentication signs end users into my app. Each product can be used alone. Keep their credentials and decisions separate while linking the same person through vendor_data.

\## My application context

\<my_stack>
Stack: [framework + language - e.g. Next.js 15 + TypeScript, Django, Rails, Go]
Surface: [web | iOS | Android | React Native | Flutter | backend-only]
Products: [Identity verification | Transaction monitoring | Authentication; choose any combination]
Existing Didit application: [configured environment, or needs setup]
Person reference: [stable server-owned identifier used as vendor_data]
Use case: [KYC at signup | KYB at business onboarding | age gate | re-auth | transaction monitoring | other]
Database: [Postgres | MySQL | Mongo | other]
\</my_stack>

\## Choose the products first

Build only the products selected in my application context. Use one stable, server-owned person reference across products. Do not add verification screens, transaction submissions or sign-in requirements for an unselected product.

\| Products | Sections to implement |
\| --- | --- |
\| Identity verification | Identity verification (steps 2-7) |
\| Transaction monitoring | Transaction monitoring |
\| Authentication | Authentication |
\| Identity verification + Transaction monitoring | Both selected sections |
\| Identity verification + Authentication | Both selected sections |
\| Transaction monitoring + Authentication | Both selected sections |
\| Identity verification + Transaction monitoring + Authentication | All three sections |

Authentication is new, and the @didit-protocol/auth* and @didit-protocol/signals packages are not published yet. Use the documented hosted flow and issuer for Authentication. First inspect the installed/published versions and the application's enabled capabilities. If a dependency is unavailable, keep that integration disabled, explain the missing capability, and complete the independently available products. Never invent an SDK export, issuer URL, webhook type, package version or working result. Do not call a release-dependent integration complete.

Use the same Didit application/environment for the selected products. Resolve vendor_data from the authenticated user on my server. Never trust a browser-supplied user identifier when creating a verification session or minting a scoped token. Sandbox and live identities are separate.

Authentication puts the person's vendor_data in the access token, the ID token and GET session/: the external_id supplied at import, otherwise the Didit identity ID. Read it from a verified token on my server and reuse it for verification sessions and transactions. Never link people solely by an unverified email or generate a new vendor_data for each product.

Credential boundaries:
\- x-api-key: server-side verification/monitoring API credential.
\- X-Transaction-Token: short-lived transaction credential scoped to vendor_data, returned as sdk_token.
\- X-Monitoring-Token: short-lived event credential scoped to vendor_data, returned as monitoring_token.
\- End-user Bearer access token: Authentication runtime session; not a console token or verification API key.
\- Publishable key: identifies an application for the upcoming Authentication SDK; never grants server authorization.

Reuse my existing Didit account and application. Step 1 is optional account provisioning, not end-user signup. Use https://docs.didit.me/integration/programmatic-registration for the current console account flow; do not change its confirmation-code contract. If I select only Authentication, obtain an enabled application and copy its sign-in origin from the console; do not create an unused verification workflow or API key integration.

\## Step 1 - Create a developer account (only if needed)

Two API calls. No browser. No 2FA. Returns an `api_key` you use for every subsequent verification API request.

\```bash
\# 1) Register - creates a pending account + emails a 6-character code (10 min TTL).
\#    Password rules: ≥8 chars, 1 uppercase, 1 lowercase, 1 digit, 1 special (!@#$%^&*()_+-=[]{}|;:,.<>?)
\#    IMPORTANT: substitute "you@yourdomain.com" with a real inbox. Reserved test domains
\#    (@example.com, @example.org, @*.test, @*.example, @*.invalid) are refused by mail delivery and the call returns 500.
curl -X POST https://apx.didit.me/auth/v2/programmatic/register/   -H "Content-Type: application/json"   -d '{"email": "you@yourdomain.com", "password": "StrongP@ss1"}'
\# 201 -> {"message": "Registration successful...", "email": "you@yourdomain.com"}

\# 2) Verify email with the code from the inbox - returns api_key, JWT tokens, org, app.
curl -X POST https://apx.didit.me/auth/v2/programmatic/verify-email/   -H "Content-Type: application/json"   -d '{"email": "you@yourdomain.com", "code": "A3K9F2"}'
\# 200 -> {
\#   "access_token": "...", "refresh_token": "...", "expires_in": 86400,
\#   "organization": { "uuid": "..." },
\#   "application":  { "uuid": "...", "client_id": "...", "api_key": "..." }
\# }
\# Persist response.application.api_key server-side; keep webhook secrets and other credentials separate.
\```

Persist the returned `api_key` as the env var `DIDIT_API_KEY`. Auth header for every verification API call:
\`x-api-key: $DIDIT_API_KEY`

If you lose the key, log in again with `POST /programmatic/login/` (same body as register) → `GET /organizations/me/` (gives org_id) → `GET /organizations/me/{org_id}/applications/` (lists apps; gives app_id) → `GET /organizations/me/{org_id}/applications/{app_id}/` and read `api_key` from the response. If your email verification code expired, request a fresh one with `POST /programmatic/resend-otp/` (email + password; 60s cooldown) or simply re-register (returns 200 with a new code).

\## Base URLs

\| Surface | Base URL |
\|---|---|
\| Verification API - every `/v3/...` endpoint (sessions, workflows, lists, billing, webhook destinations, entities, transactions, standalone APIs) | `https://verification.didit.me` |
\| Auth / account-management API (`/auth/v2/programmatic/...`, `/auth/v2/organizations/...`) | `https://apx.didit.me` |
\| Console (human UI) | `https://business.didit.me` |
\| Hosted verification URL handed to end users | `https://verify.didit.me` |
\| Docs (cross-check anything) | `https://docs.didit.me` |
\| OpenAPI spec (verification API) | `https://docs.didit.me/openapi-25.json` |
\| OpenAPI spec (auth + apps) | `https://docs.didit.me/openapi-auth.json` |

\## Identity verification (steps 2-7, only when selected)

If Identity verification is not selected, skip to the selected product section below.

\## Step 2 - Choose the verification approach

Pick exactly one, based on my use case:

\- **Approach A - Sessions API + SDK (recommended for any end-user verification flow).** My backend creates a Didit session → my frontend opens the Didit-hosted verification UI (via SDK / iframe / redirect) → user completes verification → my backend receives a webhook with the decision. Best for KYC at signup, KYB at business onboarding, age verification, re-auth. Didit-hosted flows are A/B-tested and convert higher than custom UIs.
\- **Approach B - Standalone APIs (server-to-server only).** My backend calls individual modules directly (`/v3/id-verification/`, `/v3/aml/`, `/v3/face-match/`, etc.) for batch jobs or fully custom UI. Best for back-office verification pipelines.

If unsure, default to Approach A.

\## Step 3 - Create or reuse a workflow

A workflow defines which modules run during a session and the per-feature thresholds. Create one via API (or `https://business.didit.me` → Workflows → Create).

\```bash
\# List existing workflows (newest first; paginated envelope {count, next, previous, results}, page size 50).
curl https://verification.didit.me/v3/workflows/   -H "x-api-key: $DIDIT_API_KEY"

\# Create a new KYC workflow.
\# - workflow_label and features are REQUIRED; the v3 API rejects any unknown
\#   body field with 400 (there is NO workflow_type field - see KYB note below).
\# - features[] entries are objects: { "feature": "<UPPERCASE_ENUM>", "config": { ... } }
\# - The config block is optional per feature; sensible defaults apply.
\# - Defaults to a published v1 workflow, ready for sessions. Not idempotent -
\#   each call creates a new workflow.
curl -X POST https://verification.didit.me/v3/workflows/   -H "x-api-key: $DIDIT_API_KEY"   -H "Content-Type: application/json"   -d '{
    "workflow_label": "Standard KYC",
    "features": [
      \{ "feature": "OCR" },
      \{ "feature": "LIVENESS", "config": { "face_liveness_method": "PASSIVE" } },
      \{ "feature": "FACE_MATCH" },
      \{ "feature": "IP_ANALYSIS" }
    ]
  }'
\# 201 -> the full workflow record. Save response.workflow_id (also exposed as
\# response.uuid) as DIDIT_WORKFLOW_ID - it's what you pass to POST /v3/session/.
\```

Feature enum values (UPPERCASE, source: `management-api/workflows/feature-configs.mdx`):
\- **KYC features:** `OCR`, `NFC`, `LIVENESS` (`face_liveness_method`: `PASSIVE` | `ACTIVE_3D` | `FLASHING`), `FACE_MATCH`, `AGE_ESTIMATION`, `PHONE_VERIFICATION`, `EMAIL_VERIFICATION`, `DATABASE_VALIDATION`, `AML`, `IP_ANALYSIS`, `PROOF_OF_ADDRESS`, `QUESTIONNAIRE`.
\- **KYB features:** `KYB_REGISTRY`, `KYB_DOCUMENTS`, `KYB_KEY_PEOPLE` (each plus `AML` / `DATABASE_VALIDATION` as needed).

There is **no** `workflow_type` request field - including any KYB feature (`KYB_REGISTRY`, `KYB_DOCUMENTS`, `KYB_KEY_PEOPLE`) automatically makes the workflow KYB; sending `workflow_type` (or any other unknown field) returns 400. Keep KYC and KYB as separate workflows.

\## Step 4 - Install the SDK that matches my stack

\| Stack | Package | Install |
\|---|---|---|
\| Web (React, Vue, Next.js, Nuxt, Svelte, vanilla JS) | `@didit-protocol/sdk-web` | `npm install @didit-protocol/sdk-web` |
\| iOS (Swift / SwiftUI) | `sdk-ios` | SPM: `https://github.com/didit-protocol/sdk-ios` |
\| Android (Kotlin / Jetpack Compose) | `me.didit:didit-sdk` | Custom Maven repository; see https://docs.didit.me/integration/native-sdks/android-sdk |
\| React Native (Expo or bare) | `@didit-protocol/sdk-react-native` | `npm install @didit-protocol/sdk-react-native` |
\| Flutter | `didit_sdk` | `flutter pub add didit_sdk` |
\| Backend-only / batch / custom UI | none - call REST directly | - |

Always prefer native iOS / Android SDKs over WebView on mobile (NFC + camera + biometrics work natively).

\## Step 5 - Create a session and present it to the user

\**Backend creates the session:**

\```bash
curl -X POST https://verification.didit.me/v3/session/   -H "x-api-key: $DIDIT_API_KEY"   -H "Content-Type: application/json"   -d '{
    "workflow_id": "'"$DIDIT_WORKFLOW_ID"'",
    "vendor_data": "internal-user-id",
    "callback": "https://myapp.com/done"
  }'
\```

Successful response (201 Created - always exactly these 10 keys):
\```json
\{
  "session_id": "11111111-2222-3333-4444-555555555555",
  "session_number": 43762,
  "session_token": "3FaJ9wLqX2Mz",
  "url": "https://verify.didit.me/session/3FaJ9wLqX2Mz",
  "vendor_data": "internal-user-id",
  "metadata": null,
  "status": "Not Started",
  "workflow_id": "11111111-2222-3333-4444-555555555555",
  "workflow_version": 3,
  "callback": "https://myapp.com/done"
}
\```

\- `session_token` is a **12-character URL-safe token** (not a JWT) embedded in `url`. Treat it as a secret - anyone holding it can open this session's verification flow.
\- After the user finishes, Didit redirects to `callback` with `?verificationSessionId={session_id}&status={status}` appended.
\- **Idempotent reuse:** if an unfinished session (`Not Started` / `In Progress` / `Resubmitted` / `Awaiting User`) with the same `vendor_data` already exists on the workflow's latest published version, that session is returned (still `201`) with its `callback` and `metadata` updated to the new values - retrying never creates duplicates. Finished sessions (`Approved`, `Declined`, `In Review`, `Expired`, `Abandoned`, `Kyc Expired`) are never reused.

\`vendor_data` is **my internal user ID** - Didit links the session to a User entity (auto-created if new). For KYB workflows it links to a Business entity.

\**Optional create-session fields:**

\| Field | Type | Notes |
\|---|---|---|
\| `callback_method` | `"initiator"` \\| `"completer"` \\| `"both"` | Which side of a cross-device flow receives the `callback` redirect. Default `"initiator"`; set `"both"` if the callback isn't triggering reliably. |
\| `metadata` | object | Arbitrary JSON; echoed back on every webhook for this session. |
\| `language` | ISO 639-1 string | Locks the hosted UI to that language (defaults to browser detection). When set, the returned `url` gains a language path segment: `https://verify.didit.me/{language}/session/{session_token}`. |
\| `contact_details` | `{ email, send_notification_emails, email_lang, phone }` | Lets Didit email the user the hosted-flow link and prefill email/phone verification steps. `phone` must be E.164. `phone` is ignored for KYB workflows. |
\| `expected_details` | KYC: `{ first_name, last_name, date_of_birth, gender, nationality, country, id_country, poa_country, address, identification_number, ip_address, expected_document_types }` - KYB: `{ company_name, registry_country, registration_number }` | KYC: cross-validates extracted document data against what your app already knows (mismatches surface as warnings). `expected_document_types` restricts which documents the user may present - codes: `P`, `ID`, `DL`, `RP`, `HIC`, `TC`, `SSC`. `address` requires `country` or `poa_country`, else 400. KYB: the business fields pre-fill the hosted company search (locked country, editable name and number fields) and the asserted search runs automatically, combining both identifiers when both are set - prefill only, no mismatch warnings. `registry_country` is `XX` or `XX-YY` for state-level registries (e.g. `US-CA`). Person-level fields are ignored for KYB. |
\| `portrait_image` | base64 string | Reference face (max 2MB; JPEG/PNG/WebP/TIFF). Required only for Biometric Authentication / Face-Match-first workflows; ignored otherwise. |
\| `sandbox_scenario` | string | Sandbox apps only (e.g. `"approve"`, `"decline_aml_hit"`) - auto-populates magic inputs to force that outcome. 400 on live apps. |

\**Auth failure shape.** On the session, workflow, and standalone-API endpoints, a missing, malformed, expired, or wrong-app `x-api-key` always returns **HTTP 403** (never 401) with body:
\```json
\{ "detail": "You do not have permission to perform this action." }
\```
There is no machine-readable discriminator - handle missing/expired/wrong-app cases uniformly by re-checking your env var or rotating the key in the console. (Exception: the management families - `/v3/users/`, `/v3/businesses/`, `/v3/transactions/`, `/v3/billing/`, `/v3/webhook/destinations/` - return **401** for a missing/malformed key and reserve **403** for a valid key lacking permission. Treat any 401/403 as "fix the key".)

\**Frontend presents the verification (pick one):**

\| Pattern | Code |
\|---|---|
\| Web JS SDK (modal) | `import { DiditSdk } from "@didit-protocol/sdk-web"; DiditSdk.shared.startVerification({ url })` |
\| Iframe (embedded) | `<iframe src={url} allow="camera; microphone; geolocation; clipboard-write; fullscreen; autoplay; encrypted-media; digital-credentials-get" />` |
\| Redirect (cross-device) | `window.location.href = url` |
\| iOS / Android / RN / Flutter | `DiditSdk.shared.startVerification(token: sessionToken)` (iOS; each SDK has the equivalent `startVerification` - token = the create-session `session_token`, never the API key) |

\## Step 6 - Set up the webhook to receive results

Build `POST /api/webhooks/didit` in my backend.

\**Register the webhook destination once:**

\```bash
curl -X POST https://verification.didit.me/v3/webhook/destinations/   -H "x-api-key: $DIDIT_API_KEY"   -H "Content-Type: application/json"   -d '{
    "label": "Production session webhooks",
    "url": "https://myapp.com/api/webhooks/didit",
    "webhook_version": "v3",
    "subscribed_events": ["status.updated", "data.updated"]
  }'
\```

Response includes `secret_shared_key` - save as `DIDIT_WEBHOOK_SECRET` immediately. Use `webhook_version: "v3"` for all new integrations (plural arrays). The `(application, url)` pair must be unique - POSTing the same URL twice returns 400.

\**Cloudflare / restrictive firewall users:** allowlist `18.203.201.92` (Didit's webhook egress IP, User-Agent `DiditWebhook/2.0 +https://didit.me`). HTTPS only, public hostnames only (no localhost / private CIDRs - SSRF guard).

\**Three signature headers** ship on every webhook. Verify **one** - `X-Signature-V2` is recommended because it survives JSON middleware re-encoding:

\| Header | What it signs | When to use |
\|---|---|---|
\| `X-Signature-V2` ★ recommended | `JSON.stringify(sortKeys(shortenFloats(parsed_body)))` with unescaped Unicode | Default - works through Express / Django / FastAPI / Next.js body parsers |
\| `X-Signature` | The raw request bytes verbatim (`sort_keys=True, ensure_ascii=True`) | Only if you can guarantee no middleware re-encodes the body |
\| `X-Signature-Simple` (deprecated) | `"{timestamp}:{session_id}:{status}:{webhook_type}"` | Last-resort fallback when nothing else works - does NOT authenticate the `decision` payload, so treat any decision data as untrusted and re-fetch it via `GET /v3/session/{id}/decision/` |

\**Endpoint requirements (in order):**

1\. Parse JSON (V2 is parser-tolerant; you can use any framework's default body parser).
2\. Read headers: `X-Signature-V2` (HMAC-SHA256 hex), `X-Timestamp` (Unix seconds).
3\. Reject if `abs(now - X-Timestamp) > 300` (replay protection).
4\. Apply `shortenFloats` (whole-number floats → integers), then `sortKeys` (recursive lexicographic), then `JSON.stringify` (unescaped Unicode - default in JS).
5\. Compute `expected = HMAC-SHA256(DIDIT_WEBHOOK_SECRET, canonical_json, "utf8")`.
6\. Compare with `X-Signature-V2` using constant-time comparison (`hmac.compare_digest` in Python, `crypto.timingSafeEqual` in Node).
7\. Dispatch on `webhook_type`. Return `2xx` within 5 seconds - Didit's delivery timeout is 5s, then it retries 5xx, 404, timeouts, and connection failures twice (~1 min, ~4 min). 3xx and other 4xx are never retried. Move heavy work to a queue.

\**Node.js / Next.js App Router example (canonical V2):**

\```ts
import crypto from "node:crypto";

// Server-side normalisation: convert whole-number floats to integers.
function shortenFloats(v: unknown): unknown {
  if (Array.isArray(v)) return v.map(shortenFloats);
  if (v && typeof v === "object") {
    return Object.fromEntries(
      Object.entries(v as Record<string, unknown>).map(([k, x]) => [k, shortenFloats(x)])
    );
  }
  if (typeof v === "number" && typeof v === "number" && v % 1 === 0) return Math.trunc(v);
  return v;
}

// Recursively sort object keys (arrays preserved in order).
function sortKeys(v: unknown): unknown {
  if (Array.isArray(v)) return v.map(sortKeys);
  if (v && typeof v === "object") {
    return Object.keys(v as object)
      .sort()
      .reduce<Record<string, unknown>>((acc, k) => {
        acc[k] = sortKeys((v as Record<string, unknown>)[k]);
        return acc;
      }, {});
  }
  return v;
}

export async function POST(req: Request) {
  const raw = await req.text();
  const sig = req.headers.get("x-signature-v2") ?? "";
  const ts = Number(req.headers.get("x-timestamp"));
  if (!ts || Math.abs(Date.now() / 1000 - ts) > 300)
    return new Response("stale", { status: 401 });

  const parsed = JSON.parse(raw);
  const canonical = JSON.stringify(sortKeys(shortenFloats(parsed)));
  const expected = crypto
    .createHmac("sha256", process.env.DIDIT_WEBHOOK_SECRET!)
    .update(canonical, "utf8")
    .digest("hex");
  if (
    sig.length !== expected.length ||
    !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))
  )
    return new Response("bad sig", { status: 401 });

  // Idempotency: dedupe on parsed.event_id - it is STABLE per event (the same
  // id is reused on retries and sent to every destination the event fans out to).
  if (await alreadyProcessed(parsed.event_id)) return new Response("ok");
  await markProcessed(parsed.event_id);

  // parsed.webhook_type → dispatch …
  return new Response("ok");
}
\```

\**Webhook event types** (use exact strings in `subscribed_events` - no wildcard; these nine are the complete set):
\`status.updated`, `data.updated`, `user.status.updated`, `user.data.updated`, `business.status.updated`, `business.data.updated`, `activity.created` (reserved - accepted in subscriptions and sent by Try Webhook tests, but not delivered for live traffic), `transaction.created`, `transaction.status.updated`.

\**Session webhook envelope (V3):**
\```json
\{
  "event_id": "uuid",
  "webhook_type": "status.updated",
  "timestamp": 1774970000,
  "created_at": 1774969994,
  "application_id": "uuid",
  "environment": "live",
  "session_id": "uuid",
  "status": "Approved",
  "workflow_id": "uuid",
  "workflow_version": 4,
  "vendor_data": "internal-user-id",
  "metadata": { "any": "json" },
  "decision": { /* present on Approved | Declined | In Review | Abandoned */ }
}
\```

\`environment` is `"live"` or `"sandbox"` - check it before mutating production state. `timestamp` is refreshed on every retry; `event_id` is not. KYB sessions additionally include `business_session_id`, `session_kind: "business"`, and (when supplied) `vendor_business_id`. `status: "Resubmitted"` carries `resubmit_info: { nodes_to_resubmit: [{ node_id, feature }], reasons: { "<node_id>": "why" } }` instead of `decision`.

The literal session status strings are exactly: `"Not Started"`, `"In Progress"`, `"Awaiting User"`, `"In Review"`, `"Approved"`, `"Declined"`, `"Resubmitted"`, `"Abandoned"`, `"Expired"`, `"Kyc Expired"` (mixed-case literal). Compare case-sensitively.

\**The `decision` object holds per-module plural arrays** (V3 schema). Every array item carries its workflow-graph `node_id` **and its own feature-level `status`** (`Not Finished`, `Approved`, `Declined`, `In Review`, `Resub Requested`; phone/email/ID steps can also be `Expired`). A block is `null` until that feature has run at least once. Key fields per array:
\- `id_verifications[]`: `first_name`, `last_name`, `document_type`, `document_subtype`, `document_number`, `personal_number`, `date_of_birth`, `age`, `gender`, `nationality`, `expiration_date`, `date_of_issue`, `issuing_state`, `address`, `parsed_address`, `mrz`, `portrait_image`, `front_image`, `back_image`, `front_image_quality_score`, `back_image_quality_score`, `warnings[]`
\- `nfc_verifications[]`: `status`, `is_nfc_skipped`, `skip_reason`, `portrait_image`, `signature_image`, `chip_data` (chip-read personal data: `document_number`, `document_type`, `birth_date`, `expiry_date`, `mrz_string`, …), `authenticity` (`{ sod_integrity, dg_integrity }` - both `true` means the chip is genuine), `certificate_summary`, `chip_protocol` (`icao` | `jpki`), and `jpki` (a Japan My Number Card read: `chain_valid`, `signature_valid`, `revocation_status`, `attributes`, `attributes_match_ocr`; the ICAO fields are then `null`). The decision also carries a top-level `nfc_skip_reason` (`USER_SKIPPED` | `DOCUMENT_WITHOUT_CHIP` | `CHIP_CERTIFICATE_UNAVAILABLE` | `DEVICE_WITHOUT_NFC` | `INTEGRATION_WITHOUT_NFC_ACCESS` | `MRZ_KEY_UNAVAILABLE` | `JPKI_UNAVAILABLE`, or `null`) stating why NFC did not run - including when the step was bypassed and `nfc_verifications` stays `null`.
\- `liveness_checks[]`: `status`, `score` (0–100), `method` (`PASSIVE` | `ACTIVE_3D` | `FLASHING` - same uppercase enum as the workflow config), `reference_image`, `video_url` (active modes only), `age_estimation`, `face_quality`, `face_luminance`
\- `face_matches[]`: `status`, `score` (0–100), `source_image`, `target_image`, `source_image_session_id`, `warnings[]`
\- `phone_verifications[]`: `status`, `full_number` (E.164; also split as `phone_number_prefix` + `phone_number`), `carrier` (`{ name, type }`), `is_disposable`, `is_virtual`, `verification_method`
\- `email_verifications[]`: `status`, `email`, `is_breached`, `is_disposable`, `is_undeliverable`, `breaches[]`
\- `poa_verifications[]`: `status`, `document_type`, `issuer`, `poa_address`, `poa_parsed_address`, `issue_date`, `expiration_date`
\- `aml_screenings[]`: `status`, `score` (0–100), `total_hits`, `entity_type` (`"person"` | `"company"`), `hits[]` (each: `match_score`, `risk_score`, `datasets[]`, `pep_matches`, `sanction_matches`, `warning_matches`, `adverse_media_matches`, `review_status`)
\- `ip_analyses[]`: `status`, `ip_address`, `ip_country`, `ip_country_code`, `ip_city`, `isp`, `is_vpn_or_tor`, `is_data_center`, `device_fingerprint`, `platform`, `time_zone`
\- `database_validations[]`: `status`, `issuing_state`, `validation_type` (`one_by_one` | `two_by_two` | `not_enabled`), `match_type` (`full_match` | `partial_match` | `no_match`), `screened_data`, `validations[]` (each: `service_id`, `service_name`, `outcome_code`, `source_data`)
\- `questionnaire_responses[]`: also a plural array (`null` until a questionnaire runs) - each item is a full submission: `questionnaire_id`, `title`, `status`, `sections[]` with per-item `answer`
\- `reviews[]`: manual-review audit trail - `user`, `new_status`, `comment`, `created_at`
\- KYB-only: `registry_checks[]`, `document_verifications[]`, `key_people_checks[]`

All media values (`*_image`, `video_url`, `document_file`, …) are **short-lived presigned URLs** - download what you need promptly and never persist the URLs themselves; re-fetch `GET /v3/session/{sessionId}/decision/` for fresh links. For the full per-feature field reference see `https://docs.didit.me/reference/data-models`.

V2 → V3 migration: V2 used singular keys (`aml`, `phone`, `email`, `poa`, `id_verification`, `nfc`, `liveness`, `face_match`). V3 renamed them to plural arrays so a single workflow can run the same module multiple times on different nodes - always index by `node_id`. Do **not** read the legacy singular fields; they only ship when a destination is explicitly pinned to `webhook_version: "v2"`.

\## Step 7 - Apply the decision to my database

\```ts
switch (event.status) {
  case "Approved":       user.verified = true; user.verifiedAt = new Date(); storeDecision(event.decision); break;
  case "Declined":       user.verificationStatus = "declined"; logWarnings(event.decision); break;
  case "In Review":      user.verificationStatus = "pending_review"; break;
  case "In Progress":    user.verificationStatus = "in_progress"; break;
  case "Awaiting User":  user.verificationStatus = "awaiting_user"; break; // KYB only - waiting for a UBO / officer KYC sub-session
  case "Resubmitted":    reopenNodes(event.resubmit_info.nodes_to_resubmit); break; // reviewer asked the user to retry specific steps
  case "Abandoned":      scheduleReminderEmail(user); break; // decision may still be present with partial data
  case "Expired":        break; // session URL aged out before the user finished
  case "Kyc Expired":    user.verified = false; createNewSession(user); break; // verified user's KYC has aged out per retention policy
  case "Not Started":    break;
}
\```

Idempotency:
\- **Dedupe on `event_id`** - it is stable per event: the same id is reused on every retry and sent to every destination the event fans out to. Fallback compound key if you don't store `event_id`: `session_id + status + webhook_type`. Never key on `timestamp` - it is refreshed on each retry.
\- Transaction webhooks (`transaction.created`, `transaction.status.updated`): same - dedupe on `event_id` (multiple events can target the same `transaction_id`).

Retry policy: on 5xx, 404, timeout, or connection failure Didit retries up to **2 times** with backoff (~1 min, then ~4 min after the first retry); 3xx and other 4xx are not retried. The outbound HTTP timeout is 5 seconds. After the second retry the delivery is dropped - replay from the **Deliveries** tab in the console if needed. Always return `2xx` immediately, even if processing is async.

After handling the webhook you can call `GET /v3/session/{session_id}/decision/` at any time - it returns the same decision (plus fresh presigned media URLs), and `session_kind` (`"user"` | `"business"`) tells you which shape you received.

\## Transaction monitoring (only when selected)

Use https://docs.didit.me/transaction-monitoring/sdk-transaction-submission and the real SDK exports. Device submissions are supported; transaction monitoring is not limited to server-to-server calls. No verification workflow is required merely to submit a transaction.

On my authenticated backend, mint a transaction token using the user's server-owned vendor_data:

\```bash
curl --fail-with-body https://verification.didit.me/v3/transactions/sdk-token/ \
  -H "x-api-key: $DIDIT_API_KEY" -H 'Content-Type: application/json' \
  -d '{"vendor_data":"example-user","ttl_seconds":900,"max_uses":3}'
\```

The response contains sdk_token and expires_at. Return only that scoped credential to the device. In my app, fetch it through my own authenticated endpoint; do not accept a different user's vendor_data from the client.

Web SDK adapter (token and transaction ID come from my application):

\```javascript
import { DiditSdk } from '@didit-protocol/sdk-web';

export async function screenPayment(transactionToken, txnId) {
  return DiditSdk.shared.submitTransaction({
    transactionToken,
    transaction: {
      txnId,
      category: 'finance',
      details: { direction: 'out', amount: '25.00', currency: 'USD', currencyType: 'fiat' },
      subject: {},
    },
  });
}
\```

Use my real transaction fields instead of the illustrative amount. The SDK returns transactionId, status and optional actionRequired. Transaction statuses use their own uppercase literals, not verification-session status strings. A pending verification_session action is not auto-launched: use its URL/session token with my verification integration, if selected and supported, or hold the transaction for a supported continuation. wallet_ownership actions can be auto-launched by the SDK. Completion is not payment authorization: my backend must retrieve the transaction or receive its verified webhook before acting.

For backend-only ingestion use POST https://verification.didit.me/v3/transactions/ with x-api-key and the documented wire schema at https://docs.didit.me/transaction-monitoring/transactions. The SDK's category/details properties are not the REST type/info properties. Keep a stable txnId for retries and reconcile an uncertain submission before resending; do not create a fresh financial transaction ID on every retry.

For events my application owns, enable event monitoring and mint an event token server-side:

\```bash
curl --fail-with-body https://verification.didit.me/v3/events/sdk-token/ \
  -H "x-api-key: $DIDIT_API_KEY" -H 'Content-Type: application/json' \
  -d '{"vendor_data":"example-user","ttl_seconds":900,"max_uses":10,"allowed_event_categories":["custom"]}'
\```

Read monitoring_token, not sdk_token, from that response. The device uses X-Monitoring-Token with POST /v1/events/ or /v1/events/batch/. The API-key server lane is POST /v3/events/ or /v3/events/batch/. All four are on https://verification.didit.me. A batch body is {"events":[...]}.

Example server event envelope (replace occurred_at with the actual action time and preserve event_id across retries):

\```json
\{
  "event_id": "example-action-1",
  "event_category": "custom",
  "action_type": "checkout_started",
  "occurred_at": "2026-10-12T00:00:00Z",
  "subject": { "vendor_data": "example-user" },
  "payload": {}
}
\```

The device lane binds the subject to its token. Handle 400 field errors, 401/403 credential or capability failures, 409 event-id conflicts and 429 throttling distinctly; respect Retry-After when supplied. Do not blindly retry invalid input. The dedicated @didit-protocol/signals package, submitEvent and flush are release-dependent: only emit SDK code once the installed exports and options are verified. Never substitute a transaction token for a monitoring token.

Do not duplicate Didit Authentication events with customer-submitted login events: Didit Authentication sends sign-ups, sign-ins, failed sign-ins, password resets, MFA changes and new devices itself. See https://docs.didit.me/authentication/events-and-webhooks.

\## Authentication (only when selected)

Read https://docs.didit.me/authentication/overview and the matching platform quickstart. Authentication signs end users into my app; it does not register the developer's Didit console account and it does not approve KYC or a payment.

Hosted flow: copy the application's sign-in origin from the console (https://<slug>.auth.didit.me, or <slug>.sandbox.auth.didit.me for sandbox; it is also the token issuer), register exact redirect URIs and origins, and enable the required methods (https://docs.didit.me/authentication/sign-in-methods). Open {origin}/sign-in with a random state, S256 code_challenge, code_challenge_method=S256 and redirect_uri. Keep the original verifier. The hosted flow handles configured signup, login, password reset and required MFA. On callback check state before exchanging code at token/ with grant_type=authorization_code, code, code_verifier and redirect_uri. Read access_token, id_token, refresh_token, token_type, expires_in and session_id. Never log tokens or callback codes.

GET session/ with Authorization: Bearer <end-user access token> validates the current session. DELETE session/ revokes it. POST token/ with grant_type=refresh_token and refresh_token rotates the token pair. Serialize refreshes: reuse of a consumed refresh token revokes the session. See https://docs.didit.me/authentication/tokens-and-jwks.

Unpublished SDK catalogue (not on any registry yet):
\- Web: @didit-protocol/auth, createDiditAuth({publishableKey}), signIn, handleRedirectCallback, getSession, getToken, onAuthStateChange, signOut and vendorData; /server is intended to provide verifyToken with cached JWKS.
\- React: @didit-protocol/auth-react, DiditAuth, useAuth, useUser, SignIn, SignUp, UserButton, SignedIn, SignedOut, ConnectedAgents.
\- Next.js: @didit-protocol/auth-nextjs, middleware, server auth(), callback handling and HttpOnly cookies.
\- iOS and Android: DiditAuth modules with system-browser PKCE, native passkeys and protected token storage.
\- React Native: @didit-protocol/auth-react-native. Flutter: didit_auth.

These names are release contracts, not permission to fabricate arguments or install versions. If a package is unavailable, leave its adapter behind a disabled feature flag or use the documented hosted flow for an enabled application. On native platforms confirm accepted redirect schemes before implementing a callback.

Each application is its own issuer: {origin}/.well-known/openid-configuration and {origin}/.well-known/jwks.json. Access tokens are ES256 JWTs (typ at+jwt, 5 minutes) with the claims iss, aud (application id), client_id, sub, sid, jti, iat, exp, vendor_data, amr, org, verified and act. Use the token's vendor_data as the person's vendor_data for verification sessions and transactions. Validate issuer, audience, algorithm, signature and time claims using trusted configuration. Never use a token-supplied issuer to choose an arbitrary JWKS URL. An ID token is not an API access token. GET session/ is required when instant revocation matters; offline JWT verification alone cannot prove the session is still active.

Use secure HttpOnly server-session cookies for server-rendered web apps with CSRF protection; keep browser tokens out of persistent plain storage where possible. Native refresh tokens belong in platform-protected storage. Logout must clear user-specific caches and report server-revocation failures.

Auth webhooks are auth.signup, auth.login, auth.login_failed, auth.password_reset_requested, auth.password_reset_completed, auth.mfa_changed, auth.new_device and auth.account_locked; deduplicate on auth_event.event_id. Bot protection policies (bots, stuffing, brute, disposable, vpn, travel; off/log/challenge/block) and attack mode are server-side: handle protection_required and attempt_blocked responses, never bypass them. User import is documented at /authentication/migration. Read /authentication/events-and-webhooks and /authentication/bot-protection on https://docs.didit.me.

\## Verify the selected combination

Test each selected product independently and then together with the same server-resolved vendor_data. Cover cross-user and cross-application token rejection, sign-in cancellation, callback state/replay, token expiry, refresh races, logout, verification decisions, pending transaction actions, webhook signature rejection and duplicate deliveries where applicable. Do not simulate unavailable SDKs and call the integration production-ready. Report which tests actually ran and which release or live checks remain unverified.

\## Verification module catalogue (only when selected)

\| Module | Endpoint | List price |
\|---|---|---|
\| Core KYC bundle (ID + Liveness + Face Match + IP) | session w/ workflow | **$0.33** |
\| ID Verification (standalone) | `POST /v3/id-verification/` | $0.20 |
\| Passive Liveness (standalone) | `POST /v3/passive-liveness/` | $0.05 |
\| Active Liveness (`ACTIVE_3D` / `FLASHING`) | session-only | $0.15 |
\| Face Match 1:1 | `POST /v3/face-match/` | $0.05 |
\| Face Search 1:N | `POST /v3/face-search/` | $0.05 |
\| Age Estimation | `POST /v3/age-estimation/` | $0.10 |
\| AML Screening | `POST /v3/aml/` | $0.20 |
\| Ongoing AML Monitoring | org-level continuous (per active user/yr) | $0.07 / user / year |
\| Database Validation (gov registries) | `POST /v3/database-validation/` | variable |
\| Proof of Address | `POST /v3/poa/` | $0.20 |
\| Email Verification | `POST /v3/email/send/` + `POST /v3/email/check/` | $0.02 |
\| Phone Verification (SMS / WhatsApp / voice / RCS / Telegram) | `POST /v3/phone/send/` + `POST /v3/phone/check/` | $0.02 + carrier |
\| NFC Verification (native SDKs only) | session module | $0.15 |
\| Biometric Authentication (re-auth) | session module | $0.10 |
\| Device & IP Analysis | session module | $0.03 |
\| Custom Questionnaire | session module | $0.10 |
\| Business Verification (KYB) | `POST /v3/session/` w/ KYB workflow | $2.00 |
\| Company AML Screening | KYB module | $0.20 |
\| Person AML (per UBO / officer) | KYB module | $0.20 |
\| Transaction Screening (rule engine) | `POST /v3/transactions/` | Free 1,000, Growth 10,000, Scale 100,000 a month included (Enterprise by contract), then $0.001 / tx at list price ($0.0009 on Scale) |
\| Wallet Verification & Screening (counterparty sanctions + wallet risk via Crystal / Merkle Science) | inside `/v3/transactions/` | $0.15 / tx |
\| Reusable KYC (share verified user across partners) | session feature | Free |
\| White Label (Scale and Enterprise) | workflow option | $0.10 per finished session |

Growth takes 4% and Scale 10% off every list price; Enterprise contracts set their own. Carrier and WhatsApp prices are passed on at cost, so only Didit's $0.02 fee per phone code, SMS segment or WhatsApp message is discounted. An SMS is billed per segment (160 GSM-7 characters, or 70 with accents or emoji), and the status emails Didit sends during a verification are free. Transactions beyond the monthly allowance cost the $0.001 list price less the plan discount ($0.0009 on Scale). See https://docs.didit.me/getting-started/pricing.

\## Operational APIs your code should know

\| Action | Endpoint |
\|---|---|
\| Read full decision JSON | `GET /v3/session/{sessionId}/decision/` |
\| List sessions (filter by status, workflow, vendor_data, date) | `GET /v3/sessions/` (trailing slash; the slash-less URL 301-redirects) |
\| Manually approve / decline / request review | `PATCH /v3/session/{sessionId}/update-status/` |
\| Patch metadata or extracted KYC/POA/NFC data | `PATCH /v3/session/{sessionId}/update-data/` |
\| Download compliance PDF | `GET /v3/session/{sessionId}/generate-pdf/` |
\| Delete a single session (GDPR / cleanup) | `DELETE /v3/session/{sessionId}/delete/` - returns 200 with `face_retention_outcome`; send `{"deletion_instruction": "privacy_erasure"}` for data-subject requests |
\| Batch delete sessions | `POST /v3/sessions/delete/`: up to 20 matching sessions return 200 with per-session `results[]`; more return 202 with a job to poll at `GET /v3/sessions/delete/{job_id}/` |
\| List / count / inspect / purge retained biometric templates (opt-in retention after session deletion) | `GET /v3/biometric-templates/`, `GET /v3/biometric-templates/count/`, `GET/DELETE /v3/biometric-templates/{template_uuid}/`, `POST /v3/biometric-templates/delete/` |
\| Mint a Reusable KYC share token | `POST /v3/session/{sessionId}/share/` |
\| Redeem a Reusable KYC share token | `POST /v3/session/import-shared/` |
\| Submit a transaction for monitoring | `POST /v3/transactions/` |
\| List / create lists (blocklist, allowlist, custom) | `GET/POST /v3/lists/` |
\| Add entry to a list | `POST /v3/lists/{list_uuid}/entries/` |
\| Upload a face image to a blocklist | `POST /v3/lists/{list_uuid}/entries/face-upload/` |
\| Check credit balance | `GET /v3/billing/balance/` |
\| Top up credits (returns Stripe checkout URL) | `POST /v3/billing/top-up/` |
\| List / get / update / delete vendor users | `GET /v3/users/`, `GET/PATCH /v3/users/{vendor_data}/`, `POST /v3/users/delete/` (deleting a user also purges its retained biometric templates) |
\| List / get / update / delete vendor businesses | `GET /v3/businesses/`, `GET/PATCH /v3/businesses/{vendor_data}/`, `POST /v3/businesses/delete/` |
\| List / create / update / delete webhook destinations | `GET/POST /v3/webhook/destinations/`, `GET/PATCH/DELETE /v3/webhook/destinations/{destination_uuid}/` |

Every `/v3/...` endpoint above is served from `https://verification.didit.me` and authenticates with `x-api-key`. Developer account registration uses `https://apx.didit.me`. End-user Authentication uses the application's own sign-in origin (https://<slug>.auth.didit.me), which is also its token issuer.

\## Identity verification checklist (only when selected)

\- [ ] `DIDIT_API_KEY`, `DIDIT_WORKFLOW_ID`, `DIDIT_WEBHOOK_SECRET` in env. Never committed.
\- [ ] Backend route that creates a session for a given user, returns `url` (the hosted verification URL on `verify.didit.me`) and the `session_id`.
\- [ ] Frontend that opens the session `url` via the right SDK / iframe / redirect for my stack.
\- [ ] Webhook endpoint with: `X-Timestamp` freshness check (≤ 300s), canonical-V2 JSON re-serialisation (`shortenFloats` → `sortKeys` → unescaped `JSON.stringify`) HMAC-SHA256, constant-time compare against `X-Signature-V2`, dispatch on `webhook_type`, return 2xx within 5 seconds.
\- [ ] DB schema + update logic for `status in ("Not Started", "In Progress", "Awaiting User", "In Review", "Approved", "Declined", "Resubmitted", "Abandoned", "Expired", "Kyc Expired")` - compare case-sensitively against the exact strings above.
\- [ ] Idempotent webhook handler (dedupe on `event_id`).
\- [ ] No API key shipped to the browser. The session creation is server-side only.
\- [ ] User-facing consent / disclosure shown BEFORE the session `url` opens (Didit handles biometric consent inside its flow but the legal layer outside is the integrator's responsibility).
\- [ ] (Optional) `/v3/session/{sessionId}/generate-pdf/` surfaced in compliance UI.
\- [ ] (Optional) Webhook destination created via `POST /v3/webhook/destinations/` with `webhook_version: "v3"` and `secret_shared_key` stored in a secrets manager.

\## When you need more detail

\- API root: `https://verification.didit.me/v3/`. Developer registration: `https://apx.didit.me/auth/v2/programmatic/`.
\- Sessions overview: `https://docs.didit.me/sessions-api/overview`
\- Create session: `https://docs.didit.me/sessions-api/create-session`
\- Retrieve session decision: `https://docs.didit.me/sessions-api/retrieve-session`
\- Standalone APIs index: `https://docs.didit.me/standalone-apis/id-verification`
\- Programmatic registration: `https://docs.didit.me/integration/programmatic-registration`
\- AI agent / MCP integration: `https://docs.didit.me/integration/mcp/overview`
\- Webhooks reference: `https://docs.didit.me/integration/webhooks`
\- Verification statuses (literal API strings): `https://docs.didit.me/integration/verification-statuses`
\- Data models (canonical V3 schema for every `decision.*` array): `https://docs.didit.me/reference/data-models`
\- SDKs overview: `https://docs.didit.me/integration/sdks`
\- Full OpenAPI 3 spec (verification API): `https://docs.didit.me/openapi-25.json`
\- Auth + apps OpenAPI: `https://docs.didit.me/openapi-auth.json`
\- Hosted MCP server config (drop into `.cursor/mcp.json` or your client's MCP config; Claude Code: `claude mcp add --transport http didit https://mcp.didit.me/mcp`; Claude Desktop: Settings → Connectors → Add custom connector with the same URL):

\```json
\{
  "mcpServers": {
    "didit": {
      "url": "https://mcp.didit.me/mcp"
    }
  }
}
\```

The hosted MCP server needs no API key: the client opens a browser, the user signs in with Didit (OAuth 2.1 with PKCE) and the server acts as that user. It exposes 169 tools spanning sessions, workflows, standalone verification APIs, transaction monitoring, lists, cases, reports, webhooks and billing - see `https://docs.didit.me/integration/mcp/tools` for the full tool catalogue.

Now build only the selected product integrations. Keep unavailable Authentication or signals adapters disabled and report them as incomplete. The verification checklist applies only if Identity verification is selected. If anything in `## My application context` is missing, ask once at the top of your reply, then implement the selected paths: credentials, server authorization, supported SDK calls, verified results and idempotent webhook handling where available. Use my stack's idioms (`fetch` / `axios` / `requests` / `okhttp`), keep the API key server-side only, treat the V3 plural-array contract (`id_verifications[]`, `nfc_verifications[]`, …) as load-bearing, and follow the verification status state machine above.````

\</pre>



      
      

Pegar en

## Elegir tu combinación de productos| Productos seleccionados | Resultado |
| - | - |
| Verificación de identidad | Creación de sesiones, el SDK de verificación, webhooks firmados y decisiones. |
| Monitoreo de transacciones | Transacciones, tokens de dispositivo con alcance limitado, eventos de la aplicación y acciones pendientes. |
| Autenticación | Inicio de sesión alojado, validación del callback, sesiones y cierre de sesión para una aplicación habilitada. |
| Verificación de identidad + Monitoreo de transacciones | Historial de verificaciones y de transacciones vinculado mediante `vendor_data`. |
| Verificación de identidad + Autenticación | Inicio de sesión y verificación en el momento en que lo exija tu política. |
| Monitoreo de transacciones + Autenticación | Credenciales de monitoreo vinculadas a la persona autenticada. |
| Los tres | Una única asignación de persona y decisiones independientes de inicio de sesión, verificación y transacciones. |El mismo prompt sirve para una aplicación fintech, de juegos, cripto o de marketplace. Describe en el bloque de contexto las acciones que quieres proteger. Un inicio de sesión exitoso no significa que se haya superado una verificación de identidad ni que un pago pueda procesarse.## Elegir el SDK según el producto> [tab] Verificación de identidad
>
> Usa `@didit-protocol/sdk-web` en la web, o los [SDK nativos y wrappers](https://docs.didit.me/es/integration/native-sdks/overview.md) en móvil. Tu servidor crea una sesión; tu aplicación recibe solo su URL o token con alcance limitado. Consulta la [guía del SDK de JavaScript](https://docs.didit.me/es/integration/web-sdks/javascript-sdk.md).> [tab] Monitoreo de transacciones
>
> Los SDK de verificación también admiten el [envío de transacciones desde el dispositivo](https://docs.didit.me/es/transaction-monitoring/sdk-transaction-submission.md) en las plataformas documentadas. Tu servidor genera un token de transacción. El paquete de eventos independiente `@didit-protocol/signals` depende de la versión publicada; no des por hecho que instalar el SDK de verificación actual proporciona `submitEvent`.> [tab] Autenticación
>
> Empieza por la [descripción general de Autenticación](https://docs.didit.me/es/authentication/overview.md) y la guía de inicio rápido de tu plataforma. `@didit-protocol/auth`, sus paquetes para React y Next.js, los módulos nativos y los wrappers aún no están publicados, así que usa el flujo alojado. Los tokens son JWT ES256 que verificas con el [JWKS](https://docs.didit.me/es/authentication/tokens-and-jwks.md) de tu aplicación, y contienen el `vendor_data` de la persona.## Proteger credenciales y decisionesMantén las claves de API y las credenciales de gestión de la consola en tu backend. Las aplicaciones de navegador y nativas reciben solo credenciales de sesión limitadas al producto o tokens de corta duración. Una clave publicable de Autenticación no es una clave de API.Verifica las firmas de los webhooks antes de aplicar actualizaciones, elimina las entregas duplicadas por `event_id` y mantén separados los estados de cada producto. No trates una redirección, un callback del SDK ni una pantalla de éxito mostrada como una autorización del servidor. Informa explícitamente de cualquier dependencia no disponible y de cualquier flujo en producción no verificado.## Guías relacionadas- [Guías de inicio rápido de autenticación](https://docs.didit.me/es/authentication/overview.md)
- [Registro programático de desarrolladores](https://docs.didit.me/es/integration/programmatic-registration.md)
- [Crear una sesión de verificación](https://docs.didit.me/es/sessions-api/create-session.md)
- [Envío de transacciones](https://docs.didit.me/es/transaction-monitoring/sdk-transaction-submission.md)
- [Webhooks](https://docs.didit.me/es/integration/webhooks.md)
- [Modelos de datos](https://docs.didit.me/es/reference/data-models.md)


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