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

# Soumission de transactions SDK - KYT côté client

> Avec Didit, soumettez des transactions depuis votre app via un token limité : analyse de l'appareil et liaison anti-usurpation du sujet.

Le SDK Didit peut soumettre des [transactions surveillées](https://docs.didit.me/fr/transaction-monitoring/transactions.md) directement depuis l'appareil de l'utilisateur final, y compris les transferts [Travel Rule](https://docs.didit.me/fr/transaction-monitoring/travel-rule.md) et le filtrage crypto.
Votre backend crée un token à courte durée de vie et limité à un périmètre ; le SDK s'occupe du reste.
Il joint automatiquement l'analyse de l'appareil et, lorsqu'un transfert Travel Rule exige une preuve de contrôle de portefeuille, il lance immédiatement ce parcours.
Une session de vérification requise n'est jamais lancée automatiquement : elle est renvoyée dans la réponse pour que votre application la lance avec votre propre intégration de vérification Didit. Voir [Actions requises de l'utilisateur](#actions-requises-de-l’utilisateur) ci-dessous.

## Vue d'ensemble

La soumission côté client repose sur trois garanties :

* **Liaison du sujet à l'abri de l'usurpation.** Le token est limité à un seul `vendor_data` (l'identifiant de votre utilisateur) au moment de sa création. L'identité du sujet de chaque transaction soumise avec ce token est appliquée côté serveur à partir de cette liaison. Un client altéré ne peut donc pas soumettre au nom d'un autre utilisateur, et le payload n'a jamais besoin de contenir des identifiants.
* **Analyse automatique de l'appareil.** Le SDK collecte une empreinte de l'appareil et l'envoie avec la soumission. Didit déduit côté serveur l'adresse IP et l'agent utilisateur. Le contexte combiné de l'appareil est rattaché au sujet de la transaction, sans bibliothèque supplémentaire, sans token d'appareil distinct et sans travail d'intégration de votre part. Il alimente les [règles](https://docs.didit.me/fr/transaction-monitoring/rules.md) basées sur l'appareil et la corrélation par appareil entre les sessions et les transactions.
* **Preuve de contrôle de portefeuille lancée automatiquement.** Lorsque la transaction créée exige que l'utilisateur final prouve le contrôle d'un portefeuille, la réponse contient un bloc `action_required` de type `wallet_ownership`. Le SDK ouvre alors immédiatement ce parcours, puis actualise la transaction à la fin. Aucune boucle d'interrogation ni envoi de lien n'est à mettre en place de votre côté. Une action `verification_session` requise est différente : elle n'est jamais lancée automatiquement et revient dans le même bloc pour que votre application la lance avec sa propre intégration de vérification Didit.

> [!NOTE]
>
> L'[`POST /v3/transactions/`](https://docs.didit.me/fr/transaction-monitoring/transactions) côté serveur reste le bon choix lorsqu'aucun utilisateur final n'est présent : virements back-office, imports par lots ou événements remontés par vos propres systèmes.
> Les deux chemins créent les mêmes enregistrements de transaction, appliquent les mêmes règles et émettent les mêmes [webhooks](https://docs.didit.me/fr/transaction-monitoring/webhooks.md).

## Déroulement

**1. Créer un token de transaction**

Votre backend appelle `POST /v3/transactions/sdk-token/` avec votre clé API et le `vendor_data` de l'utilisateur, puis transmet le `sdk_token` renvoyé à votre application.

**2. Soumettre depuis l'appareil**

L'application appelle `submitTransaction` sur le SDK, qui envoie le payload à `POST /v1/transactions/` avec l'en-tête `X-Transaction-Token` et l'enrichissement automatique de l'appareil.

**3. Traiter toute action requise**

Si `action_required.type` vaut `wallet_ownership`, le SDK lance automatiquement le widget, puis actualise la transaction via `GET /v1/transactions/{transaction_id}/`.
Si la valeur est `verification_session`, le SDK ne lance rien : lisez `action_required` et démarrez votre propre parcours de vérification avec son `session_token` (ou `url` sur le web).

## Création d'un token de transaction

Créez les tokens depuis votre backend avec `POST /v3/transactions/sdk-token/`, avec la même authentification par clé API que le reste des endpoints de transactions `/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"
}
```

| Champ | Type | Description |
| - | - | - |
| `vendor_data` | string | **Obligatoire.** Votre identifiant interne de l'utilisateur final auquel ce token est limité. Chaque transaction soumise avec le token voit son identité de sujet appliquée à partir de cette valeur. |
| `ttl_seconds` | integer | Durée de vie du token en secondes. Valeur par défaut : `900` (15 minutes) ; maximum : `86400` (24 heures). |
| `max_uses` | integer | Facultatif. Nombre maximal de soumissions réussies autorisées avec ce token. Omettez ce champ pour un usage illimité dans la limite du TTL. |

Les alias en camelCase (`vendorData`, `ttlSeconds`, `maxUses`) sont également acceptés, comme pour le reste de l'API `/v3/`.

> [!WARNING]
>
> **Ne placez jamais votre clé API dans une application.**
> La clé API reste sur votre backend et crée les tokens ; seul le `sdk_token` à courte durée de vie est transmis à l'appareil.
> Créez un nouveau token pour chaque session utilisateur plutôt que de réutiliser des tokens de longue durée, et limitez `max_uses` au plus juste lorsque le nombre de soumissions attendues est connu.

## Soumission depuis l'appareil

Les SDK envoient une requête `POST /v1/transactions/` avec le token dans l'en-tête `X-Transaction-Token`.
Le payload transmis est en camelCase. Les méthodes `submitTransaction` le construisent pour vous : vous ne manipulez ce format directement que si vous appelez l'endpoint depuis votre propre client 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
  }'
```

Chaque champ de premier niveau correspond à son équivalent dans [`POST /v3/transactions/`](https://docs.didit.me/fr/transaction-monitoring/transactions) :

| Champ transmis | Équivalent `/v3/` | Remarques |
| - | - | - |
| `txnId` | `transaction_id` | **Obligatoire.** Votre identifiant unique de la transaction. |
| `txnDate` | `transaction_at` | Horodatage ISO-8601 ; valeur par défaut : maintenant. |
| `zoneId` | `time_zone` | Identifiant de fuseau horaire IANA, par exemple `Europe/Madrid`. |
| `type` | `transaction_category` | **Obligatoire.** `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet` et les autres catégories. |
| `info` | `transaction_details` | **Obligatoire.** `direction`, `amount`, `currency` (obligatoire), ainsi que `currencyType`, `amountInDefaultCurrency`, `defaultCurrencyCode`, `paymentDetails`, `paymentTxnId`, `type` (type d'action), `cryptoParams`. |
| `subject`, `counterparty` | `subject`, `counterparty` | Participants : `type` (type d'entité), `externalUserId`, `fullName`, `firstName`, `lastName`, `dob`, `address`, `institutionInfo`, `device`, `paymentMethod` (`type`, `accountId`, `issuingCountry`). Pour la contrepartie, un `paymentMethod.type` de `unhosted_wallet` déclare la destination comme portefeuille auto-hébergé : voir [ci-dessous](#déclarer-un-portefeuille-auto-hébergé). |
| `props` | `custom_properties` | Paires clé-valeur personnalisées, utilisables dans les règles sous la forme `custom_values.<key>`. |
| `travelRule` | `travel_rule_details` | `status`, `protocol`, `required`, `obligationsCount`, `originatorData`, `beneficiaryData`, `metadata` ; `status` est obligatoire dès que vous envoyez ce bloc, sinon la requête échoue avec `400`. Lorsque Travel Rule est activé, Didit ignore tout `status` que vous envoyez et pilote lui-même l'échange. |
| `includeCryptoScreening` | `include_crypto_screening` | Remplacement, pour une transaction donnée, du filtrage crypto. |

La réponse reprend le même détail de transaction en snake\_case que les endpoints `/v3/`, auquel s'ajoute le bloc `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"
  }
}
```

### Déclarer un portefeuille auto-hébergé

L'exemple ci-dessus déclare la destination comme le portefeuille auto-hébergé de l'utilisateur : le `paymentMethod.type` de la contrepartie est `unhosted_wallet`.
Cette déclaration modifie le fonctionnement de l'[échange Travel Rule](https://docs.didit.me/fr/transaction-monitoring/travel-rule.md#portefeuilles-auto-hébergés-déclarés) :

* Didit ne passe pas par le routage vers un VASP de la contrepartie, car aucun VASP ne se trouve derrière un portefeuille auto-hébergé, et crée directement le transfert en `UNCONFIRMED_OWNERSHIP`.
* Lorsque `auto_wallet_verification` est activé dans vos [paramètres Travel Rule](https://docs.didit.me/fr/transaction-monitoring/travel-rule.md#configuration) (valeur par défaut), une session du [widget de vérification de propriété du portefeuille](https://docs.didit.me/fr/transaction-monitoring/wallet-ownership-widget.md) est créée automatiquement et renvoyée dans `action_required`, exactement comme dans la réponse ci-dessus.
* Le SDK lance automatiquement le widget. Une fois que l'utilisateur a prouvé qu'il contrôle le portefeuille, le transfert effectue la correspondance du nom et passe à `COMPLETED`, ou à `COUNTERPARTY_MISMATCHED_DATA` si le nom ne correspond pas au titulaire de l'adresse dans le carnet d'adresses du portefeuille.

Sans cette déclaration, Didit essaie d'abord les [canaux](https://docs.didit.me/fr/transaction-monitoring/travel-rule.md#vue-d’ensemble) dans l'ordre et ne recourt au flux de propriété que si aucun VASP de la contrepartie n'est trouvé.
Déclarez le portefeuille dès que votre produit sait que la cible du retrait est le propre portefeuille de l'utilisateur : vous évitez une tentative de routage inutile et l'utilisateur accède immédiatement au parcours de preuve.

### Contrôle du sujet

Trois éléments sont toujours décidés par le token, jamais par le payload :

* L'application sous laquelle la transaction est créée.
* L'identité du sujet : `subject.externalUserId` dans le payload est ignoré et remplacé par le `vendor_data` du token.
* Le contexte d'appareil du sujet (voir ci-dessous) : les champs calculés côté serveur l'emportent en cas de conflit avec ce qui figure dans `subject.device`.

Un `401` renvoyé par cet endpoint signifie que le token a été rejeté : il est absent, inconnu, révoqué, a dépassé son `max_uses` ou a expiré.
Les réponses pour un token expiré contiennent toujours le mot `expired` dans le détail de l'erreur ; c'est ainsi que les SDK distinguent les erreurs `expired_token` des erreurs `invalid_token`.

### Relire la transaction

`GET /v1/transactions/{transaction_id}/` renvoie le même détail, avec la même authentification par `X-Transaction-Token`.
Les SDK l'appellent automatiquement après la fin d'un flux d'action lancé ou à la fermeture de sa fenêtre, si bien que l'état final ne dépend jamais d'un seul événement ou d'une seule notification.
Vous pouvez l'appeler vous-même pour vérifier `status` et `action_required` à tout moment tant que le token est valide ; depuis votre backend, [`GET /v3/transactions/{transaction_id}/`](https://docs.didit.me/fr/transaction-monitoring/transactions) renvoie le même enregistrement.

## Analyse automatique de l'appareil

Chaque soumission transmet des informations sur l'appareil sans aucun travail d'intégration de votre côté :

* Le SDK collecte une empreinte d'appareil respectueuse de la vie privée (le schéma `didit-fp-v2` : signaux de plateforme et de navigateur, identifiant d'appareil persistant, score de bot et hachage composite) et l'envoie avec la requête.
* Didit déduit côté serveur l'adresse IP, l'agent utilisateur et l'accept-language à partir de la connexion elle-même : le payload ne peut donc pas les falsifier.

Le résultat fusionné est normalisé dans le contexte d'appareil du participant sujet, puis inséré ou mis à jour dans le graphe des appareils, le même que celui alimenté par les sessions KYC.
Les transactions deviennent ainsi corrélables par appareil à l'échelle de l'ensemble de vos utilisateurs (« liées par appareil »), les signaux d'appareil et de réseau alimentent les [règles](https://docs.didit.me/fr/transaction-monitoring/rules.md) et l'[enrichissement IP](https://docs.didit.me/fr/transaction-monitoring/ip-enrichment.md), et ils apparaissent dans le détail de la transaction dans la [Console](https://docs.didit.me/fr/transaction-monitoring/console.md).

Les transactions `/v3/` soumises depuis votre serveur peuvent continuer à transmettre des objets `device_context` explicites pour chaque participant ; avec le chemin SDK, vous obtenez gratuitement une version plus complète de ces mêmes données.

## Actions requises de l'utilisateur

Certaines transactions exigent que l'utilisateur final effectue une action avant de pouvoir aboutir : une règle peut exiger une session de vérification, ou un transfert Travel Rule peut exiger la preuve que le portefeuille de destination appartient bien à l'utilisateur.
Les deux cas apparaissent au même endroit : dans le bloc `action_required` de la réponse à la soumission (et de chaque lecture ultérieure de la transaction) :

```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"
}
```

| Type | Quand il apparaît | Ce que fait l'utilisateur |
| - | - | - |
| `verification_session` | Une [action de règle](https://docs.didit.me/fr/transaction-monitoring/rules.md) a exigé une vérification supplémentaire de l'utilisateur, et la transaction est passée à `AWAITING_USER`. | Complète une session de vérification hébergée (par exemple une nouvelle vérification biométrique), lancée par votre application : voir [Lancer une session de vérification](#lancer-une-session-de-vérification) ci-dessous. |
| `wallet_ownership` | Un transfert Travel Rule nécessite une preuve de contrôle du portefeuille par l'utilisateur final : la destination a été [déclarée portefeuille auto-hébergé](#déclarer-un-portefeuille-auto-hébergé) (`UNCONFIRMED_OWNERSHIP`), ou un transfert sortant n'a trouvé aucun VASP de la contrepartie, et `auto_wallet_verification` est activé dans vos [paramètres Travel Rule](https://docs.didit.me/fr/transaction-monitoring/travel-rule.md#configuration). Didit crée automatiquement une session du [widget de preuve de propriété du portefeuille](https://docs.didit.me/fr/transaction-monitoring/wallet-ownership-widget.md) liée à la transaction. | Prouve le contrôle du portefeuille par signature de message (sur une chaîne prise en charge), capture d'écran ou auto-déclaration, selon ce qu'autorisent vos paramètres Travel Rule, mais jamais par un test Satoshi sur une session créée automatiquement : le SDK lance celle-ci automatiquement pour vous. |

Si les deux cas pourraient s'appliquer à la même transaction, `wallet_ownership` est prioritaire, car la preuve de propriété manquante bloque l'échange Travel Rule lui-même.
`action_required` devient `null` une fois l'action en attente résolue.
Le champ `remediation` existant est conservé par souci de compatibilité ascendante ; `action_required` est le bloc de référence.

### Contrat de lancement automatique

`autoLaunchAction` (par défaut `true`) ne lance automatiquement que l'action `wallet_ownership` :

* **Web** : ouverte dans la fenêtre modale superposée propre au SDK, la fin du parcours étant détectée par une messagerie dont l'origine est validée, complétée par une interrogation bornée de la transaction (jamais en dépendant d'un événement unique).
* **iOS / Android** (ainsi que les wrappers React Native et Flutter, qui délèguent aux SDK natifs) : présentée sous forme d'**expérience native de preuve de propriété du portefeuille** du SDK (SwiftUI / Jetpack Compose) lorsque `useNativeWalletOwnership` vaut `true` (valeur par défaut), avec repli sur `SFSafariViewController` / un Chrome Custom Tab, puis, en dernier recours, sur le navigateur par défaut. Une fois le parcours fermé ou lorsque l'application revient au premier plan, le SDK relit automatiquement la transaction.

Une action `verification_session` n'est jamais lancée automatiquement : elle revient toujours dans `action_required`, pour que votre application la démarre avec sa propre intégration de vérification Didit.

> [!WARNING]
>
> Le widget de preuve de propriété du portefeuille ne doit jamais être affiché dans une webview mobile intégrée : les liens profonds vers les portefeuilles ne peuvent pas en sortir, et le parcours de preuve se bloque.
> Les SDK utilisent déjà la bonne surface de navigation lors du lancement automatique ; conservez cette règle si vous gérez vous-même l'`url`.
> Consultez le [guide du widget de preuve de propriété du portefeuille](https://docs.didit.me/fr/transaction-monitoring/wallet-ownership-widget.md#ouvrir-l’url-renvoyée) pour plus de détails.

Une fois le parcours de preuve de propriété du portefeuille lancé automatiquement par le SDK terminé ou fermé, le SDK relit la transaction et transmet le résultat actualisé à votre callback `onActionCompleted`.
Une action `verification_session` ne fait pas partie de ce cycle, puisque votre application la lance : voir la section suivante.

### Lancer une session de vérification

Lorsque `action_required.type` vaut `verification_session`, lisez le bloc dans `result.actionRequired` et transmettez-le à votre intégration de vérification Didit existante :

```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);
}
```

Une fois le parcours de vérification terminé, appelez `GET /v1/transactions/{transaction_id}/` (ou attendez le [webhook de transaction](#webhooks)) pour récupérer le statut actualisé de la transaction.

### Exemple de renforcement biométrique pour un retrait d'un montant élevé

C'est là que ce parcours est le plus utile : conditionner les transactions à risque (montants élevés, nouveaux bénéficiaires, nouveaux appareils) à une nouvelle vérification biométrique de la même personne que celle vérifiée lors de l'onboarding.

La prise de contrôle de compte ressemble exactement à cela : mot de passe correct, code 2FA correct (obtenu par hameçonnage ou par échange de SIM), retrait vers un portefeuille que vous n'avez jamais vu.
À ce stade, toutes les défenses fondées sur des identifiants ont déjà échoué.
Le visage, lui, n'a pas échoué : l'attaquant ne peut pas réussir un selfie soumis à une détection du vivant et qui doit correspondre au visage capturé lors de l'onboarding KYC du titulaire légitime du compte.

**1. Créer la règle une fois, dans la console**

**Transactions → Rules → Create Rule** : condition `amount gte 5000`, action **Change status → `AWAITING_USER`**, et sélectionnez votre workflow **Authentification biométrique** pour cette action.
Ajoutez les autres conditions de risque souhaitées (contrepartie inédite (ou contrepartie utilisée pour la première fois), nouvelle empreinte d'appareil, changement de pays de l'IP) sous forme de conditions supplémentaires ou de règles supplémentaires.

**2. Soumettre le retrait comme d'habitude**

Votre code de soumission ne change pas. Lorsque la règle se déclenche, la réponse renvoie `status: "AWAITING_USER"` et un bloc `action_required` de type `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. Lancer la nouvelle vérification**

