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

# Flujo completo por API: guía de integración KYC

> Integración completa con la API REST de Didit: obtén credenciales, crea una sesión, comparte el enlace, gestiona webhooks y obtén decisiones KYC.

En esta guía se explica el flujo completo para integrar la verificación de identidad de Didit mediante la API REST: obtener las credenciales, crear una sesión, presentarla al usuario y recibir los resultados.

***

## Paso 1: Obtén tus credenciales

Tienes dos formas de obtener una `api_key`:

### Opción A: Registro programático (sin navegador)

Es la mejor opción para CI/CD y agentes de IA. Dos llamadas a la API, `POST /programmatic/register/` y después `POST /programmatic/verify-email/`, devuelven la `api_key` directamente en la respuesta.

→ Consulta [Registro programático](https://docs.didit.me/es/integration/programmatic-registration.md) para ver el flujo completo, las reglas de contraseñas, la duración del JWT y la política de bloqueo.

### Opción B: Desde la Didit Console

1. Ve a la [Didit Console](https://business.didit.me).
2. Crea o selecciona tu organización.
3. Ve a **Settings** → **API & Webhooks**.
4. Copia:
   * **API Key**: se usa como encabezado `x-api-key` en cada llamada de verificación.
   * **Webhook Secret Key**: se usa para verificar las firmas HMAC de los webhooks.

### Variables de entorno

```bash theme={null}
# Required
DIDIT_API_KEY=your_api_key_here
DIDIT_WEBHOOK_SECRET=your_webhook_secret_here

# Your workflow ID (from the Console)
DIDIT_WORKFLOW_ID=your_workflow_id_here
```

***

## Paso 2: Crea un flujo de verificación (workflow)

Antes de crear sesiones, necesitas un flujo de verificación. Los flujos definen los pasos de verificación que siguen los usuarios.

### Créalo en la Console

1. Ve a la [Didit Console](https://business.didit.me) → **Workflows**
2. Haz clic en **Create Workflow**
3. Elige una plantilla base:

| Plantilla | Ideal para |
| - | - |
| **KYC** | Verificación de identidad completa con documento de identidad |
| **Adaptive Age Verification** | Verificación de edad con documento de identidad opcional como respaldo |
| **Biometric Authentication** | Reverificación de usuarios recurrentes |
| **Address Verification** | Verificación de comprobante de domicilio |

4. Añade funciones opcionales:

| Función | Descripción |
| - | - |
| Verificación NFC | Lee los datos del chip del pasaporte o documento de identidad |
| Prueba de vida | Evita la suplantación con un video selfie |
| Coincidencia facial | Compara la selfie con la foto del documento |
| Verificación de teléfono | Verifica el número de teléfono por SMS |
| Verificación de correo electrónico | Verifica la dirección de correo electrónico |
| Análisis AML | Comprueba en listas de sanciones y de PEP |
| Validación de base de datos | Verifica contra bases de datos gubernamentales |
| Análisis de dispositivo e IP | Detecta VPN, proxies y geolocalización |

5. Copia tu **Workflow ID**

> [!NOTE]
>
> Consulta la [documentación de flujos](https://docs.didit.me/es/console/workflows.md) para ver
> las opciones de configuración en detalle.

***

## Paso 3: Crea una sesión de verificación

Llama a la API para crear una sesión para tu usuario.

### Solicitud

```bash theme={null}
curl -X POST https://verification.didit.me/v3/session/ \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "workflow_id": "your-workflow-id",
    "callback": "https://yourapp.com/verification-complete",
    "vendor_data": "user-123"
  }'
```

### Parámetros de la solicitud

| Parámetro | Obligatorio | Descripción |
| - | - | - |
| `workflow_id` | Sí | El UUID de tu flujo, que encontrarás en la consola. |
| `callback` | No | URL a la que se redirige al usuario tras la verificación. Didit añade `verificationSessionId` y `status` como parámetros de consulta. Admite esquemas personalizados (p. ej., `myapp://`). Si se omite, se usa la URL de callback configurada en el flujo. |
| `callback_method` | No | Qué dispositivo gestiona la redirección: `initiator`, `completer` o `both`. Valor predeterminado: `initiator`. |
| `vendor_data` | No | Tu identificador interno del usuario; se devuelve en los webhooks para que puedas correlacionarlos. |
| `metadata` | No | Objeto JSON arbitrario que se guarda con la sesión y se devuelve en los webhooks. |
| `language` | No | Idioma sugerido para la interfaz (ISO 639-1, p. ej., `en`, `es`). Si se omite, se detecta automáticamente el idioma del navegador. |
| `contact_details` | No | Correo electrónico o teléfono del usuario para el autocompletado y las notificaciones por correo electrónico. |
| `expected_details` | No | Datos esperados del usuario para contrastarlos con los datos verificados. |
| `portrait_image` | Condicional | Imagen facial codificada en Base64 (máx. 2MB; JPEG, PNG, WebP o TIFF). Rostro de referencia para la Autenticación biométrica o para flujos que empiezan por la Coincidencia facial. Es opcional cuando el usuario de `vendor_data` ya tiene un rostro almacenado (rostro de una prueba de vida aprobada, foto de un pasaporte electrónico, retrato de un documento de identidad o rostro de perfil registrado): Didit lo reutiliza automáticamente; se devuelve `400` si no hay ninguno disponible. |
| `sandbox_scenario` | No | Identificador de escenario, solo para sandbox (p. ej., `approve`, `decline_aml_hit`), que rellena automáticamente la sesión con datos de prueba preconfigurados. |

### Ejemplo completo con todas las opciones

```bash theme={null}
curl -X POST https://verification.didit.me/v3/session/ \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "workflow_id": "11111111-2222-3333-4444-555555555555",
    "callback": "https://yourapp.com/verification-complete",
    "vendor_data": "user-123",
    "metadata": {
      "plan": "premium",
      "signup_source": "mobile-app"
    },
    "contact_details": {
      "email": "alex.sample@example.com",
      "email_lang": "en",
      "send_notification_emails": true,
      "phone": "+15550101000"
    },
    "expected_details": {
      "first_name": "John",
      "last_name": "Doe",
      "date_of_birth": "1990-01-01",
      "expected_document_types": ["P", "ID"]
    }
  }'
```

> [!NOTE]
>
> `expected_details.expected_document_types` restringe el paso de verificación
> de identidad a tipos de documento concretos: la pantalla de selección de
> documento solo muestra los tipos solicitados. Valores permitidos: `P` (pasaporte), `ID` (documento nacional de identidad), `DL`
> (licencia de conducir), `RP` (permiso de residencia), `HIC` (tarjeta de seguro médico),
> `TC` (tarjeta fiscal), `SSC` (tarjeta de seguridad social). Los valores no distinguen entre mayúsculas y minúsculas
> y se eliminan los duplicados; los valores desconocidos devuelven `400`. Consulta la
> [referencia de Create Session](https://docs.didit.me/es/sessions-api/create-session.md) para ver el esquema completo.

### Respuesta

`201 Created`:

```json theme={null}
{
  "session_id": "11111111-2222-3333-4444-555555555555",
  "session_number": 43762,
  "session_token": "3FaJ9wLqX2Mz",
  "url": "https://verify.didit.me/session/3FaJ9wLqX2Mz",
  "vendor_data": "user-123",
  "metadata": {
    "plan": "premium",
    "signup_source": "mobile-app"
  },
  "status": "Not Started",
  "workflow_id": "11111111-2222-3333-4444-555555555555",
  "workflow_version": 3,
  "callback": "https://yourapp.com/verification-complete"
}
```

### Campos clave de la respuesta

| Campo | Descripción |
| - | - |
| `session_id` | UUID único de esta sesión. Úsalo para `GET /v3/session/{sessionId}/decision/` y para los webhooks. |
| `session_number` | Número de secuencia legible que se muestra en la consola de Didit. |
| `session_token` | Token de 12 caracteres compatible con URL que autoriza al usuario final a abrir el flujo alojado e inicializa los SDK nativos (el SDK web usa `url` en su lugar). Trátalo como un secreto. |
| `url` | URL de verificación alojada a la que redirigir al usuario o que puedes insertar en un iframe. (El nombre del campo es `url`, no `verification_url`). Cuando la solicitud incluye `language`, la URL contiene un segmento de ruta con el idioma; p. ej., `https://verify.didit.me/en/session/3FaJ9wLqX2Mz`. |
| `status` | Estado actual: `"Not Started"` para una sesión recién creada. |
| `workflow_id` | Identificador estable del flujo en el que se ejecuta la sesión. |
| `workflow_version` | Versión publicada del flujo a la que se fijó la sesión al crearse. |

> [!NOTE]
>
> **Idempotencia.** Cuando se proporciona `vendor_data` y ya existe una sesión sin terminar
> (`Not Started`, `In Progress`, `Resubmitted` o `Awaiting User`) con el
> mismo `vendor_data` en la última versión publicada del flujo,
> se devuelve esa sesión existente (igualmente con `201`) en lugar de crear un
> duplicado, con su `callback` y su `metadata` actualizados a los nuevos valores, y
> con un `status` que refleja el de la sesión existente.

> [!NOTE]
>
> **Si falta `x-api-key`, se devuelve `403`** con
> `{"detail": "You do not have permission to perform this action."}`, el
> mismo cuerpo que con una clave válida sin permiso. Una clave incorrecta, revocada o expirada
> devuelve `401`. Si los créditos son insuficientes, se devuelve
> `400`. Consulta la [referencia de la API de Create Session](https://docs.didit.me/es/sessions-api/create-session.md) para ver
> la documentación completa.

***

## Paso 4: Presenta la verificación al usuario

Elige cómo presentar el flujo de verificación a tu usuario:

### Opción A: SDK nativo (recomendado para móvil)

Usa el `session_token` con los SDK nativos para integrar la cámara, NFC y la biometría.

→ [SDK de iOS](https://docs.didit.me/es/integration/native-sdks/ios-sdk.md) · [SDK de Android](https://docs.didit.me/es/integration/native-sdks/android-sdk.md) · [React Native](https://docs.didit.me/es/integration/native-sdks/react-native-sdk.md) · [Flutter](https://docs.didit.me/es/integration/native-sdks/flutter-sdk.md)

### Opción B: iframe InContext (recomendado para web)

Inserta la verificación en tu página con la `url` devuelta en el paso 3.

```html theme={null}
<iframe
  src="{url}"
  style="width: 100%; height: 700px; border: none;"
  allow="camera; microphone; geolocation; clipboard-write; fullscreen; autoplay; encrypted-media; digital-credentials-get"
></iframe>
```

→ [iframe InContext](https://docs.didit.me/es/integration/web-sdks/incontext-iframe.md)

### Opción C: Redirección

Redirige al usuario a la URL de verificación.

```javascript theme={null}
window.location.href = session.url;
```

→ [Redirección web](https://docs.didit.me/es/integration/web-sdks/web-redirect.md)

### Opción D: WebView móvil

Para aplicaciones móviles sin SDK nativo.

```jsx theme={null}
// React Native
<WebView source={{ uri: session.url }} />
```

→ [WebView en iOS/Android](https://docs.didit.me/es/integration/web-sdks/webview-in-ios-android.md)

***

## Paso 5: Recibe los resultados

### Mediante webhook (recomendado)

Los webhooks notifican a tu servidor en tiempo real cuando cambia el estado de una verificación.

**Configuración:**

1. Crea un destino de webhook en la [Console](https://business.didit.me) (**API y Webhooks**) o mediante [`POST /v3/webhook/destinations/`](https://docs.didit.me/es/management-api/webhook/create-destination) con `webhook_version: "v3"` y un array `subscribed_events` (por ejemplo, `["status.updated"]`)
2. Guarda el `secret_shared_key` que se devuelve al crearlo: es el secreto HMAC de todas las variantes de firma
3. Implementa un endpoint de webhook en tu servidor

**Ejemplo de payload de webhook:**

Dentro de `decision`, cada resultado por función se entrega como un **array en plural** (`id_verifications[]`, `nfc_verifications[]`, `liveness_checks[]`, `face_matches[]`, …), con una entrada por cada nodo del flujo. Este es el contrato de V3 y es intocable: nunca programes contra una clave en singular como `nfc` / `id_verification` / `liveness`.

```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",
  "session_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "status": "Approved",
  "workflow_id": "22222222-3333-4444-5555-666666666666",
  "workflow_version": 4,
  "vendor_data": "user-123",
  "metadata": { "plan": "premium" },
  "decision": {
    "session_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "status": "Approved",
    "id_verifications": [
      { "node_id": "id_verification_1", "status": "Approved", "document_type": "Passport", "warnings": [] }
    ],
    "liveness_checks": [
      { "node_id": "liveness_1", "status": "Approved", "method": "PASSIVE", "score": 95.4, "warnings": [] }
    ],
    "face_matches": [
      { "node_id": "face_match_1", "status": "Approved", "score": 96.1, "warnings": [] }
    ],
    "reviews": []
  }
}
```

El objeto `decision` solo está presente cuando `status` es `Approved`, `Declined`, `In Review` o `Abandoned` (un evento `Resubmitted` incluye `resubmit_info` en su lugar). Didit reutiliza el mismo `event_id` en los reintentos y en el envío a varios destinos, así que basa tu idempotencia en él. Consulta [Modelos de datos](https://docs.didit.me/es/reference/data-models.md) para ver cada array en plural y cada campo.

**Verifica la firma (X-Signature-V2, recomendado):**

`X-Signature-V2` es un HMAC-SHA256 calculado sobre una recodificación canónica del cuerpo JSON: los números de punto flotante con valor entero se reducen a enteros, las claves se ordenan de forma recursiva, se usan separadores compactos y se conserva Unicode.

```javascript theme={null}
const crypto = require("crypto");

// Match Didit's float normalisation: whole-valued floats serialise 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)])
    );
  }
  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 verifyWebhook(rawBody, signatureV2, timestamp, secret) {
  // 1. Reject anything older than 5 minutes
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) return false;

  // 2. Recompute HMAC-SHA256 against the canonical JSON Didit signs
  const canonical = JSON.stringify(sortKeys(shortenFloats(JSON.parse(rawBody))));
  const expected = crypto.createHmac("sha256", secret).update(canonical, "utf8").digest("hex");

  // 3. Constant-time compare
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signatureV2, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

→ [Documentación completa de webhooks](https://docs.didit.me/es/integration/webhooks.md), que incluye la firma `X-Signature` sobre los bytes sin procesar, la firma obsoleta `X-Signature-Simple`, que solo firma el envoltorio, y cómo elegir entre ellas. Didit reintenta las entregas fallidas (`5xx`, `404`, tiempo de espera agotado) hasta 2 veces (a los \~1 min y luego a los \~4 min) y el tiempo de espera es de **5 segundos**, así que devuelve un `2xx` rápidamente.

### Mediante la API (opcional)

Obtén el mismo payload de decisión en cualquier momento:

```bash theme={null}
curl https://verification.didit.me/v3/session/{session_id}/decision/ \
  -H "x-api-key: YOUR_API_KEY"
```

La estructura de la respuesta (arrays en plural) es idéntica a la del objeto `decision` del payload del webhook, y el endpoint se puede llamar en cualquier momento del ciclo de vida de la sesión. Usa webhooks para las actualizaciones en tiempo real y reserva las llamadas a la API para completar datos faltantes, auditorías y reconstrucciones: el endpoint `/decision/` tiene un límite de 600 solicitudes GET por minuto por clave de API (consulta [Límites de solicitudes](https://docs.didit.me/es/integration/rate-limiting.md)). Semántica de errores: un `session_id` desconocido devuelve `404` con `{"detail": "Not found."}`; los fallos de autenticación se devuelven como `403`, y este endpoint nunca devuelve `401`. Las URL de medios de la respuesta son enlaces prefirmados de corta duración: descárgalos cuanto antes en lugar de almacenarlos.

→ [Referencia de la API para obtener la sesión](https://docs.didit.me/es/sessions-api/retrieve-session.md)

***

## Ejemplos de código completos

### Node.js / Express

```javascript theme={null}
const express = require("express");
const crypto = require("crypto");
const app = express();

// Create verification session
app.post("/api/verify", async (req, res) => {
  const { userId } = req.body;

  const response = await fetch("https://verification.didit.me/v3/session/", {
    method: "POST",
    headers: {
      "x-api-key": process.env.DIDIT_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      workflow_id: process.env.DIDIT_WORKFLOW_ID,
      callback: `${process.env.APP_URL}/verification-complete`,
      vendor_data: userId,
    }),
  });

  const session = await response.json();

  // Store session_id associated with user
  await db.users.update({
    where: { id: userId },
    data: { verificationSessionId: session.session_id },
  });

  res.json({
    verificationUrl: session.url,             // hosted verification URL
    sessionToken: session.session_token,      // for native/web SDKs
  });
});

// Handle webhook — uses verifyWebhook / shortenFloats / sortKeys from Step 5
app.post(
  "/api/webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.get("X-Signature-V2");
    const timestamp = req.get("X-Timestamp");

    if (
      !verifyWebhook(
        req.body.toString("utf8"),
        signature,
        timestamp,
        process.env.DIDIT_WEBHOOK_SECRET,
      )
    ) {
      return res.status(401).json({ error: "Invalid signature" });
    }

    const payload = JSON.parse(req.body);
    const { event_id, session_id, status, vendor_data } = payload;

    // Idempotency: the same event_id is reused on retries and fan-out.
    // Skip events you have already processed (e.g. a processed_events table).

    // Update user's verification status (status values are case-sensitive strings)
    if (status === "Approved") {
      db.users.update({
        where: { id: vendor_data },
        data: { isVerified: true, verifiedAt: new Date() },
      });
    }

    // Return 2xx quickly (Didit's delivery timeout is 5s); do heavy work asynchronously
    res.json({ received: true });
  },
);

app.listen(3000);
```

### Python / FastAPI

```python theme={null}
from fastapi import FastAPI, Request, HTTPException
import httpx
import hashlib
import hmac
import json
import os
import time

app = FastAPI()

@app.post("/api/verify")
async def create_verification(user_id: str):
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://verification.didit.me/v3/session/",
            headers={
                "x-api-key": os.environ["DIDIT_API_KEY"],
                "Content-Type": "application/json"
            },
            json={
                "workflow_id": os.environ["DIDIT_WORKFLOW_ID"],
                "callback": f"{os.environ['APP_URL']}/verification-complete",
                "vendor_data": user_id
            }
        )

    session = response.json()
    return {
        "verification_url": session["url"],            # hosted verification URL
        "session_token": session["session_token"],     # for native/web SDKs
    }

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

@app.post("/api/webhook")
async def handle_webhook(request: Request):
    payload = await request.json()
    signature = request.headers.get("X-Signature-V2")
    timestamp = request.headers.get("X-Timestamp")

    # Verify timestamp freshness
    if not signature or not timestamp or abs(int(time.time()) - int(timestamp)) > 300:
        raise HTTPException(status_code=401, detail="Request too old")

    # Recompute Didit's V2 canonical JSON: shortened floats, sorted keys,
    # compact separators, Unicode preserved (ensure_ascii=False)
    canonical = json.dumps(
        shorten_floats(payload),
        sort_keys=True,
        separators=(",", ":"),
        ensure_ascii=False,
    )
    expected = hmac.new(
        os.environ["DIDIT_WEBHOOK_SECRET"].encode("utf-8"),
        canonical.encode("utf-8"),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        raise HTTPException(status_code=401, detail="Invalid signature")

    # Idempotency: dedupe on payload["event_id"] (reused on retries)

    if payload["status"] == "Approved":
        # Update user in database
        pass

    return {"received": True}
```

***

## Estados de la sesión

Estos son los 10 valores posibles de `status`, como cadenas exactas que distinguen mayúsculas de minúsculas (ten en cuenta que `Kyc Expired` lleva una sola `K` mayúscula):

| Estado | Descripción |
| - | - |
| `Not Started` | La sesión se ha creado, pero el usuario aún no ha abierto el enlace. |
| `In Progress` | El usuario está completando la verificación. |
| `In Review` | La verificación se ha marcado para revisión humana. |
| `Approved` | Todas las comprobaciones se han superado. |
| `Declined` | Una o más comprobaciones han fallado. |
| `Resubmitted` | Un revisor ha solicitado que se repitan pasos concretos. |
| `Awaiting User` | Sesión KYB principal a la espera de que terminen las partes KYC secundarias. |
| `Abandoned` | El usuario empezó, pero no terminó dentro del plazo establecido. |
| `Expired` | La sesión expiró antes de que el usuario abriera el enlace. |
| `Kyc Expired` | Sesión aprobada anteriormente cuyo documento ha superado su fecha de caducidad. |

Consulta [Estados de verificación](https://docs.didit.me/es/integration/verification-statuses.md) para ver el ciclo de vida completo, las transiciones y el diagrama Mermaid.

***

## Próximos pasos

1. **Elige tu método de integración**:
   * Aplicaciones móviles → [SDK nativos](https://docs.didit.me/es/integration/native-sdks/overview.md)
   * Aplicaciones web → [SDK web](https://docs.didit.me/es/integration/web-sdks/overview.md)
2. **Configura los webhooks**: [Webhooks](https://docs.didit.me/es/integration/webhooks.md)
3. **Planifica la capacidad**: [Límites de solicitudes](https://docs.didit.me/es/integration/rate-limiting.md)
4. **Gestiona todos los estados**: [Estados de verificación](https://docs.didit.me/es/integration/verification-statuses.md)
5. **Personaliza tu flujo de trabajo**: [Flujos de trabajo](https://docs.didit.me/es/console/workflows.md)
6. **Comprueba que la API está accesible**: [Healthcheck](https://docs.didit.me/es/api-reference/healthcheck.md)

## Necesitas ayuda

* **Soporte**: [support@didit.me](mailto:support@didit.me)
* **Onboarding programático**: [Registro programático](https://docs.didit.me/es/integration/programmatic-registration.md)


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