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

> 通过限定作用域的 token，在 Didit 中从应用提交受监控的交易，并自动进行设备分析、防欺骗的主体绑定和钱包所有权验证流程。

Didit SDK 可以直接在终端用户的设备上提交[受监控交易](https://docs.didit.me/zh/transaction-monitoring/transactions.md)，包括[Travel Rule](https://docs.didit.me/zh/transaction-monitoring/travel-rule.md)转账和加密货币筛查。
您的后端生成一个短期有效、限定范围的令牌，其余工作由 SDK 完成。
SDK 会自动附加设备信息。当 Travel Rule 转账需要钱包控制权证明时，SDK 会立即自动启动该流程。
必需的验证会话永远不会被自动启动。它会在响应中返回，由您的应用使用自有的 Didit 验证集成来启动。详见下方的[必需的用户操作](#用户所需操作)。

## 概述

客户端提交基于三项保障：

* **防伪造的主体绑定。** 令牌在生成时即限定于一个 `vendor_data`（您的用户 ID）。使用该令牌提交的每笔交易，其主体身份都由服务器端根据此绑定强制执行。因此，被篡改的客户端无法代表其他用户提交交易，而且 payload 中无需包含任何凭证。
* **自动设备信息。** SDK 会采集设备指纹并随提交一同发送，Didit 则在服务器端推导出地址和用户代理。组合后的设备上下文会附加到交易的主体上，无需额外的库、单独的设备令牌，也无需您自行实现。它可用于基于设备的[规则](https://docs.didit.me/zh/transaction-monitoring/rules.md)，以及跨会话和交易的设备关联分析。
* **自动启动的钱包所有权证明。** 当已创建的交易要求终端用户证明其控制某个钱包时，响应会包含类型为 `wallet_ownership` 的 `action_required` 块，SDK 会立即打开该流程，并在完成后刷新交易。您无需自行构建轮询循环或链接分发。必需的 `verification_session` 操作则不同：它永远不会被自动启动，而是在同一个块中返回，由您的应用使用自有的 Didit 验证集成来启动。

> [!NOTE]
>
> 当没有终端用户参与时，服务器端的 [`POST /v3/transactions/`](https://docs.didit.me/zh/transaction-monitoring/transactions) 仍是正确的选择，例如后台转账、批量导入，或由您自有系统上报的事件。
> 两种路径会创建相同的交易记录，运行相同的规则，并发出相同的 [Webhook](https://docs.didit.me/zh/transaction-monitoring/webhooks.md)。

## 流程

**1. 生成交易令牌**

您的后端调用 `POST /v3/transactions/sdk-token/`，并提供您的 API 密钥和用户的 `vendor_data`，然后将返回的 `sdk_token` 交给您的应用。

**2. 从设备提交**

应用在 SDK 上调用 `submitTransaction`，该调用会携带 `X-Transaction-Token` 请求头，并自动补充设备信息，将 payload 发送到 `POST /v1/transactions/`。

**3. 处理任何必需的操作**

如果 `action_required.type` 为 `wallet_ownership`，SDK 会自动启动组件，然后通过 `GET /v1/transactions/{transaction_id}/` 刷新交易。
如果为 `verification_session`，SDK 不会启动任何流程。请读取 `action_required`，并使用其 `session_token`（在 Web 端则为 `url`）启动您自有的验证流程。

## 生成交易令牌

请在您的后端使用 `POST /v3/transactions/sdk-token/` 生成令牌。认证方式与其他 `/v3/` 交易接口相同，均使用 API 密钥。

```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 | 可选。使用该令牌允许的最大成功提交次数。若省略，则在有效期内不限次数。 |

`vendorData`、`ttlSeconds` 和 `maxUses` 这几个驼峰命名别名同样可用，与 `/v3/` API 的其他接口一致。

> [!WARNING]
>
> **切勿将您的 API 密钥放在应用中。**
> API 密钥只保留在您的后端，用于生成令牌。只有短期有效的 `sdk_token` 会发送到设备上。
> 请为每个用户会话生成新令牌，而不是重复使用长期有效的令牌。如果已知预期的提交次数，请将 `max_uses` 设置得尽量严格。

## 从设备提交

SDK 会将请求发送到 `POST /v1/transactions/`，并在 `X-Transaction-Token` 请求头中携带 token。
传输载荷采用 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/zh/transaction-monitoring/transactions) 中的等效字段：

| 传输字段 | `/v3/` 对应字段 | 说明 |
| - | - | - |
| `txnId` | `transaction_id` | **必填。** 您为该交易设定的唯一标识符。 |
| `txnDate` | `transaction_at` | ISO-8601 时间戳；默认为当前时间。 |
| `zoneId` | `time_zone` | IANA 时区 ID，例如 `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` | 针对单笔交易的加密货币筛查覆盖设置。 |

响应与 `/v3/` 接口返回的 snake\_case 交易详情相同，并额外包含 `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/zh/transaction-monitoring/travel-rule.md#已声明的自托管钱包) 的运行方式：

* Didit 会完全跳过交易对手 VASP 路由，因为自托管钱包背后不存在 VASP，并直接以 `UNCONFIRMED_OWNERSHIP` 状态创建转账。
* 当您的 [Travel Rule 设置](https://docs.didit.me/zh/transaction-monitoring/travel-rule.md#设置) 中的 `auto_wallet_verification` 开启时（默认开启），系统会自动生成 [钱包所有权组件](https://docs.didit.me/zh/transaction-monitoring/wallet-ownership-widget.md) 的会话，并如上方响应所示，在 `action_required` 中返回。
* SDK 会自动拉起该组件。用户证明对钱包的控制权后，转账会执行姓名匹配，并推进至 `COMPLETED`；若姓名与钱包地址簿持有人不一致，则推进至 `COUNTERPARTY_MISMATCHED_DATA`。

若未做此声明，Didit 会按顺序尝试各个[通道](https://docs.didit.me/zh/transaction-monitoring/travel-rule.md#概述)，只有在找不到交易对手 VASP 时才会回退到所有权验证流程。
只要您的产品已经知道提现目标是用户自己的钱包，就应当做此声明。这样可以省去一次失败的路由尝试，并让用户立即进入证明流程。

### 主体约束

以下三项始终由 token 决定，而非由载荷决定：

* 创建交易所属的应用。
* 主体的身份：载荷中的 `subject.externalUserId` 会被忽略，并替换为 token 的 `vendor_data`。
* 主体的设备上下文（见下文）；服务端推导的字段在与 `subject.device` 中提供的内容冲突时以服务端为准。

该接口返回 `401` 表示 token 被拒绝：缺失、未知、已撤销、超出其 `max_uses`，或已过期。
过期 token 的响应错误详情中始终包含 `expired` 一词，SDK 正是据此区分 `expired_token` 与 `invalid_token` 错误。

### 读取交易详情

`GET /v1/transactions/{transaction_id}/` 使用相同的 `X-Transaction-Token` 认证方式，返回相同的详情。
SDK 会在已拉起的操作流程完成或其窗口关闭后自动调用该接口，因此最终状态不依赖于单个事件或通知。
只要 token 仍然有效，您随时都可以自行调用此接口，以重新检查 `status` 和 `action_required`；在您的后端，[`GET /v3/transactions/{transaction_id}/`](https://docs.didit.me/zh/transaction-monitoring/transactions) 会返回相同的记录。

## 自动设备情报

每次提交都会携带设备情报，您无需进行任何额外集成：

* SDK 会收集一份注重隐私的设备指纹（`didit-fp-v2` 结构：平台与 navigator 信号、持久设备 ID、机器人分数以及复合哈希），并随请求一同发送。
* Didit 在服务端根据连接本身推导 IP 地址、用户代理和 accept-language，因此无法通过载荷伪造这些信息。

合并后的结果会被规范化为主体参与方的设备上下文，并将其插入或更新到设备图谱中（与 KYC 会话写入的是同一图谱）。
由此，交易可以在您的全部用户之间按设备进行关联（“按设备关联”），设备与网络信号会输入到[规则](https://docs.didit.me/zh/transaction-monitoring/rules.md)和 [IP 信息补全](https://docs.didit.me/zh/transaction-monitoring/ip-enrichment.md)中，并显示在[控制台](https://docs.didit.me/zh/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/zh/transaction-monitoring/rules.md)要求用户进行额外验证，交易已进入 `AWAITING_USER`。 | 完成托管验证会话（例如生物识别复核），由您的应用发起。参见下文的[启动验证会话](#启动验证会话)。 |
| `wallet_ownership` | Travel Rule 转账需要终端用户证明其对钱包的控制权：目标钱包已被[声明为自托管钱包](#声明自托管钱包)（`UNCONFIRMED_OWNERSHIP`），或出站转账未找到对应的 VASP 交易对手，并且您的 [Travel Rule 设置](https://docs.didit.me/zh/transaction-monitoring/travel-rule.md#设置)中已启用 `auto_wallet_verification`。Didit 会自动创建一个与该交易绑定的[钱包所有权组件](https://docs.didit.me/zh/transaction-monitoring/wallet-ownership-widget.md)会话。 | 根据您的 Travel Rule 设置，通过消息签名（需支持的链）、截图或自我声明来证明对钱包的控制权，但自动创建的会话不允许使用 Satoshi 测试证明。SDK 会自动为您启动此操作。 |

如果两种情况可能同时适用于同一笔交易，则以 `wallet_ownership` 为准，因为缺少所有权证明会阻止 Travel Rule 信息交换本身。
待处理操作解决后，`action_required` 会变为 `null`。
现有的 `remediation` 字段为向后兼容而保留；`action_required` 是标准的数据块。

### 自动启动约定

`autoLaunchAction`（默认为 `true`）仅自动启动 `wallet_ownership` 操作：

* **Web**：在 SDK 自带的浮层弹窗中打开，通过经过来源校验的消息传递，并结合有上限的交易轮询来检测完成状态（绝不依赖单一事件）。
* **iOS / Android**（以及委托给原生 SDK 的 React Native 和 Flutter 封装）：当 `useNativeWalletOwnership` 为 `true`（默认值）时，以 SDK 的**原生钱包所有权体验**（SwiftUI / Jetpack Compose）呈现；如果无法使用原生钱包所有权体验，则回退到 `SFSafariViewController` / Chrome Custom Tab，最后使用默认浏览器。流程关闭或应用回到前台后，SDK 会自动重新读取交易。

`verification_session` 操作从不自动启动：它始终在 `action_required` 中返回，由您的应用使用自己的 Didit 验证集成来发起。

> [!WARNING]
>
> 钱包所有权组件绝不能在嵌入式移动端 webview 中渲染：钱包深层链接无法逃逸出该容器，证明流程会因此中断。
> SDK 在自动启动时已使用正确的浏览器界面；如果您自行处理 `url`，请保持这一规则。
> 详情参见[钱包所有权组件指南](https://docs.didit.me/zh/transaction-monitoring/wallet-ownership-widget.md#打开返回的-url)。

SDK 自动启动的钱包所有权流程完成或关闭后，会重新读取交易，并将刷新后的结果传递给您的 `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"`，以及类型为 `verification_session` 的 `action_required` 块：

```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`，无论哪种结果都会发送 `transaction.status.updated` Webhook（交易时间线中的 `reason: remediation_completed`）。
您的应用可通过类似 `onActionCompleted` 的回调、`GET /v1/transactions/{transaction_id}/` 或 Webhook 获取结果。

这之所以可行，是因为 KYC 入驻、生物识别再验证和交易决策都运行在同一个平台上：入驻时采集的人脸是升级验证的参考基准，设备图谱将会话与交易关联起来，规则引擎把它们串联在一起。无需第三方厂商对接，不导出人脸图像，也无需单独签订生物识别合同。
只有在实际执行时，再验证才会按标准的生物识别认证功能计费（[\$0.10](https://docs.didit.me/zh/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? }`。
在 SDK 层的 payload 中，您需要填写 `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]
>
> 原生 `submitTransaction` API 随 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)` 方法，用同一个 token 重新读取交易。启动操作流程后，SDK 会自动为您调用该方法，您也可以随时自行调用。

在移动端，设备指纹传输已应用于所有 SDK 网络调用，因此交易提交会自动携带相同的设备信息。

## 不使用 SDK

直接使用 HTTPS 即可完成 SDK 的全部功能，无需依赖 SDK。
提供两种等效的集成方式：

**仅服务端（无需任何 token）。** 使用您的 API 密钥，从后端调用 [`POST /v3/transactions/`](https://docs.didit.me/zh/management-api/transactions/create) 提交交易。响应以及之后每次 `GET /v3/transactions/{transaction_id}/` 的返回都会包含相同的 `action_required` 块。由于没有 SDK 为您收集设备信息，您还可以为每个参与方显式传入 `device` 上下文。

**客户端使用自有 HTTP 栈。** 先通过 [`POST /v3/transactions/sdk-token/`](https://docs.didit.me/zh/management-api/transactions/sdk-token) 生成 token，然后在设备端使用 `X-Transaction-Token` 请求头调用 [`POST /v1/transactions/`](https://docs.didit.me/zh/management-api/transactions/submit-sdk) 和 [`GET /v1/transactions/{transaction_id}/`](https://docs.didit.me/zh/management-api/transactions/get-sdk)。主体绑定以及服务端的 IP 与 User-Agent 推导同样适用。

然后，请自行处理 `action_required`：

| `action_required.type` | 处理方式 |
| - | - |
| `wallet_ownership` | 将 `url` 交付给用户，并在**真实浏览器界面**中打开：网页端使用普通标签页，移动端使用 `SFSafariViewController` / Chrome Custom Tabs，或通过邮件链接或二维码在应用外传递。切勿在嵌入式 webview 中渲染该链接，因为钱包深链无法跳出 webview。 |
| `verification_session` | 将用户引导至 `url`（即 Didit 标准托管验证页面），或将 `session_token` 传给您已在使用的任何 Didit 验证集成。 |

无需任何客户端回调即可检测完成状态：

* **Webhook（推荐）。** 待处理操作使交易得出结果时，会触发 `transaction.status.updated`；每次 Travel Rule 交换状态发生变化时，会触发 `travel_rule.status.updated`。两者都包含足够的信息，可在无需轮询的情况下更新您的记录。
* **轮询。** 重新读取交易（后端使用 `/v3/`，或在 token 有效期内使用 `/v1/` 配合 token）。操作完成后，`action_required` 会恢复为 `null`，`status` 和 `travel_rule.status` 则会给出结果。

该设计中不存在只能单次使用的依赖：组件、验证会话和您的后端最终都汇聚到同一份持久化的交易状态上，因此关闭的标签页或丢失的 webhook 总能通过重新读取交易来恢复。

## Webhook

您的后端无需依赖应用来回传状态。
提交的交易需要用户操作时，[交易 webhook](https://docs.didit.me/zh/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/zh/transaction-monitoring/travel-rule#webhook)。

## 后续步骤

> [card] [提交交易](https://docs.didit.me/zh/transaction-monitoring/transactions.md)
>
> 服务端 Create Transaction API 及完整 payload 参考。

> [card] [Travel Rule](https://docs.didit.me/zh/transaction-monitoring/travel-rule.md)
>
> 托管式汇款人与收款人交换、设置及生命周期。

> [card] [钱包所有权组件](https://docs.didit.me/zh/transaction-monitoring/wallet-ownership-widget.md)
>
> wallet\_ownership 操作背后的托管式控制权证明流程。

> [card] [Webhook](https://docs.didit.me/zh/transaction-monitoring/webhooks.md)
>
> transaction.created、transaction.status.updated 以及签名验证。

> [card] [规则](https://docs.didit.me/zh/transaction-monitoring/rules.md)
>
> 规则操作，包括用于创建验证会话的操作。

> [card] [状态](https://docs.didit.me/zh/transaction-monitoring/statuses.md)
>
> AWAITING\_USER 在交易生命周期中的含义。


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