Transmettez le `session_token` (ou le `url`) à votre intégration de vérification Didit existante, exactement comme dans [Lancer une session de vérification](#lancer-une-session-de-vérification).
La session est créée avec le même `vendor_data` que le sujet de la transaction : Didit dispose donc déjà du visage de référence vérifié de l'utilisateur, issu de l'onboarding (le selfie vérifié par détection du vivant, la photo du passeport stockée sur la puce ou le portrait du document).
L'utilisateur prend un seul selfie en direct ; Didit effectue une détection du vivant et une correspondance faciale 1:1 avec cette référence enregistrée.
Pas de nouveau scan de document, pas de nouvel onboarding : quelques secondes de friction, uniquement sur les transactions qui le méritent.

**4. Conclure selon le résultat**

Lorsque la session se termine, la transaction est réévaluée automatiquement : `APPROVED` si la nouvelle vérification est réussie, `DECLINED` si elle échoue, avec un webhook `transaction.status.updated` dans les deux cas (`reason: remediation_completed` dans l'historique de la transaction).
Votre application récupère le résultat via des callbacks de type `onActionCompleted`, via `GET /v1/transactions/{transaction_id}/` ou via le webhook.

Ce fonctionnement repose sur le fait que l'onboarding KYC, la revérification biométrique et la décision sur la transaction s'exécutent sur une même plateforme : le visage capturé à l'onboarding sert de référence pour l'étape de vérification renforcée, le graphe de l'appareil relie la session à la transaction, et le moteur de règles les associe. Aucun code d'intégration entre fournisseurs, aucun export d'images faciales, aucun contrat biométrique distinct.
La revérification est facturée comme une fonctionnalité standard d’Authentification biométrique ([\$0.10](https://docs.didit.me/fr/getting-started/pricing.md)), uniquement lorsqu'elle est effectivement exécutée.

## Web SDK

`submitTransaction` est disponible dès aujourd'hui sur le singleton du Web SDK :

```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);
```

Le résultat est `{ transactionId, status, travelRuleStatus?, actionRequired? }`.
Dans le payload au niveau du SDK, vous renseignez `category`, `details` et `customProperties` ; le SDK les convertit pour vous vers les alias du protocole (`type`, `info`, `props`).

Les erreurs sont typées via `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 natifs

> [!NOTE]
>
> L'API native `submitTransaction` est livrée avec les SDK iOS et Android 4.1.0 (ainsi que les versions React Native et Flutter qui utilisent cette version).
> Contrairement au Web SDK, les payloads natifs utilisent directement les noms de champs du protocole : `type`, `info` et `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
```

Tous les SDK exposent aussi `getTransaction(transactionToken, transactionId)` pour relire la transaction avec le même token. Les SDK l'appellent pour vous après un flux d'action lancé, et vous pouvez l'appeler vous-même à tout moment.

Sur mobile, le mécanisme de transport de l'empreinte de l'appareil est déjà appliqué à tous les appels réseau du SDK : les soumissions de transactions transmettent donc automatiquement les mêmes informations sur l'appareil.

## Sans les SDK

Tout ce que font les SDK passe par du HTTPS standard, rien ici ne les nécessite.
Deux formes d'intégration équivalentes existent :

**Côté serveur uniquement (sans aucun token).** Soumettez avec [`POST /v3/transactions/`](https://docs.didit.me/fr/management-api/transactions/create) depuis votre backend, avec votre clé API. La réponse (et chaque `GET /v3/transactions/{transaction_id}/` ultérieur) contient le même bloc `action_required`. Vous pouvez également transmettre un contexte `device` explicite pour chaque participant, puisqu'aucun SDK ne le collecte à votre place.

**Côté client avec votre propre pile HTTP.** Générez un token avec [`POST /v3/transactions/sdk-token/`](https://docs.didit.me/fr/management-api/transactions/sdk-token), puis appelez [`POST /v1/transactions/`](https://docs.didit.me/fr/management-api/transactions/submit-sdk) et [`GET /v1/transactions/{transaction_id}/`](https://docs.didit.me/fr/management-api/transactions/get-sdk) depuis l'appareil avec l'en-tête `X-Transaction-Token`. La même liaison au sujet et la dérivation côté serveur de l'IP et du user-agent s'appliquent.

Gérez ensuite vous-même `action_required` :

| `action_required.type` | Action à effectuer |
| - | - |
| `wallet_ownership` | Transmettez `url` à l'utilisateur et ouvrez-la dans une **véritable surface de navigateur** : un onglet classique sur le web, `SFSafariViewController` / Chrome Custom Tabs sur mobile, ou hors bande via un lien e-mail ou un QR code. Ne l'affichez jamais dans une webview intégrée : les liens profonds de portefeuille ne peuvent pas en sortir. |
| `verification_session` | Envoyez l'utilisateur vers `url`, qui correspond à la page de vérification Didit hébergée standard, ou transmettez `session_token` à toute intégration de vérification Didit que vous utilisez déjà. |

Et détectez la fin du flux sans aucun callback côté client :

* **Webhooks (recommandé).** `transaction.status.updated` se déclenche lorsque l'action en attente fait aboutir la transaction, et `travel_rule.status.updated` se déclenche à chaque transition des échanges. Les deux contiennent suffisamment d'informations pour mettre à jour vos dossiers sans aucune interrogation.
* **Interrogation.** Relisez la transaction (`/v3/` depuis votre backend, ou `/v1/` avec le token tant qu'il est valide) : `action_required` revient à `null` une fois l'action résolue, et `status` / `travel_rule.status` indiquent le résultat.

Cette conception ne comporte aucune dépendance à usage unique : le widget, la session de vérification et votre backend convergent vers le même état durable de la transaction. L’état reste donc toujours récupérable, en relisant la transaction, après la fermeture d’un onglet ou la perte d’un webhook.

## Webhooks

Votre backend n'a pas besoin que l'application lui remonte l'information.
Lorsqu'une transaction soumise nécessite une action de l'utilisateur, le [webhook de transaction](https://docs.didit.me/fr/transaction-monitoring/webhooks.md) (`transaction.created` ou `transaction.status.updated`) contient le même bloc `action_required`. Vos systèmes connaissent donc l'action en attente, même si l'application est fermée avant la fin du flux.
Une action `verification_session` s'accompagne du statut `AWAITING_USER`, mais une action `wallet_ownership` ne modifie pas le statut de la transaction à elle seule : lisez donc `action_required` plutôt que le statut.
Les transitions de la Travel Rule émettent en outre [`travel_rule.status.updated`](https://docs.didit.me/fr/transaction-monitoring/travel-rule#webhooks).

## Étapes suivantes

> [card] [Soumission de transactions](https://docs.didit.me/fr/transaction-monitoring/transactions.md)
>
> L'API Create Transaction côté serveur et la référence complète des payloads.

> [card] [Travel Rule](https://docs.didit.me/fr/transaction-monitoring/travel-rule.md)
>
> L’échange géré entre le donneur d’ordre et le bénéficiaire, ses paramètres et son cycle de vie.

> [card] [Widget de propriété de portefeuille](https://docs.didit.me/fr/transaction-monitoring/wallet-ownership-widget.md)
>
> Le flux hébergé de preuve de contrôle sous-jacent aux actions wallet\_ownership.

> [card] [Webhooks](https://docs.didit.me/fr/transaction-monitoring/webhooks.md)
>
> transaction.created, transaction.status.updated et vérification de signature.

> [card] [Règles](https://docs.didit.me/fr/transaction-monitoring/rules.md)
>
> Actions de règles, y compris celles qui créent des sessions de vérification.

> [card] [Statuts](https://docs.didit.me/fr/transaction-monitoring/statuses.md)
>
> Ce que signifie AWAITING\_USER dans le cycle de vie de la transaction.


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