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

# Отправка транзакций через SDK: клиентский KYT

> Отправка транзакций из приложения через Didit: токен с ограниченным доступом, анализ устройства, защита от подмены и подтверждение кошелька.

Didit SDK могут отправлять [мониторируемые транзакции](https://docs.didit.me/ru/transaction-monitoring/transactions.md) прямо с устройства конечного пользователя, включая переводы по [Travel Rule](https://docs.didit.me/ru/transaction-monitoring/travel-rule.md) и скрининг криптовалют.
Ваш бэкенд выпускает короткоживущий токен с ограниченной областью действия, а SDK выполняет остальное.
Он автоматически прикрепляет данные об устройстве, а если переводу Travel Rule нужно подтверждение владения кошельком, сразу запускает соответствующий сценарий.
Обязательная сессия верификации никогда не запускается автоматически: она возвращается в ответе, чтобы ваше приложение запустило её через собственную интеграцию верификации Didit. См. [Обязательные действия пользователя](#требуемые-действия-пользователя) ниже.

## Обзор

Отправка с клиентской стороны основана на трёх гарантиях:

* **Защищённая от спуфинга привязка субъекта.** Токен привязывается к одному `vendor_data` (идентификатору вашего пользователя) при выпуске. Идентичность субъекта каждой транзакции, отправленной с этим токеном, принудительно устанавливается на стороне сервера на основе этой привязки. Поэтому подменённый клиент не может отправлять транзакции от имени другого пользователя, а payload никогда не должен содержать учётные данные.
* **Автоматический сбор данных об устройстве.** SDK собирает отпечаток устройства и отправляет его вместе с транзакцией, а Didit определяет IP-адрес и user agent на стороне сервера. Совокупные данные об устройстве прикрепляются к субъекту транзакции без дополнительных библиотек, отдельного токена устройства и какой-либо настройки с вашей стороны. Они используются для [правил](https://docs.didit.me/ru/transaction-monitoring/rules.md) на основе устройства и для корреляции сессий и транзакций, связанных через устройство.
* **Автоматически запускаемое подтверждение владения кошельком.** Если созданная транзакция требует, чтобы конечный пользователь подтвердил контроль над кошельком, ответ содержит блок `action_required` типа `wallet_ownership`. SDK сразу открывает этот сценарий, а после его завершения обновляет транзакцию. Не нужно создавать циклы опроса или самостоятельно отправлять ссылки. Обязательное действие `verification_session` устроено иначе: оно никогда не запускается автоматически и возвращается в том же блоке, чтобы ваше приложение запустило его через собственную интеграцию верификации Didit.

> [!NOTE]
>
> Серверный [`POST /v3/transactions/`](https://docs.didit.me/ru/transaction-monitoring/transactions) остаётся правильным выбором, когда конечного пользователя нет: бэкофисные переводы, пакетный импорт или события, передаваемые вашими собственными системами.
> Оба пути создают одни и те же записи транзакций, выполняют одни и те же правила и отправляют одни и те же [webhook](https://docs.didit.me/ru/transaction-monitoring/webhooks.md).

## Процесс

**1. Выпустите токен транзакции**

Ваш бэкенд вызывает `POST /v3/transactions/sdk-token/` с вашим API-ключом и `vendor_data` пользователя и передаёт возвращённый `sdk_token` вашему приложению.

**2. Отправка с устройства**

Приложение вызывает `submitTransaction` в SDK, который отправляет payload на `POST /v1/transactions/` с заголовком `X-Transaction-Token` и автоматическим обогащением данными об устройстве.

**3. Обработайте обязательное действие**

Если `action_required.type` равен `wallet_ownership`, SDK автоматически открывает виджет, а затем обновляет транзакцию через `GET /v1/transactions/{transaction_id}/`.
Если это `verification_session`, SDK ничего не запускает: прочитайте `action_required` и начните собственный сценарий верификации с его `session_token` (или `url` в веб-версии).

## Выпуск токена транзакции

Выпускайте токены со своего бэкенда через `POST /v3/transactions/sdk-token/`. Аутентификация по API-ключу такая же, как у остальных эндпоинтов транзакций `/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"
}
```

| Поле | Тип | Описание |
| - | - | - |
| `vendor_data` | string | **Обязательно.** Ваш внутренний идентификатор конечного пользователя, к которому привязан этот токен. Идентичность субъекта каждой транзакции, отправленной с этим токеном, принудительно устанавливается на стороне сервера на основе этого значения. |
| `ttl_seconds` | integer | Время жизни токена в секундах. По умолчанию `900` (15 минут); максимум `86400` (24 часа). |
| `max_uses` | integer | Необязательно. Максимальное количество успешных отправок с этим токеном. Не указывайте, если нужно неограниченное число использований в пределах TTL. |

Как и в остальном API `/v3/`, принимаются также псевдонимы в camelCase (`vendorData`, `ttlSeconds`, `maxUses`).

> [!WARNING]
>
> **Никогда не передавайте API-ключ в приложение.**
> API-ключ остаётся на вашем бэкенде, а бэкенд выпускает токены. На устройство передаётся только короткоживущий `sdk_token`.
> Выпускайте новый токен для каждой пользовательской сессии, а не используйте повторно долгоживущие токены. Когда известно ожидаемое число отправок, ограничивайте `max_uses`.

## Отправка с устройства

SDK отправляют запрос на `POST /v1/transactions/` с токеном в заголовке `X-Transaction-Token`.
Payload на уровне протокола передаётся в формате camelCase. Методы `submitTransaction` формируют его за вас, поэтому работать с ним напрямую нужно только при вызове эндпоинта из собственного 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
  }'
```

Каждое поле верхнего уровня соответствует своему аналогу в [`POST /v3/transactions/`](https://docs.didit.me/ru/transaction-monitoring/transactions):

| Поле в wire-формате | Аналог в `/v3/` | Примечания |
| - | - | - |
| `txnId` | `transaction_id` | **Обязательно.** Ваш уникальный идентификатор транзакции. |
| `txnDate` | `transaction_at` | Временная метка ISO-8601; по умолчанию текущее время. |
| `zoneId` | `time_zone` | Идентификатор часового пояса IANA, например `Europe/Madrid`. |
| `type` | `transaction_category` | **Обязательно.** `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet` и другие категории. |
| `info` | `transaction_details` | **Обязательно.** `direction`, `amount`, `currency` (обязательно), а также `currencyType`, `amountInDefaultCurrency`, `defaultCurrencyCode`, `paymentDetails`, `paymentTxnId`, `type` (тип действия), `cryptoParams`. |
| `subject`, `counterparty` | `subject`, `counterparty` | Участники: `type` (тип сущности), `externalUserId`, `fullName`, `firstName`, `lastName`, `dob`, `address`, `institutionInfo`, `device`, `paymentMethod` (`type`, `accountId`, `issuingCountry`). Если `paymentMethod.type` у контрагента равен `unhosted_wallet`, назначение считается некастодиальным кошельком, см. [ниже](#объявление-некастодиального-кошелька). |
| `props` | `custom_properties` | Пользовательские пары ключ-значение, которые в правилах доступны как `custom_values.<key>`. |
| `travelRule` | `travel_rule_details` | `status`, `protocol`, `required`, `obligationsCount`, `originatorData`, `beneficiaryData`, `metadata`; `status` обязателен, если вы передаёте этот блок, иначе запрос завершится с ошибкой `400`. Если Travel Rule включён, Didit игнорирует присланный вами `status` и сам управляет обменом. |
| `includeCryptoScreening` | `include_crypto_screening` | Переопределение криптоскрининга для отдельной транзакции. |

Ответ содержит те же данные транзакции в snake\_case, что и эндпоинты `/v3/`, а также блок `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"
  }
}
```

### Объявление некастодиального кошелька

В примере выше получатель объявлен собственным некастодиальным кошельком пользователя: `paymentMethod.type` контрагента равен `unhosted_wallet`.
Это объявление меняет работу [обмена Travel Rule](https://docs.didit.me/ru/transaction-monitoring/travel-rule.md#объявленные-некастодиальные-кошельки):

* Didit полностью пропускает маршрутизацию через VASP-контрагента, так как за некастодиальным кошельком нет VASP, и создаёт перевод сразу в статусе `UNCONFIRMED_OWNERSHIP`.
* Если в [настройках Travel Rule](https://docs.didit.me/ru/transaction-monitoring/travel-rule.md#настройка) включён `auto_wallet_verification` (по умолчанию), сессия [виджета подтверждения владения кошельком](https://docs.didit.me/ru/transaction-monitoring/wallet-ownership-widget.md) создаётся автоматически и возвращается в `action_required`, как в ответе выше.
* SDK автоматически запускает виджет. Когда пользователь подтверждает контроль над кошельком, для перевода выполняется проверка совпадения имени, после чего он переходит в `COMPLETED`, либо в `COUNTERPARTY_MISMATCHED_DATA`, если имя не совпадает с владельцем адреса в адресной книге кошелька.

Без этого объявления Didit сначала перебирает [маршруты](https://docs.didit.me/ru/transaction-monitoring/travel-rule.md#обзор) по порядку и переходит к потоку подтверждения владения, только если ни один VASP-контрагент не найден.
Объявляйте кошелёк всякий раз, когда ваш продукт уже знает, что адрес вывода принадлежит самому пользователю: так вы избегаете неудачной попытки маршрутизации и сразу переводите пользователя в процесс подтверждения.

### Что задаёт токен

Три вещи всегда определяются токеном, а не payload:

* Приложение, в котором создаётся транзакция.
* Личность субъекта: `subject.externalUserId` в payload игнорируется и заменяется значением `vendor_data` из токена.
* Контекст устройства субъекта (см. ниже); данные, полученные на сервере, имеют приоритет над любыми значениями в `subject.device`.

Ответ `401` от этого эндпоинта означает, что токен отклонён: он отсутствует, неизвестен, отозван, исчерпал `max_uses` или истёк.
В ответах с истекшим токеном в описании ошибки всегда есть слово `expired`. По нему SDK отличают ошибку `expired_token` от `invalid_token`.

### Чтение транзакции

`GET /v1/transactions/{transaction_id}/` возвращает те же данные с той же аутентификацией `X-Transaction-Token`.
SDK вызывают его автоматически после завершения запущенного потока действия или по закрытии его окна, поэтому итоговое состояние никогда не зависит от одного события или уведомления.
Вы можете сами проверить `status` и `action_required` в любой момент, пока токен действителен. С вашего бэкенда та же запись возвращается через [`GET /v3/transactions/{transaction_id}/`](https://docs.didit.me/ru/transaction-monitoring/transactions).

## Автоматический анализ устройства

Каждая отправка содержит данные об устройстве без каких-либо дополнительных действий с вашей стороны:

* SDK собирает отпечаток устройства, безопасный с точки зрения конфиденциальности (схема `didit-fp-v2`: сигналы платформы и navigator, постоянный идентификатор устройства, оценка бота и составной хеш), и передаёт его вместе с запросом.
* Didit определяет IP-адрес, user agent и accept-language на сервере по самому соединению, поэтому их нельзя подделать через payload.

Объединённый результат нормализуется в контекст устройства участника-субъекта и добавляется или обновляется в графе устройств, том же, в который поступают сессии KYC. Благодаря этому транзакции можно сопоставлять по устройству во всей вашей пользовательской базе («связаны по устройству»). Сигналы устройства и сети попадают в [правила](https://docs.didit.me/ru/transaction-monitoring/rules.md) и [обогащение данных по IP](https://docs.didit.me/ru/transaction-monitoring/ip-enrichment.md), а также отображаются в деталях транзакции в [Консоли](https://docs.didit.me/ru/transaction-monitoring/console.md).

Транзакции `/v3/`, отправленные с сервера, по-прежнему могут передавать явные объекты `device_context` для каждого участника. На пути SDK вы получаете более полную версию тех же данных без дополнительной работы.

## Требуемые действия пользователя

Некоторые транзакции не могут завершиться, пока конечный пользователь не выполнит действие. Правило может требовать сессию верификации, а для перевода по Travel Rule может понадобиться подтверждение того, что целевой кошелёк действительно принадлежит пользователю.
Оба случая отображаются в одном месте: в блоке `action_required` ответа на отправку (и в каждом последующем чтении транзакции):

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

| Тип | Когда появляется | Что делает пользователь |
| - | - | - |
| `verification_session` | [Действие правила](https://docs.didit.me/ru/transaction-monitoring/rules.md) потребовало дополнительной верификации пользователя, и транзакция перешла в `AWAITING_USER`. | Проходит размещённую сессию верификации (например, повторную биометрическую проверку), которую запускает ваше приложение. См. [Запуск сессии верификации](#запуск-сессии-верификации) ниже. |
| `wallet_ownership` | Перевод по Travel Rule требует, чтобы конечный пользователь подтвердил контроль над кошельком. Либо получатель был [объявлен некастодиальным кошельком](#объявление-некастодиального-кошелька) (`UNCONFIRMED_OWNERSHIP`), либо исходящий перевод не нашёл VASP-контрагента. При этом в ваших [настройках Travel Rule](https://docs.didit.me/ru/transaction-monitoring/travel-rule.md#настройка) включён `auto_wallet_verification`. Didit автоматически создаёт сессию [виджета подтверждения владения кошельком](https://docs.didit.me/ru/transaction-monitoring/wallet-ownership-widget.md), привязанную к транзакции. | Подтверждает контроль над кошельком подписью сообщения (в поддерживаемой сети), скриншотом или самодекларацией, в зависимости от ваших настроек Travel Rule. Но не через тест Satoshi для автоматически созданной сессии. SDK запускает её автоматически. |

Если оба варианта могут относиться к одной транзакции, приоритет у `wallet_ownership`, поскольку отсутствие подтверждения владения блокирует сам обмен по Travel Rule.
`action_required` становится `null`, когда ожидающее действие разрешено.
Поле `remediation` сохранено для обратной совместимости. Каноническим блоком является `action_required`.

### Контракт автозапуска

`autoLaunchAction` (по умолчанию `true`) автоматически запускает только действие `wallet_ownership`:

* **Веб** - открывается во встроенном модальном окне SDK. Завершение отслеживается через сообщения с проверкой origin и ограниченный опрос транзакции (никогда не зависит только от одного события).
* **iOS / Android** (а также обёртки React Native и Flutter, которые передают работу нативным SDK) - показывается как **нативный интерфейс подтверждения владения кошельком** SDK (SwiftUI / Jetpack Compose), если `useNativeWalletOwnership` имеет значение `true` (по умолчанию). При необходимости используется `SFSafariViewController` или Chrome Custom Tab, а в крайнем случае браузер по умолчанию. После закрытия потока или возврата приложения на передний план SDK автоматически повторно читает транзакцию.

Действие `verification_session` никогда не запускается автоматически: оно всегда возвращается в `action_required`, и ваше приложение запускает его через собственную интеграцию верификации Didit.

> [!WARNING]
>
> Виджет подтверждения владения кошельком никогда не должен отображаться во встроенном мобильном веб-представлении. Глубокие ссылки кошельков не могут выйти из него, и поток подтверждения заходит в тупик.
> При автозапуске SDK уже использует подходящий интерфейс браузера. Сохраните это правило, если вы обрабатываете `url` самостоятельно.
> Подробнее см. в [руководстве по виджету подтверждения владения кошельком](https://docs.didit.me/ru/transaction-monitoring/wallet-ownership-widget.md#откройте-возвращённый-url).

После того как автоматически запущенный SDK поток подтверждения владения кошельком завершится или будет закрыт, SDK повторно читает транзакцию и передаёт обновлённый результат в ваш callback `onActionCompleted`.
Действие `verification_session` не входит в этот цикл, поскольку его запускает ваше приложение. См. следующий раздел.

### Запуск сессии верификации

Когда `action_required.type` имеет значение `verification_session`, прочитайте блок из `result.actionRequired` и передайте его в существующую интеграцию верификации Didit:

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

Когда ваш процесс верификации завершится, вызовите `GET /v1/transactions/{transaction_id}/` (или дождитесь [webhook транзакции](#webhook)), чтобы получить обновлённый статус транзакции.

### Пример: повторная биометрическая проверка для крупного вывода средств

Это наиболее эффективный сценарий: рискованные транзакции (крупные суммы, новые получатели, новые устройства) требуют повторной биометрической проверки того же человека, которого вы верифицировали при онбординге.

Захват учётной записи выглядит именно так: верный пароль, верный код 2FA (выманенный фишингом или перехваченный в результате SIM-свопа) и вывод на кошелёк, который вы никогда не видели.
К этому моменту все защиты на основе учётных данных уже не сработали.
Но лицо - нет: злоумышленник не может пройти проверку живости по селфи, которое должно совпасть с лицом, снятым при онбординге настоящего владельца в рамках KYC.

**1. Создайте правило один раз в консоли**

**Transactions → Rules → Create Rule**: условие `amount gte 5000`, действие **Change status → `AWAITING_USER`**, и на этом действии выберите ваш сценарий **Биометрическая аутентификация**.
Добавьте любые другие условия риска, которые вам нужны, например контрагента, с которым проводится первая транзакция, новый отпечаток устройства или смену страны IP-адреса, как дополнительные условия или дополнительные правила.

**2. Отправьте вывод как обычно**

Код отправки не меняется. Когда срабатывает правило, ответ возвращается со значением `status: "AWAITING_USER"` и блоком `action_required` типа `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. Запустите повторную проверку**

Передайте `session_token` (или `url`) в вашу существующую интеграцию верификации Didit точно так же, как в разделе [Запуск сессии верификации](#запуск-сессии-верификации).
Поскольку сессия создаётся для того же `vendor_data`, что и субъект транзакции, у Didit уже есть проверенный эталонный снимок лица пользователя из онбординга: одобренное селфи с проверкой живости, фото с чипа паспорта или портрет из документа.
Пользователь делает одно селфи в реальном времени, а Didit выполняет проверку живости и сравнение лиц 1:1 с сохранённым эталоном. Повторное сканирование документа и повторный онбординг не требуются. Небольшое неудобство длится несколько секунд и возникает только для транзакций, которые этого заслуживают.

**4. Определите итог**

Когда сессия завершится, транзакция автоматически переоценивается: `APPROVED`, если повторная проверка пройдена, или `DECLINED`, если нет. В любом случае отправляется webhook `transaction.status.updated` (`reason: remediation_completed` в истории транзакции).
Ваше приложение получает результат через callback в стиле `onActionCompleted`, через `GET /v1/transactions/{transaction_id}/` или через webhook.

Так получается, потому что KYC-онбординг, повторная биометрическая проверка и решение по транзакции работают на одной платформе: лицо, снятое при онбординге, служит эталоном для повышенной проверки, граф устройств связывает сессию с транзакцией, а движок правил объединяет всё это. Никаких сторонних интеграционных связок, экспорта изображений лиц или отдельного договора на биометрию.
Повторная проверка тарифицируется как стандартная функция «Биометрическая аутентификация» ([\$0.10](https://docs.didit.me/ru/getting-started/pricing.md)), и только в том случае, когда она действительно выполняется.

## Web SDK

`submitTransaction` уже доступен в синглтоне 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);
```

Результат имеет вид `{ transactionId, status, travelRuleStatus?, actionRequired? }`.
В payload на уровне SDK вы передаёте `category`, `details` и `customProperties`; SDK сам сопоставляет их с псевдонимами протокола (`type`, `info`, `props`).

Ошибки типизированы через `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

> [!NOTE]
>
> Нативный API `submitTransaction` входит в состав iOS и Android SDK 4.1.0, а также релизов React Native и Flutter, которые используют эту версию.
> В отличие от Web SDK, в нативных payload напрямую используются имена полей протокола: `type`, `info` и `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
```

Каждый SDK также предоставляет `getTransaction(transactionToken, transactionId)` для повторного чтения транзакции с тем же токеном. SDK вызывают его автоматически после запуска потока действий, а вы можете вызвать его самостоятельно в любой момент.

На мобильных устройствах механизм передачи отпечатка устройства уже используется для всех сетевых вызовов SDK, поэтому отправки транзакций автоматически передают те же данные об устройстве.

## Без SDK

Всё, что делают SDK, выполняется посредством обычных HTTPS-запросов, и ничто здесь не требует SDK.
Есть два равноценных варианта интеграции.

**Только серверная сторона (без токенов вообще).** Отправьте транзакцию через [`POST /v3/transactions/`](https://docs.didit.me/ru/management-api/transactions/create) с вашего бэкенда, используя API-ключ. Ответ (и каждый последующий `GET /v3/transactions/{transaction_id}/`) содержит тот же блок `action_required`. Также можно передавать явный контекст `device` для каждого участника, поскольку собрать его за вас некому: SDK в этом варианте нет.

**Клиентская сторона с собственным HTTP-стеком.** Выпустите токен через [`POST /v3/transactions/sdk-token/`](https://docs.didit.me/ru/management-api/transactions/sdk-token), затем вызывайте [`POST /v1/transactions/`](https://docs.didit.me/ru/management-api/transactions/submit-sdk) и [`GET /v1/transactions/{transaction_id}/`](https://docs.didit.me/ru/management-api/transactions/get-sdk) с устройства, передав заголовок `X-Transaction-Token`. Та же привязка к субъекту и то же серверное определение IP и user-agent действуют и здесь.

Далее обрабатывайте `action_required` самостоятельно:

| `action_required.type` | Что делать |
| - | - |
| `wallet_ownership` | Передайте `url` пользователю и откройте его в **реальном браузерном окне**: в обычной вкладке браузера, через `SFSafariViewController` или Chrome Custom Tabs на мобильных устройствах либо вне приложения, по ссылке из письма или с помощью QR-кода. Никогда не отображайте его во встроенном webview: глубокие ссылки кошельков не могут из него выйти. |
| `verification_session` | Отправьте пользователя по `url`: это стандартная размещённая страница верификации Didit. Или передайте `session_token` в любую интеграцию Didit для верификации, которая у вас уже работает. |

И определяйте завершение без клиентских обратных вызовов:

* **Webhooks (рекомендуется).** `transaction.status.updated` срабатывает, когда ожидающее действие завершает обработку транзакции, а `travel_rule.status.updated` срабатывает при каждом изменении состояния обмена. Оба события содержат достаточно данных, чтобы обновить ваши записи без опроса.
* **Опрос.** Перечитывайте транзакцию (`/v3/` с вашего бэкенда или `/v1/` с токеном, пока он действителен): `action_required` возвращается к `null` после завершения, а `status` и `travel_rule.status` содержат результат.

В этом дизайне нет зависимостей, которые работают только один раз: виджет, сессия верификации и ваш бэкенд сходятся к одному и тому же сохраняемому состоянию транзакции, поэтому после закрытия вкладки или пропуска webhook состояние всегда можно восстановить повторным чтением транзакции.

## Webhook

Вашему бэкенду не нужно, чтобы приложение передавало результат.
Когда отправленная транзакция требует действия пользователя, [webhook транзакции](https://docs.didit.me/ru/transaction-monitoring/webhooks.md) (`transaction.created` или `transaction.status.updated`) содержит тот же блок `action_required`. Ваши системы узнают об ожидающем действии, даже если приложение закрыто до завершения потока.
Действие `verification_session` сопровождается статусом `AWAITING_USER`, но действие `wallet_ownership` само по себе не меняет статус транзакции, поэтому ориентируйтесь на `action_required`, а не на статус.
Переходы Travel Rule дополнительно генерируют событие [`travel_rule.status.updated`](https://docs.didit.me/ru/transaction-monitoring/travel-rule#webhooks).

## Следующие шаги

> [card] [Отправка транзакций](https://docs.didit.me/ru/transaction-monitoring/transactions.md)
>
> Серверный API Create Transaction и полный справочник payload.

> [card] [Travel Rule](https://docs.didit.me/ru/transaction-monitoring/travel-rule.md)
>
> Управляемый обмен данными между отправителем и получателем, настройки и жизненный цикл.

> [card] [Виджет подтверждения владения кошельком](https://docs.didit.me/ru/transaction-monitoring/wallet-ownership-widget.md)
>
> Размещённый поток подтверждения контроля, на котором основаны действия wallet\_ownership.

> [card] [Webhook](https://docs.didit.me/ru/transaction-monitoring/webhooks.md)
>
> transaction.created, transaction.status.updated и проверка подписи.

> [card] [Правила](https://docs.didit.me/ru/transaction-monitoring/rules.md)
>
> Действия правил, включая те, что создают сессии верификации.

> [card] [Статусы](https://docs.didit.me/ru/transaction-monitoring/statuses.md)
>
> Что означает AWAITING\_USER в жизненном цикле транзакции.


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