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

# Envío de transacciones con SDK - KYT del lado del cliente

> Didit: envía transacciones desde tu app con un token acotado, inteligencia de dispositivo, sujeto antisuplantación y propiedad de wallet.

Los SDK de Didit pueden enviar [transacciones monitorizadas](https://docs.didit.me/es/transaction-monitoring/transactions.md) directamente desde el dispositivo del usuario final, incluidas las transferencias sujetas a la [Travel Rule](https://docs.didit.me/es/transaction-monitoring/travel-rule.md) y el análisis de criptoactivos.
Tu backend emite un token de corta duración y alcance limitado, y el SDK se encarga del resto.
Adjunta la inteligencia del dispositivo automáticamente y, cuando una transferencia sujeta a la Travel Rule requiere una prueba de control de la wallet, inicia automáticamente ese flujo en el momento.
Una sesión de verificación requerida nunca se inicia automáticamente: se devuelve en la respuesta para que tu aplicación la inicie con tu propia integración de verificación de Didit. Consulta [Acciones requeridas del usuario](#acciones-requeridas-del-usuario) más abajo.

## Visión general

El envío desde el cliente se basa en tres garantías:

* **Vinculación del sujeto a prueba de suplantación.** Al emitirse, el token queda limitado a un único `vendor_data` (el ID de tu usuario). La identidad del sujeto de cada transacción enviada con él se garantiza en el servidor a partir de esa vinculación, de modo que un cliente manipulado no puede enviar transacciones en nombre de otro usuario, y el payload nunca necesita incluir credenciales.
* **Inteligencia del dispositivo automática.** El SDK recopila una huella digital del dispositivo y la envía junto con la transacción, y Didit obtiene la dirección IP y el user agent en el servidor. El contexto combinado del dispositivo se adjunta al sujeto de la transacción sin bibliotecas adicionales, sin un token de dispositivo aparte y sin ninguna integración extra por tu parte. Permite aplicar [reglas](https://docs.didit.me/es/transaction-monitoring/rules.md) basadas en el dispositivo y correlacionar sesiones y transacciones relacionadas por dispositivo.
* **Prueba de propiedad de la wallet iniciada automáticamente.** Cuando la transacción creada requiere que el usuario final demuestre el control de una wallet, la respuesta incluye un bloque `action_required` de tipo `wallet_ownership` y el SDK abre ese flujo de inmediato y, cuando termina, actualiza la transacción. No tienes que crear bucles de sondeo ni gestionar la entrega de enlaces. Una acción `verification_session` requerida es diferente: nunca se inicia automáticamente y se devuelve en el mismo bloque para que tu aplicación la inicie con su propia integración de verificación de Didit.

> [!NOTE]
>
> El endpoint [`POST /v3/transactions/`](https://docs.didit.me/es/transaction-monitoring/transactions) en el servidor sigue siendo la opción adecuada cuando no hay un usuario final presente: transferencias de back office, importaciones por lotes o eventos notificados por tus propios sistemas.
> Ambas vías crean los mismos registros de transacciones, ejecutan las mismas reglas y emiten los mismos [webhooks](https://docs.didit.me/es/transaction-monitoring/webhooks.md).

## Flujo

**1. Genera un token de transacción**

Tu backend llama a `POST /v3/transactions/sdk-token/` con tu clave de API y el `vendor_data` del usuario, y entrega el `sdk_token` devuelto a tu aplicación.

**2. Envía desde el dispositivo**

La aplicación llama a `submitTransaction` en el SDK, que envía el payload a `POST /v1/transactions/` con el encabezado `X-Transaction-Token` y el enriquecimiento automático con datos del dispositivo.

**3. Gestiona cualquier acción requerida**

Si `action_required.type` es `wallet_ownership`, el SDK inicia automáticamente el widget y después actualiza la transacción mediante `GET /v1/transactions/{transaction_id}/`.
Si es `verification_session`, el SDK no inicia nada: lee `action_required` e inicia tu propio flujo de verificación con su `session_token` (o con `url` en la web).

## Emitir un token de transacción

Emite los tokens desde tu backend con `POST /v3/transactions/sdk-token/`, que usa la misma autenticación con clave de API que el resto de los endpoints de transacciones `/v3/`.

```bash theme={null}
curl -X POST https://verification.didit.me/v3/transactions/sdk-token/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "vendor_data": "user-042",
    "ttl_seconds": 900,
    "max_uses": 3
  }'
```

```json theme={null}
{
  "sdk_token": "q7Zt...urlsafe",
  "expires_at": "2026-07-07T12:15:00Z"
}
```

| Campo | Tipo | Descripción |
| - | - | - |
| `vendor_data` | string | **Obligatorio.** Tu identificador interno del usuario final al que está vinculado este token. La identidad del sujeto de cada transacción enviada con el token queda determinada por este valor. |
| `ttl_seconds` | integer | Vigencia del token en segundos. El valor predeterminado es `900` (15 minutos) y el máximo es `86400` (24 horas). |
| `max_uses` | integer | Opcional. Número máximo de envíos exitosos permitidos con este token. Omítelo para permitir usos ilimitados dentro del TTL. |

También se aceptan alias en camelCase (`vendorData`, `ttlSeconds`, `maxUses`), como en el resto de la API `/v3/`.

> [!WARNING]
>
> **Nunca incluyas tu clave de API en una aplicación.**
> La clave de API permanece en tu backend y emite los tokens; solo el `sdk_token` de corta duración llega al dispositivo.
> Emite un token nuevo por cada sesión de usuario en lugar de reutilizar tokens de larga duración, y ajusta `max_uses` al mínimo cuando conozcas el número de envíos previstos.

## Envío desde el dispositivo

Los SDK envían una solicitud a `POST /v1/transactions/` con el token en el encabezado `X-Transaction-Token`.
El payload que viaja por la red usa camelCase. Los métodos `submitTransaction` lo construyen por ti, así que solo tienes que manejarlo directamente si llamas al endpoint desde tu propio cliente HTTP:

```bash theme={null}
curl -X POST https://verification.didit.me/v1/transactions/ \
  -H "X-Transaction-Token: SDK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "txnId": "wd-2026-07-07-0042",
    "type": "travelRule",
    "info": {
      "direction": "out",
      "amount": "0.25",
      "currency": "ETH",
      "currencyType": "crypto",
      "cryptoParams": { "crypto_chain": "ETH" }
    },
    "subject": { "fullName": "Jane Doe" },
    "counterparty": {
      "fullName": "Ana Diaz",
      "paymentMethod": { "type": "unhosted_wallet", "accountId": "0xBeneficiaryWallet01" }
    },
    "travelRule": {
      "status": "AWAITING_COUNTERPARTY",
      "beneficiaryData": { "wallet_address": "0xBeneficiaryWallet01", "name": "Ana Diaz" }
    },
    "includeCryptoScreening": true
  }'
```

Cada campo de nivel superior corresponde a su equivalente en [`POST /v3/transactions/`](https://docs.didit.me/es/transaction-monitoring/transactions):

| Campo en la red | Equivalente en `/v3/` | Notas |
| - | - | - |
| `txnId` | `transaction_id` | **Obligatorio.** Tu identificador único de la transacción. |
| `txnDate` | `transaction_at` | Marca de tiempo ISO-8601. Si no lo envías, se usa el momento actual. |
| `zoneId` | `time_zone` | Identificador de zona horaria IANA, por ejemplo `Europe/Madrid`. |
| `type` | `transaction_category` | **Obligatorio.** `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet` y el resto de categorías. |
| `info` | `transaction_details` | **Obligatorio.** `direction`, `amount`, `currency` (obligatorio), además de `currencyType`, `amountInDefaultCurrency`, `defaultCurrencyCode`, `paymentDetails`, `paymentTxnId`, `type` (tipo de acción) y `cryptoParams`. |
| `subject`, `counterparty` | `subject`, `counterparty` | Participantes: `type` (tipo de entidad), `externalUserId`, `fullName`, `firstName`, `lastName`, `dob`, `address`, `institutionInfo`, `device`, `paymentMethod` (`type`, `accountId`, `issuingCountry`). Si el `paymentMethod.type` de la contraparte es `unhosted_wallet`, el destino se declara como wallet sin custodia. Consulta [más abajo](#declarar-una-wallet-sin-custodia). |
| `props` | `custom_properties` | Pares clave-valor personalizados, que puedes usar en las reglas como `custom_values.<key>`. |
| `travelRule` | `travel_rule_details` | `status`, `protocol`, `required`, `obligationsCount`, `originatorData`, `beneficiaryData`, `metadata`. `status` es obligatorio siempre que envíes este bloque; si falta, la solicitud falla con `400`. Cuando Travel Rule está activado, Didit ignora cualquier `status` que envíes y gestiona el intercambio por su cuenta. |
| `includeCryptoScreening` | `include_crypto_screening` | Anula, para una transacción concreta, la configuración del análisis cripto. |

La respuesta es el mismo detalle de transacción en snake\_case que devuelven los endpoints `/v3/`, más el bloque `action_required`:

```json theme={null}
{
  "uuid": "22222222-3333-4444-5555-666666666666",
  "txn_id": "wd-2026-07-07-0042",
  "transaction_type": "travelRule",
  "direction": "OUTBOUND",
  "status": "APPROVED",
  "amount": "0.25",
  "currency": "ETH",
  "travel_rule": {
    "uuid": "33333333-4444-5555-6666-777777777777",
    "status": "UNCONFIRMED_OWNERSHIP",
    "required": true
  },
  "action_required": {
    "type": "wallet_ownership",
    "url": "https://verify.didit.me/wallet-ownership/3s9x...urlsafe",
    "widget_session_id": "77777777-8888-9999-aaaa-bbbbbbbbbbbb",
    "expires_at": "2026-07-07T13:00:00Z"
  }
}
```

### Declarar una wallet sin custodia

El ejemplo anterior declara el destino como la wallet sin custodia del propio usuario: el `paymentMethod.type` de la contraparte es `unhosted_wallet`.
Esa declaración cambia la forma en que se ejecuta el [intercambio de Travel Rule](https://docs.didit.me/es/transaction-monitoring/travel-rule.md#wallets-sin-custodia-declaradas):

* Didit omite por completo el enrutamiento hacia el VASP de la contraparte (detrás de una wallet sin custodia no hay ningún VASP) y crea la transferencia directamente en `UNCONFIRMED_OWNERSHIP`.
* Cuando `auto_wallet_verification` está activado en tu [configuración de Travel Rule](https://docs.didit.me/es/transaction-monitoring/travel-rule.md#configuración) (opción predeterminada), se genera automáticamente una sesión del [widget de propiedad de wallet](https://docs.didit.me/es/transaction-monitoring/wallet-ownership-widget.md) y se devuelve en `action_required`, exactamente como en la respuesta anterior.
* El SDK abre el widget automáticamente. Cuando el usuario demuestra que controla la wallet, la transferencia ejecuta la comprobación de nombre y pasa a `COMPLETED`, o a `COUNTERPARTY_MISMATCHED_DATA` si el nombre no coincide con el titular de la wallet en la libreta de direcciones.

Sin la declaración, Didit prueba en orden las distintas [vías](https://docs.didit.me/es/transaction-monitoring/travel-rule.md#descripción-general) y solo recurre al flujo de prueba de propiedad si no encuentra ningún VASP de la contraparte.
Declara la wallet siempre que tu producto ya sepa que el destino de la retirada es la wallet del propio usuario: así te ahorras el intento de enrutamiento fallido y el usuario entra de inmediato en el flujo de prueba.

### Control del sujeto

Hay tres cosas que siempre decide el token y nunca el payload:

* La aplicación en la que se crea la transacción.
* La identidad del sujeto: el `subject.externalUserId` del payload se ignora y se sustituye por el `vendor_data` del token.
* El contexto de dispositivo del sujeto (consulta más abajo). Si hay conflicto con cualquier dato enviado en `subject.device`, prevalecen los campos obtenidos en el servidor.

Un `401` de este endpoint significa que el token se rechazó: falta, es desconocido, se revocó, superó su `max_uses` o expiró.
Las respuestas de token expirado siempre contienen la palabra `expired` en el detalle del error, y así es como los SDK distinguen los errores `expired_token` de los `invalid_token`.

### Consultar la transacción

`GET /v1/transactions/{transaction_id}/` devuelve el mismo detalle con la misma autenticación `X-Transaction-Token`.
Los SDK lo llaman automáticamente cuando termina un flujo de acción abierto o se cierra su ventana, de modo que el estado final nunca depende de un único evento o notificación.
Úsalo tú mismo para volver a comprobar `status` y `action_required` en cualquier momento mientras el token sea válido. Desde tu backend, [`GET /v3/transactions/{transaction_id}/`](https://docs.didit.me/es/transaction-monitoring/transactions) devuelve el mismo registro.

## Inteligencia de dispositivo automática

Cada envío incluye inteligencia de dispositivo sin que tengas que hacer ningún trabajo de integración:

* El SDK recopila una huella del dispositivo que respeta la privacidad (el esquema `didit-fp-v2`: señales de la plataforma y del navegador, un identificador de dispositivo persistente, una puntuación de bot y un hash compuesto) y la envía con la solicitud.
* Didit obtiene en el servidor la dirección IP, el user agent y el accept-language a partir de la propia conexión, por lo que no se pueden falsificar desde el payload.

El resultado combinado se normaliza y se incorpora al contexto de dispositivo del participante sujeto y se inserta o actualiza en el grafo de dispositivos, el mismo que alimentan las sesiones de KYC.
Esto permite correlacionar transacciones por dispositivo en toda tu base de usuarios ("relacionadas por dispositivo"), aporta señales de dispositivo y de red a las [reglas](https://docs.didit.me/es/transaction-monitoring/rules.md) y al [enriquecimiento de IP](https://docs.didit.me/es/transaction-monitoring/ip-enrichment.md), y se muestra en el detalle de la transacción en la [Console](https://docs.didit.me/es/transaction-monitoring/console.md).

Las transacciones `/v3/` enviadas desde el servidor pueden seguir incluyendo objetos `device_context` explícitos por participante. Con el SDK obtienes gratis una versión más sólida de los mismos datos.

## Acciones requeridas del usuario

Algunas transacciones necesitan que el usuario final haga algo antes de poder liquidarse: una regla puede exigir una sesión de verificación, o una transferencia sujeta a la Travel Rule puede exigir una prueba de que la wallet de destino pertenece realmente al usuario.
Ambos casos aparecen en el mismo lugar: el bloque `action_required` de la respuesta del envío (y de cada lectura posterior de la transacción):

```json theme={null}
"action_required": {
  "type": "verification_session",
  "url": "https://verify.didit.me/session/...",
  "session_id": "11111111-2222-3333-4444-555555555555",
  "session_token": "SESSION_TOKEN",
  "status": "Not Started"
}
```

```json theme={null}
"action_required": {
  "type": "wallet_ownership",
  "url": "https://verify.didit.me/wallet-ownership/3s9x...urlsafe",
  "widget_session_id": "77777777-8888-9999-aaaa-bbbbbbbbbbbb",
  "expires_at": "2026-07-07T13:00:00Z"
}
```

| Tipo | Cuándo aparece | Qué hace el usuario |
| - | - | - |
| `verification_session` | Una [acción de regla](https://docs.didit.me/es/transaction-monitoring/rules.md) exigió una verificación adicional del usuario y la transacción pasó a `AWAITING_USER`. | Completa una sesión de verificación alojada (por ejemplo, una nueva comprobación biométrica) que lanza tu aplicación; consulta [Lanzar una sesión de verificación](#lanzar-una-sesión-de-verificación) más abajo. |
| `wallet_ownership` | Una transferencia sujeta a la Travel Rule necesita que el usuario final demuestre el control de la wallet: el destino se [declaró como wallet sin custodia](#declarar-una-wallet-sin-custodia) (`UNCONFIRMED_OWNERSHIP`), o una transferencia saliente no encontró ningún VASP de contraparte, y `auto_wallet_verification` está activado en tu [configuración de la Travel Rule](https://docs.didit.me/es/transaction-monitoring/travel-rule.md#configuración). Didit genera automáticamente una sesión del [widget de propiedad de wallet](https://docs.didit.me/es/transaction-monitoring/wallet-ownership-widget.md) vinculada a la transacción. | Demuestra el control de la wallet mediante la firma de un mensaje (en una cadena compatible), una captura de pantalla o una autodeclaración, según lo permita tu configuración de la Travel Rule, pero nunca mediante una prueba de Satoshi en una sesión generada automáticamente. El SDK la lanza automáticamente por ti. |

Si ambas pudieran aplicarse a la misma transacción, `wallet_ownership` tiene prioridad, porque la falta de la prueba de propiedad bloquea el propio intercambio de la Travel Rule.
`action_required` pasa a ser `null` en cuanto se resuelve la acción pendiente.
El campo `remediation` existente se mantiene por compatibilidad con versiones anteriores; `action_required` es el bloque canónico.

### Contrato de lanzamiento automático

`autoLaunchAction` (por defecto `true`) solo lanza automáticamente la acción `wallet_ownership`:

* **Web**: se abre en un modal superpuesto del propio SDK, y la finalización se detecta mediante mensajería con validación de origen más una consulta periódica acotada de la transacción (nunca depende de un único evento).
* **iOS / Android** (y los wrappers de React Native y Flutter, que delegan en los SDK nativos): se presenta como la **experiencia nativa de propiedad de wallet** del SDK (SwiftUI / Jetpack Compose) cuando `useNativeWalletOwnership` es `true` (el valor por defecto), y recurre, si no es posible, a `SFSafariViewController` / una Chrome Custom Tab y, por último, al navegador predeterminado. Cuando el flujo se cierra o la aplicación vuelve al primer plano, el SDK vuelve a leer la transacción automáticamente.

Una acción `verification_session` nunca se lanza automáticamente: siempre vuelve en `action_required` para que tu aplicación la inicie con su propia integración de verificación de Didit.

> [!WARNING]
>
> El widget de propiedad de wallet nunca debe mostrarse dentro de un webview móvil integrado: los deep links de las wallets no pueden salir de él y el flujo de prueba queda sin salida.
> Los SDK ya usan la superficie de navegador correcta al lanzarlo automáticamente; respeta esa regla si gestionas tú mismo la `url`.
> Consulta la [guía del widget de propiedad de wallet](https://docs.didit.me/es/transaction-monitoring/wallet-ownership-widget.md#abre-la-url-devuelta) para más detalles.

Cuando el flujo de propiedad de wallet lanzado automáticamente por el SDK se completa o se cierra, el SDK vuelve a leer la transacción y entrega el resultado actualizado a tu callback `onActionCompleted`.
Una acción `verification_session` no forma parte de ese ciclo, ya que la lanza tu aplicación; consulta la siguiente sección.

### Lanzar una sesión de verificación

Cuando `action_required.type` es `verification_session`, lee el bloque de `result.actionRequired` y pásalo a tu integración de verificación de Didit existente:

```typescript Web theme={null}
if (result.actionRequired?.type === "verification_session") {
  DiditSdk.shared.startVerification({ url: result.actionRequired.url });
}
```

```swift iOS theme={null}
if actionRequired.type == "verification_session" {
    try await DiditSdk.shared.startVerification(token: actionRequired.sessionToken)
}
```

```kotlin Android theme={null}
if (actionRequired.type == "verification_session") {
    DiditSdk.startVerification(token = actionRequired.sessionToken) { result ->
        // handle the verification result
    }
}
```

```typescript React Native theme={null}
if (actionRequired.type === "verification_session") {
  await startVerification(actionRequired.sessionToken);
}
```

```dart Flutter theme={null}
if (actionRequired.type == 'verification_session') {
  await DiditSdk.startVerification(actionRequired.sessionToken);
}
```

Cuando termine tu flujo de verificación, llama a `GET /v1/transactions/{transaction_id}/` (o espera al [webhook de transacciones](#webhooks)) para obtener el estado actualizado de la transacción.

### Ejemplo: biometría reforzada para un retiro de alto valor

Este es el flujo en su mejor versión: condicionar las transacciones de riesgo (importes elevados, beneficiarios nuevos, dispositivos nuevos) a una nueva comprobación biométrica de la misma persona que verificaste en el onboarding.

Una toma de control de cuenta se ve exactamente así: contraseña correcta, código 2FA correcto (obtenido por phishing o mediante SIM swapping) y un retiro a una wallet que nunca has visto.
Para entonces, todas las defensas basadas en credenciales ya han fallado.
Un rostro no: el atacante no puede superar una selfie con prueba de vida que debe coincidir con el rostro capturado durante el onboarding KYC del titular legítimo.

**1. Crea la regla una sola vez, en la consola**

**Transactions → Rules → Create Rule**: condición `amount gte 5000`, acción **Change status → `AWAITING_USER`**, y selecciona tu flujo de **Biometric Authentication** en la acción.
Añade cualquier otra condición de riesgo que quieras (contraparte nueva, nueva huella del dispositivo, cambio de país de la IP) como condiciones o reglas adicionales.

**2. Envía el retiro como de costumbre**

No cambia nada en tu código de envío. Cuando la regla se dispara, la respuesta incluye `status: "AWAITING_USER"` y un bloque `action_required` de tipo `verification_session`:

```json theme={null}
{
  "status": "AWAITING_USER",
  "action_required": {
    "type": "verification_session",
    "url": "https://verify.didit.me/session/...",
    "session_id": "11111111-2222-3333-4444-555555555555",
    "session_token": "SESSION_TOKEN",
    "status": "Not Started"
  }
}
```

**3. Lanza la nueva comprobación**

Pasa el `session_token` (o la `url`) a tu integración de verificación de Didit existente, exactamente como en [Lanzar una sesión de verificación](#lanzar-una-sesión-de-verificación).
Como la sesión se crea para el mismo `vendor_data` que el sujeto de la transacción, Didit ya tiene el rostro de referencia verificado del usuario desde el onboarding: la selfie con prueba de vida aprobada, la foto del chip del pasaporte o el retrato del documento.
El usuario se hace una sola selfie en vivo; Didit ejecuta la prueba de vida y una coincidencia facial 1:1 con esa referencia almacenada. Sin volver a escanear el documento ni repetir el onboarding: unos segundos de fricción, solo en las transacciones que lo merecen.

**4. Liquida según el resultado**

Cuando la sesión se completa, la transacción se reevalúa automáticamente: `APPROVED` si la nueva comprobación fue exitosa, `DECLINED` si falló, con un webhook `transaction.status.updated` en ambos casos (`reason: remediation_completed` en el historial de la transacción).
Tu aplicación obtiene el resultado mediante callbacks del tipo `onActionCompleted`, `GET /v1/transactions/{transaction_id}/` o el webhook.

Esto funciona porque el onboarding KYC, la reverificación biométrica y la decisión sobre la transacción se ejecutan en una sola plataforma: el rostro capturado en el onboarding es la referencia para la verificación adicional, el grafo de dispositivos vincula la sesión con la transacción y el motor de reglas lo une todo, sin integraciones entre proveedores, sin exportar imágenes faciales y sin un contrato de biometría aparte.
La reverificación se factura como una función estándar de Autenticación biométrica ([\$0.10](https://docs.didit.me/es/getting-started/pricing.md)) solo cuando realmente se ejecuta.

## SDK web

`submitTransaction` ya está disponible en el singleton del SDK web:

```typescript theme={null}
import { DiditSdk } from "@didit-protocol/sdk-web";

const result = await DiditSdk.shared.submitTransaction({
  transactionToken: sdkToken, // minted by your backend via POST /v3/transactions/sdk-token/
  transaction: {
    txnId: "wd-2026-07-07-0042",
    txnDate: new Date().toISOString(),
    zoneId: "Europe/Madrid",
    category: "travelRule",
    details: {
      direction: "out",
      amount: "0.25",
      currency: "ETH",
      currencyType: "crypto",
      paymentDetails: "ETH withdrawal to self-hosted wallet",
      cryptoParams: { crypto_chain: "ETH" }
    },
    subject: {
      type: "individual",
      fullName: "Jane Doe"
      // no externalUserId needed: the subject identity comes from the token
    },
    counterparty: {
      type: "individual",
      fullName: "Ana Diaz",
      paymentMethod: {
        type: "unhosted_wallet",
        accountId: "0xBeneficiaryWallet01"
      }
    },
    travelRule: {
      status: "AWAITING_COUNTERPARTY", // ignored when Travel Rule is enabled: Didit drives the exchange
      beneficiaryData: {
        wallet_address: "0xBeneficiaryWallet01",
        name: "Ana Diaz"
      }
    },
    includeCryptoScreening: true
  },
  autoLaunchAction: true, // default: auto-launches wallet_ownership only; verification_session comes back in result.actionRequired for you to launch
  onActionCompleted: (refreshed) => {
    console.log(refreshed.status, refreshed.travelRuleStatus);
  }
});

console.log(result.transactionId, result.status, result.actionRequired?.type);
```

El resultado es `{ transactionId, status, travelRuleStatus?, actionRequired? }`.
En el payload del SDK escribes `category`, `details` y `customProperties`; el SDK los asigna por ti a los alias de transmisión (`type`, `info`, `props`).

Los errores están tipados mediante `DiditTransactionError`:

```typescript theme={null}
import { DiditSdk, DiditTransactionError } from "@didit-protocol/sdk-web";

try {
  await DiditSdk.shared.submitTransaction({ transactionToken, transaction });
} catch (error) {
  if (error instanceof DiditTransactionError) {
    switch (error.type) {
      case "expired_token": // mint a fresh token and retry
        break;
      case "invalid_token": // rejected: unknown, revoked, or over max_uses
        break;
      case "validation": // inspect error.fieldErrors for per-field details
        break;
      case "network": // retry with backoff
        break;
    }
  }
}
```

## SDK nativos

> [!NOTE]
>
> La API nativa `submitTransaction` se incluye en los SDK de iOS y Android 4.1.0 (y en las versiones de React Native y Flutter que fijan esta versión).
> A diferencia del SDK web, los payloads nativos usan directamente los nombres de campo que se envían por la red: `type`, `info` y `props`.

```swift Swift theme={null}
import DiditSDK

let payload = DiditTransactionPayload(
    txnId: "wd-2026-07-07-0042",
    type: "travelRule",
    info: DiditTransactionInfo(
        direction: "out",
        amount: 0.25,
        currency: "ETH",
        currencyType: "crypto",
        cryptoParams: ["crypto_chain": "ETH"]
    ),
    subject: DiditTransactionParticipant(type: "individual", fullName: "Jane Doe"),
    counterparty: DiditTransactionParticipant(
        type: "individual",
        fullName: "Ana Diaz",
        paymentMethod: DiditTransactionPaymentMethod(type: "unhosted_wallet", accountId: "0xBeneficiaryWallet01")
    ),
    travelRule: DiditTravelRuleInfo(
        status: "AWAITING_COUNTERPARTY", // required by the API; ignored when Travel Rule is enabled
        beneficiaryData: ["wallet_address": "0xBeneficiaryWallet01", "name": "Ana Diaz"]
    ),
    includeCryptoScreening: true
)

let result = try await DiditSdk.shared.submitTransaction(
    transactionToken: sdkToken,
    transaction: payload,
    options: DiditTransactionOptions(
        autoLaunchAction: true,           // default
        useNativeWalletOwnership: true,   // default: native flow, browser fallback
        onTransactionUpdated: { refreshed in
            print(refreshed.status ?? "", refreshed.travelRuleStatus ?? "")
        }
    )
)

print(result.transactionId, result.status ?? "", result.actionRequired?.type ?? "none")
// Errors: DiditTransactionError .invalidToken / .expiredToken / .validation(fieldErrors) / .network
```

```kotlin Kotlin theme={null}
import me.didit.sdk.DiditSdk
import me.didit.sdk.transactions.*

val payload = DiditTransactionPayload(
    txnId = "wd-2026-07-07-0042",
    type = "travelRule",
    info = DiditTransactionInfo(
        direction = "out",
        amount = 0.25,
        currency = "ETH",
        currencyType = "crypto",
        cryptoParams = mapOf("crypto_chain" to "ETH")
    ),
    subject = DiditTransactionParticipant(type = "individual", fullName = "Jane Doe"),
    counterparty = DiditTransactionParticipant(
        type = "individual",
        fullName = "Ana Diaz",
        paymentMethod = DiditTransactionPaymentMethod(
            type = "unhosted_wallet",
            accountId = "0xBeneficiaryWallet01"
        )
    ),
    travelRule = DiditTravelRule(
        status = "AWAITING_COUNTERPARTY", // required by the API; ignored when Travel Rule is enabled
        beneficiaryData = mapOf("wallet_address" to "0xBeneficiaryWallet01", "name" to "Ana Diaz")
    ),
    includeCryptoScreening = true
)

// suspend fun - call from a coroutine; DiditSdk.initialize() must have run first
val result = DiditSdk.submitTransaction(
    transactionToken = sdkToken,
    transaction = payload,
    options = DiditTransactionOptions(
        autoLaunchAction = true,          // default
        useNativeWalletOwnership = true,  // default: native flow, Custom Tab fallback
        onTransactionUpdated = { refreshed ->
            Log.d("Didit", "${refreshed.status} ${refreshed.travelRuleStatus}")
        }
    )
)

Log.d("Didit", "${result.transactionId} ${result.status} ${result.actionRequired?.type}")
// Errors: sealed DiditTransactionException - InvalidToken / ExpiredToken / Validation(fieldErrors) / Network
```

```typescript React Native theme={null}
import {
  submitTransaction,
  getTransaction,
  DiditTransactionError,
} from "@didit-protocol/sdk-react-native";

const result = await submitTransaction(sdkToken, {
  txnId: "wd-2026-07-07-0042",
  type: "travelRule",
  info: {
    direction: "out",
    amount: 0.25,
    currency: "ETH",
    currencyType: "crypto",
    cryptoParams: { crypto_chain: "ETH" },
  },
  subject: { type: "individual", fullName: "Jane Doe" },
  counterparty: {
    type: "individual",
    fullName: "Ana Diaz",
    paymentMethod: { type: "unhosted_wallet", accountId: "0xBeneficiaryWallet01" },
  },
  travelRule: {
    status: "AWAITING_COUNTERPARTY", // required by the API; ignored when Travel Rule is enabled
    beneficiaryData: { wallet_address: "0xBeneficiaryWallet01", name: "Ana Diaz" },
  },
  includeCryptoScreening: true,
}, {
  autoLaunchAction: true, // default
  onTransactionUpdated: (refreshed) => {
    console.log(refreshed.status, refreshed.travelRuleStatus);
  },
});

console.log(result.transactionId, result.status, result.actionRequired?.type);
// Errors: DiditTransactionError with code invalid_token | expired_token | validation | network
```

```dart Flutter theme={null}
import 'package:didit_sdk/sdk_flutter.dart';

final result = await DiditSdk.submitTransaction(
  sdkToken,
  DiditTransactionPayload(
    txnId: 'wd-2026-07-07-0042',
    type: 'travelRule',
    info: DiditTransactionInfo(
      direction: 'out',
      amount: 0.25,
      currency: 'ETH',
      currencyType: 'crypto',
      cryptoParams: {'crypto_chain': 'ETH'},
    ),
    subject: DiditTransactionParticipant(type: 'individual', fullName: 'Jane Doe'),
    counterparty: DiditTransactionParticipant(
      type: 'individual',
      fullName: 'Ana Diaz',
      paymentMethod: DiditTransactionPaymentMethod(
        type: 'unhosted_wallet',
        accountId: '0xBeneficiaryWallet01',
      ),
    ),
    travelRule: DiditTravelRule(
      status: 'AWAITING_COUNTERPARTY', // required by the API; ignored when Travel Rule is enabled
      beneficiaryData: {'wallet_address': '0xBeneficiaryWallet01', 'name': 'Ana Diaz'},
    ),
    includeCryptoScreening: true,
  ),
  options: DiditTransactionOptions(
    autoLaunchAction: true, // default
    onTransactionUpdated: (refreshed) {
      debugPrint('${refreshed.status} ${refreshed.travelRuleStatus}');
    },
  ),
);

debugPrint('${result.transactionId} ${result.status} ${result.actionRequired?.type}');
// Errors: DiditTransactionException with code invalid_token | expired_token | validation | network
```

Todos los SDK exponen también `getTransaction(transactionToken, transactionId)` para volver a leer la transacción con el mismo token: los SDK la invocan por ti después de un flujo de acción iniciado, y tú puedes llamarla en cualquier momento.

En dispositivos móviles, el transporte de la huella del dispositivo ya se aplica a todas las llamadas de red del SDK, por lo que los envíos de transacciones llevan automáticamente la misma inteligencia del dispositivo.

## Sin los SDK

Todo lo que hacen los SDK se realiza mediante HTTPS estándar: no necesitas los SDK para nada de esto.
Hay dos formas de integración equivalentes:

**Solo del lado del servidor (sin tokens).** Envía la solicitud con [`POST /v3/transactions/`](https://docs.didit.me/es/management-api/transactions/create) desde tu backend usando tu clave de API. La respuesta (y cada `GET /v3/transactions/{transaction_id}/` posterior) incluye el mismo bloque `action_required`. También puedes pasar un contexto `device` explícito por participante, ya que no hay un SDK que lo recopile por ti.

**Del lado del cliente con tu propia pila HTTP.** Genera un token con [`POST /v3/transactions/sdk-token/`](https://docs.didit.me/es/management-api/transactions/sdk-token) y luego llama a [`POST /v1/transactions/`](https://docs.didit.me/es/management-api/transactions/submit-sdk) y [`GET /v1/transactions/{transaction_id}/`](https://docs.didit.me/es/management-api/transactions/get-sdk) desde el dispositivo con el encabezado `X-Transaction-Token`: se mantiene la misma vinculación al sujeto, y la IP y el user-agent se derivan en el servidor de la misma forma.

Después, gestiona `action_required` por tu cuenta:

| `action_required.type` | Qué hacer |
| - | - |
| `wallet_ownership` | Entrega `url` al usuario y ábrela en un **navegador real**: una pestaña normal en web, `SFSafariViewController` / Chrome Custom Tabs en móvil, o por otro canal mediante un enlace por correo electrónico o un código QR. Nunca la muestres dentro de un webview integrado: los deep links de wallet no pueden salir de él. |
| `verification_session` | Envía al usuario a `url` (es la página de verificación alojada estándar de Didit) o pasa `session_token` a cualquier integración de verificación de Didit que ya utilices. |

Y detecta la finalización sin ningún callback en el cliente:

* **Webhooks (recomendado).** `transaction.status.updated` se dispara cuando la acción pendiente resuelve la transacción, y `travel_rule.status.updated` se dispara en cada transición del intercambio; ambos incluyen lo suficiente para actualizar tus registros sin polling.
* **Polling.** Vuelve a leer la transacción (`/v3/` desde tu backend, o `/v1/` con el token mientras sea válido): `action_required` vuelve a `null` una vez resuelta, y `status` / `travel_rule.status` contienen el resultado.

En este diseño no hay dependencias de un solo intento: el widget, la sesión de verificación y tu backend convergen en el mismo estado persistente de la transacción, por lo que siempre puedes recuperarte de una pestaña cerrada o de un webhook perdido volviendo a leer la transacción.

## Webhooks

Tu backend no necesita que la aplicación le informe.
Cuando una transacción enviada requiere una acción del usuario, el [webhook de transacción](https://docs.didit.me/es/transaction-monitoring/webhooks.md) (`transaction.created` o `transaction.status.updated`) incluye el mismo bloque `action_required`, de modo que tus sistemas conocen la acción pendiente aunque la aplicación se cierre antes de que termine el flujo.
Una acción `verification_session` tiene el estado `AWAITING_USER`, pero una acción `wallet_ownership` no cambia por sí sola el estado de la transacción, así que lee `action_required` en lugar del estado.
Las transiciones de Travel Rule emiten además [`travel_rule.status.updated`](https://docs.didit.me/es/transaction-monitoring/travel-rule#webhooks).

## Próximos pasos

> [card] [Envío de transacciones](https://docs.didit.me/es/transaction-monitoring/transactions.md)
>
> La API Create Transaction del lado del servidor y la referencia completa del payload.

> [card] [Travel Rule](https://docs.didit.me/es/transaction-monitoring/travel-rule.md)
>
> El intercambio gestionado entre ordenante y beneficiario, su configuración y su ciclo de vida.

> [card] [Widget de propiedad de wallet](https://docs.didit.me/es/transaction-monitoring/wallet-ownership-widget.md)
>
> El flujo alojado para demostrar el control que sustenta las acciones wallet\_ownership.

> [card] [Webhooks](https://docs.didit.me/es/transaction-monitoring/webhooks.md)
>
> transaction.created, transaction.status.updated y verificación de firma.

> [card] [Reglas](https://docs.didit.me/es/transaction-monitoring/rules.md)
>
> Acciones de las reglas, incluidas las que crean sesiones de verificación.

> [card] [Estados](https://docs.didit.me/es/transaction-monitoring/statuses.md)
>
> Qué significa AWAITING\_USER en el ciclo de vida de la transacción.


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