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

# Flutter 身份验证 SDK：Dart 移动端 KYC

> Didit 提供跨平台 Flutter Dart KYC SDK，支持原生 iOS 与 Android、生物识别活体检测和 NFC 护照验证。完整 KYC 每次 $0.33 起，每月 $10 免费，无需合同。



Flutter SDK 集成提示
        将提示词复制到剪贴板



      
      

\<pre>

\`Integrate Didit identity verification into my Flutter app using the native SDK.

\## Requirements
\- Flutter 3.3+, Dart 3.11+
\- iOS 13.0+ (NFC requires iOS 15+)
\- Android API 23+ (Android 6.0)

\## Installation
flutter pub add didit_sdk

iOS — the plugin picks the native variant from the $DiditSdkIosVariant Ruby global in ios/Podfile (or the DIDIT_SDK_IOS_VARIANT env var); valid values are 'all' (default), 'core', 'autodetection', 'nfc'. At the top of ios/Podfile:
$DiditSdkIosVariant = 'all'
platform :ios, ['all', 'nfc'].include?($DiditSdkIosVariant) ? '15.0' : '13.0'
Inside the target block, add the matching subspec:
pod 'DiditSDK/All', :podspec => 'https://raw.githubusercontent.com/didit-protocol/sdk-ios/4.13.2/DiditSDK.podspec'
The tag in that URL must equal the native iOS SDK version the didit_sdk release pins (4.13.2 pins 4.13.2); do not use 'main', because the plugin requires that exact version and pod install fails when the URL resolves to a different one.
'all' and 'nfc' require platform :ios, '15.0'; 'core' and 'autodetection' run on iOS 13.0+.
Then: cd ios && pod install
Info.plist must include NSCameraUsageDescription, NSMicrophoneUsageDescription, NSPhotoLibraryUsageDescription (required) and NFCReaderUsageDescription if using NFC.

Android — for no-NFC builds add to android/gradle.properties:
diditSdkAndroidNfcEnabled=false
For NFC-enabled (default), add to android/app/build.gradle.kts inside android block:
packaging {
  resources {
      pickFirsts += "META-INF/versions/9/OSGI-INF/MANIFEST.MF"
  }
}

\## Backend: Create Session (your server)
POST https://verification.didit.me/v3/session/
Headers: { "x-api-key": DIDIT_API_KEY, "Content-Type": "application/json" }
Body: { "workflow_id": DIDIT_WORKFLOW_ID, "vendor_data": "user-id" }
Response: { "session_id": "uuid", "session_token": "short-token", "url": "https://verify.didit.me/session/short-token", "status": "Not Started" }
Send session_token to the Flutter app (never expose DIDIT_API_KEY to the device).

\## Integration
import 'package:didit_sdk/sdk_flutter.dart';

// Option A: With session token from your backend (recommended)
final result = await DiditSdk.startVerification('session-token-from-backend');

// Option B: With workflow ID (SDK creates session internally)
final result = await DiditSdk.startVerificationWithWorkflow('your-workflow-id',
vendorData: 'user-123',
);

// Handle result
switch (result) {
case VerificationCompleted(:final session):
if (session.status == VerificationStatus.approved) { /* grant access */ }
case VerificationCancelled():
// User dismissed the verification flow
case VerificationFailed(:final error):
// SDK error — show error.message
}

\## Result Statuses (VerificationStatus enum)
\- VerificationStatus.approved — user identity verified, grant access
\- VerificationStatus.declined — verification failed, show retry or contact support
\- VerificationStatus.pending  — needs manual review by your compliance team

\## Error Types (VerificationErrorType enum)
sessionExpired, networkError, cameraAccessDenied, notInitialized, apiError, retryBlocked, unknown

\## Advanced Session Parameters
contact_details, expected_details, and metadata are only available via the Session Token method (your backend creates the session). The Dart startVerificationWithWorkflow API only accepts workflowId, vendorData, and config.

\## Full Verification Results
The SDK only returns the status. Full decision data (document fields, face match scores, AML hits, etc.) arrives via webhook to your backend.
Set up webhooks: https://docs.didit.me/integration/webhooks

\## Rate Limits (session creation)
\- 600 session-create requests / minute per x-api-key (see https://docs.didit.me/integration/rate-limiting)
\- On 429, retry honouring Retry-After / X-RateLimit-* headers

\## Environment Variables (backend only — never expose in the app)
\- DIDIT_API_KEY — from Didit Console > API & Webhooks
\- DIDIT_WORKFLOW_ID — from Didit Console > Workflows

\## Docs
\- Flutter SDK: https://docs.didit.me/integration/native-sdks/flutter-sdk
\- GitHub: https://github.com/didit-protocol/sdk-flutter
\- pub.dev: https://pub.dev/packages/didit_sdk
\- Webhooks: https://docs.didit.me/integration/webhooks
\- API Reference: https://docs.didit.me/api-reference/overview`

\</pre>



      
      

粘贴到

这是一个跨平台的 Flutter 插件，封装了原生 iOS 和 Android SDK，并提供统一的 Dart API，用于身份验证。> [card] [GitHub 仓库](https://github.com/didit-protocol/sdk-flutter)
>
> 在 GitHub 上查看源代码和示例> [card] [pub.dev 包](https://pub.dev/packages/didit_sdk)
>
> didit\_sdk> [!NOTE]
>
> ### **原生 SDK**：这是 Flutter 应用推荐的集成方式。该 SDK 通过平台通道封装了原生 iOS 和 Android SDK，可提供最佳的用户体验、经过优化的摄像头处理，以及完整的 NFC 支持。***## 要求| 要求 | 最低版本 |
| - | - |
| Flutter | 3.3+ |
| Dart | 3.11+ |### 平台要求| 平台 | 最低版本 | 说明 |
| - | - | - |
| iOS | 13.0+ | NFC 护照读取需要 iOS 15.0+ |
| Android | API 23+ (6.0) | Java 17+ |> [!NOTE]
>
> **生物识别同意需要原生 SDK 4.10.0 或更高版本。** 对于适用 Illinois、Texas 或 Colorado 法律的用户，Didit 可以在首次人脸采集前请求用户明确给予[生物识别同意](https://docs.didit.me/zh/getting-started/biometric-consent.md)。锁定 iOS 和 Android 原生 SDK 4.10.0 或更高版本的 Flutter SDK 版本会显示该步骤。较早版本无法显示该步骤：其会话照常运行，但决策结果会将同意标记为未收集（`biometric_consent.reason: "BIOMETRIC_CONSENT_UNSUPPORTED_SDK"`）。如果您在这些州为用户进行验证，请升级。***## 安装```bash theme={null}
flutter pub add didit_sdk
```或者手动将其添加到 `pubspec.yaml` 中：```yaml theme={null}
dependencies:
  didit_sdk: ^4.13.2
```然后运行：```bash theme={null}
flutter pub get
```### 原生 SDK 变体`didit_sdk` 内置完整的原生 SDK。
如需更小的二进制文件，可以改为依赖变体包。变体包公开的 Dart API 完全相同，并在两个平台上锁定与之匹配的原生 SDK 版本：| 包 | 自动采集 | NFC 护照读取 | 最低 iOS 版本 |
| - | - | - | - |
| `didit_sdk`（默认） | 是 | 是 | 15.0 |
| `didit_sdk_core` | 否 | 否 | 13.0 |
| `didit_sdk_autodetection` | 是 | 否 | 13.0 |
| `didit_sdk_nfc` | 否 | 是 | 15.0 |```yaml theme={null}
dependencies:
  didit_sdk_core: ^4.13.2
```请只依赖以上其中一个包，并从您选择的包中导入，例如 `package:didit_sdk_core/sdk_flutter.dart`。### iOS 设置本插件同时支持 Flutter 的两种 iOS 依赖管理工具：Swift Package Manager 和 CocoaPods。#### Swift Package Manager如果您的 Flutter 版本已启用 Swift Package Manager 支持（在 Flutter 3.24+ 上通过 `flutter config --enable-swift-package-manager` 手动开启），插件会自动以 Swift 包的形式引入。
无需配置 Podfile，原生 DiditSDK 依赖会自行从 GitHub 解析。
请在 Xcode 中（Runner 目标和项目）将应用的 iOS 部署目标设置为所选包的最低版本：`didit_sdk` 和 `didit_sdk_nfc` 为 15.0，`didit_sdk_core` 和 `didit_sdk_autodetection` 为 13.0。#### CocoaPods对于 `didit_sdk`，插件会根据 `$DiditSdkIosVariant` 选择原生 iOS SDK 变体。它是一个 Ruby 全局变量，需在应用的 `ios/Podfile` 中设置（DiditSDK pod 不在 CocoaPods trunk 上）：```ruby theme={null}
# Pick one: 'all', 'core', 'autodetection', 'nfc'
$DiditSdkIosVariant = 'all'

didit_sdk_ios_deployment_target = ['all', 'nfc'].include?($DiditSdkIosVariant) ? '15.0' : '13.0'
platform :ios, didit_sdk_ios_deployment_target

didit_sdk_ios_pod = case $DiditSdkIosVariant
                    when 'all' then 'DiditSDK/All'
                    when 'core' then 'DiditSDK/Core'
                    when 'autodetection' then 'DiditSDK/AutoDetection'
                    when 'nfc' then 'DiditSDK/NFC'
                    end
didit_sdk_ios_podspec = 'https://raw.githubusercontent.com/didit-protocol/sdk-ios/4.13.2/DiditSDK.podspec'

target 'Runner' do
  use_frameworks!

  pod didit_sdk_ios_pod, :podspec => didit_sdk_ios_podspec

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
    target.build_configurations.each do |config|
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = didit_sdk_ios_deployment_target
    end
  end
end
```如果您使用变体包（`didit_sdk_core`、`didit_sdk_autodetection`、`didit_sdk_nfc`），它会直接锁定其原生子规格，并忽略 `$DiditSdkIosVariant`。请保持 Podfile 中的 `pod` 行和部署目标与该变体一致（例如使用 `pod 'DiditSDK/Core', :podspec => ...` 和 `platform :ios, '13.0'`）。> [!NOTE]
>
> `all` 和 `nfc` 变体需要 iOS 15.0+ 的部署目标；`core` 和 `autodetection` 可以面向 iOS 13.0。切换变体时，请先清理 CocoaPods（`rm -rf Pods Podfile.lock`），以免复用之前的 SDK 变体。> [!WARNING]
>
> podspec URL 中的标签必须与您的 `didit_sdk` 版本所锁定的原生 iOS SDK 版本一致（`4.13.2` 对应原生 `4.13.2`）。如果 URL 指向较旧的标签，`pod install` 将无法完成解析：CocoaPods 会找到一个声明了较旧版本的 podspec，而插件要求版本完全匹配。升级 `didit_sdk` 时，请同时将此 URL 中的标签更新为相同版本。### Android 设置默认情况下，插件依赖包含 NFC 的完整 Android SDK。若要在不含 NFC 的情况下构建，请在 `android/gradle.properties` 中添加以下内容：```properties theme={null}
diditSdkAndroidNfcEnabled=false
```此设置会将依赖从 `me.didit:didit-sdk` 切换为 `me.didit:didit-sdk-core`，从而移除 NFC 读取模块及其 JMRTD/SCUBA/BouncyCastle 依赖。启用 NFC（默认）时，请在 `android/app/build.gradle.kts` 中添加以下打包规则，以解决 BouncyCastle 带来的重复元数据文件问题：```kotlin theme={null}
android {
    packaging {
        resources {
            pickFirsts += "META-INF/versions/9/OSGI-INF/MANIFEST.MF"
        }
    }
}
```如果没有此规则，构建会因 `mergeDebugJavaResource` 错误而失败。当 `diditSdkAndroidNfcEnabled=false` 时，不需要此规则。> [!NOTE]
>
> 自插件 3.4.0 起，Android 原生 SDK 从远程 GitHub Maven 仓库解析，您的应用无需手动配置 Maven。***## 权限### iOS将以下键添加到应用的 `Info.plist` 中：| 权限 | Info.plist 键 | 说明 | 是否必需 |
| - | - | - | - |
| 相机 | `NSCameraUsageDescription` | 证件扫描和人脸验证 | 是 |
| 麦克风 | `NSMicrophoneUsageDescription` | 用于活体检测的视频录制 | 是 |
| 相册 | `NSPhotoLibraryUsageDescription` | 从设备相册上传证件 | 是 |
| NFC | `NFCReaderUsageDescription` | 读取护照和身份证中的 NFC 芯片 | 使用 NFC 时必需 |
| 位置信息 | `NSLocationWhenInUseUsageDescription` | 用于防欺诈的地理位置 | 可选 |> [!WARNING]
>
> 如果缺少任何必需的 iOS 隐私键，SDK 一旦尝试访问该受保护资源，iOS 就会终止应用。例如，缺少 `NSCameraUsageDescription` 会导致用户点击证件相机的拍照按钮时应用崩溃。#### NFC 配置要启用对带芯片护照和身份证的 NFC 读取：1) **在 Xcode 中添加 NFC 能力**：
   * 选择您的 target > **Signing & Capabilities** > **+ Capability** > **Near Field Communication Tag Reading**

2) 在 `Info.plist` 中**添加 ISO7816 标识符**：```xml theme={null}
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
    <string>D23300000045737445494420763335</string>
    <string>A0000002471001</string>
    <string>A0000002472001</string>
    <string>00000000000000</string>
    <string>D392F000260100000001</string>
</array>
````D392F000260100000001` 让 SDK 能够通过 [JPKI](https://docs.didit.me/zh/core-technology/nfc-verification/overview.md#日本个人编号卡（jpki）) 读取日本 My Number Card。若没有它，iOS 不会向 SDK 报告该卡片信息。My Number Card 读取尚未在生产环境中启用，且该标识符对其他证件没有任何影响，因此您现在即可添加。3) 添加启用了 NFC 标签读取的授权文件：```xml theme={null}
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
    <string>TAG</string>
</array>
```请确保应用的描述文件包含 NFC Tag Reading 能力。如果您的 Apple Developer 账户中未为该 bundle ID 配置 NFC，Xcode 将因缺少 `com.apple.developer.nfc.readersession.formats` 授权而签名失败。对于 `core` 或 `autodetection` 变体（`$DiditSdkIosVariant`）以及 `didit_sdk_core` / `didit_sdk_autodetection` 包，不需要此 NFC 配置，因为它们都不读取芯片。### Android以下权限在 SDK 的 `AndroidManifest.xml` 中声明，并会自动合并：| 权限 | 说明 | 是否必需 |
| - | - | - |
| `INTERNET` | 用于 API 通信的网络访问 | 是 |
| `ACCESS_NETWORK_STATE` | 检测网络是否可用 | 是 |
| `CAMERA` | 证件扫描和人脸验证 | 是 |
| `NFC` | 读取护照和身份证中的 NFC 芯片 | 使用 NFC 时必需 |相机和 NFC 硬件功能被声明为可选（`android:required="false"`），因此即使设备不具备这些功能，您的应用也可以安装。当 `diditSdkAndroidNfcEnabled=false` 时，SDK 不会添加 Android NFC 权限和功能。#### 运行时权限SDK 会自动处理 Android 运行时权限请求。当用户到达需要相机权限的步骤时：1) 如果尚未授予相机权限，SDK 会提示用户授权
2) 如果用户**拒绝**该权限，将显示带有 **Try Again** 按钮的错误消息
3) 如果用户**授予**该权限，验证流程继续进行调用 `startVerification()` 之前，您无需在应用代码中请求相机权限，SDK 会在内部处理。***## 快速开始```dart theme={null}
import 'package:didit_sdk/sdk_flutter.dart';

// Start verification with a session token from your backend
final result = await DiditSdk.startVerification('your-session-token');

switch (result) {
  case VerificationCompleted(:final session):
    if (session.status == VerificationStatus.approved) {
      print('Identity verified!');
    }
  case VerificationCancelled():
    print('User cancelled');
  case VerificationFailed(:final error):
    print('Error: ${error.message}');
}
```***## 集成方式SDK 支持两种集成方式：### 方法 1: 会话 token (推荐用于生产环境)使用[创建验证会话 API](https://docs.didit.me/zh/sessions-api/create-session.md)在您的后端创建会话，然后将 token 传递给 SDK：```dart theme={null}
import 'package:didit_sdk/sdk_flutter.dart';

// Your backend creates a session and returns the token
final sessionToken = await yourBackend.createVerificationSession(userId);

// Pass the token to the SDK
final result = await DiditSdk.startVerification(sessionToken);
```这种方式让您可以完全控制以下内容：- 将会话与您的用户关联（`vendor_data`）
- 设置自定义元数据
- 为每次会话配置回调### 方法 2: 工作流 ID (更简单的集成)对于更简单的集成，SDK 可以直接使用您的工作流 ID 创建会话，无需后端：```dart theme={null}
import 'package:didit_sdk/sdk_flutter.dart';

final result = await DiditSdk.startVerificationWithWorkflow(
  'your-workflow-id',
  vendorData: 'user-123',
  config: const DiditConfig(loggingEnabled: true),
);
```> [!NOTE]
>
> 高级会话参数（`contact_details`、`expected_details`、`metadata`）**仅**通过会话 token 方式支持，此时您的后端调用[创建会话 API](https://docs.didit.me/zh/sessions-api/create-session.md)并传入完整参数。将返回的 `session_token` 传递给 `DiditSdk.startVerification()`。***## 配置通过传入 `DiditConfig` 对象自定义 SDK 行为：```dart theme={null}
final result = await DiditSdk.startVerification(
  'your-session-token',
  config: const DiditConfig(
    languageCode: 'es',       // Force Spanish language
    showLanguageSelector: true, // Let the user change language
    fontFamily: 'Avenir',     // Custom font
    loggingEnabled: true,     // Enable debug logging
  ),
);
```调用 `startVerificationWithWorkflow` 时，以命名参数的形式传入 `config`：```dart theme={null}
final result = await DiditSdk.startVerificationWithWorkflow(
  'your-workflow-id',
  vendorData: 'user-123',
  config: const DiditConfig(
    languageCode: 'es',
    showLanguageSelector: true,
    loggingEnabled: true,
  ),
);
```### 配置选项| 属性 | 类型 | 默认值 | 说明 |
| - | - | - | - |
| `languageCode` | `String?` | 设备区域设置 | ISO 639-1 语言代码（例如 `"en"`、`"fr"`、`"ar"`） |
| `showLanguageSelector` | `bool` | `false` | 在欢迎界面显示网页风格的语言选择器 |
| `fontFamily` | `String?` | 系统字体 | 自定义字体族名称（必须已在原生端注册） |
| `loggingEnabled` | `bool` | `false` | 启用 SDK 调试日志 |
| `showCloseButton` | `bool` | `true` | 在验证步骤界面显示关闭（X）按钮 |
| `showExitConfirmation` | `bool` | `true` | 用户尝试退出时显示确认对话框 |
| `closeOnComplete` | `bool` | `false` | 完成后自动关闭验证界面 |
| `defaultDocumentCamera` | `CameraLens?` | 原生默认值（`CameraLens.back`） | 证件拍摄界面打开时使用的镜头 |
| `defaultLivenessCamera` | `CameraLens?` | 原生默认值（`CameraLens.front`） | 人脸（活体）拍摄界面打开时使用的镜头 |
| `showDocumentCameraSwitchButton` | `bool` | `true` | 在证件拍摄界面显示相机切换按钮 |
| `showLivenessCameraSwitchButton` | `bool` | `true` | 在人脸拍摄界面显示相机切换按钮 |### 相机选项相机选项与 [Web SDK 相机选项](https://docs.didit.me/zh/integration/web-sdks/javascript-sdk.md#摄像头选项) 一致。
每个镜头为 `CameraLens.front` 或 `CameraLens.back`。
如果设备没有所请求的镜头，SDK 将使用其第一个可用相机。
在只有一个相机的设备上，相机切换按钮也会隐藏。
将切换选项设为 `false`，即可将用户锁定在该步骤的默认镜头上。```dart theme={null}
config: const DiditConfig(
  defaultLivenessCamera: CameraLens.back,
  showLivenessCameraSwitchButton: false,
),
```> [!NOTE]
>
> **主题与颜色**：颜色、背景和引导页设置需在 Didit 控制台中通过您的[白标设置](https://business.didit.me)进行配置，而不是在 SDK 配置中设置。这样可以确保所有平台的品牌形象保持一致。***## 语言支持SDK 支持 **54 种语言**。如果未指定语言，SDK 将使用设备区域设置，并以英语作为备用语言。```dart theme={null}
// Use device locale (default)
await DiditSdk.startVerification(token);

// Force specific language
await DiditSdk.startVerification(token,
  config: const DiditConfig(languageCode: 'fr'),
);

// Let the user change language on the welcome screen
await DiditSdk.startVerification(token,
  config: const DiditConfig(showLanguageSelector: true),
);
```**[查看所有支持的语言 →](https://docs.didit.me/zh/integration/supported-languages.md)*****## 高级会话参数`contact_details`、`expected_details` 和 `metadata` 等参数**仅**通过会话令牌方式支持。您的后端调用 [`POST /v3/session/`](https://docs.didit.me/zh/sessions-api/create-session) 并完整支持所有参数，然后将返回的 `session_token` 传递给 SDK。```dart theme={null}
// Your backend handles the full session creation:
// POST /v3/session/ with contact_details, expected_details, metadata, etc.
final sessionToken = await yourBackend.createSession(userId);

// The SDK only needs the token
final result = await DiditSdk.startVerification(sessionToken);
```> [!NOTE]
>
> Dart 的 `startVerificationWithWorkflow` 方法仅接受 `workflowId`、`vendorData` 和 `config`。3.4.0 之前的版本曾提供 `contactDetails`、`expectedDetails` 和 `metadata` 参数，但由于 Unilink endpoint 不支持这些参数，它们已被移除。***## 处理结果`startVerification` 和 `startVerificationWithWorkflow` 都返回 `Future<VerificationResult>`。该结果是一个密封类：请使用模式匹配判断结果。### 结果情况| 情况 | 说明 |
| - | - |
| `VerificationCompleted` | 验证流程已完成（请查看 `session.status` 获取结果） |
| `VerificationCancelled` | 用户取消了验证流程 |
| `VerificationFailed` | 验证过程中发生错误 |### SessionData 属性| 属性 | 类型 | 说明 |
| - | - | - |
| `sessionId` | `String` | 会话的唯一标识符 |
| `status` | `VerificationStatus` | `approved`、`pending` 或 `declined` |### 错误类型`VerificationErrorType` 枚举包含以下情况：| 错误 | 说明 |
| - | - |
| `sessionExpired` | 会话已过期 |
| `networkError` | 网络连接问题 |
| `cameraAccessDenied` | 未授予相机权限 |
| `notInitialized` | SDK 未初始化（仅 Android） |
| `apiError` | API 请求失败 |
| `retryBlocked` | 当前步骤已超过最大重试次数 |
| `unknown` | 其他错误，请查看 `error.message` 了解详情 |### 连接中断如果设备在验证过程中断开连接，SDK 会显示“无网络连接”界面覆盖验证流程，而不是让某个步骤一直处于加载状态。
SDK 会在后台持续检测，一旦能够再次连接 Didit，就会自动关闭该界面，用户将从中断前所在的步骤继续操作。
该界面提供 **重试** 按钮；除非关闭按钮已关闭，否则还提供 **退出验证** 按钮。
退出会使验证以 `VerificationFailed` 结果结束，其错误类型为 `networkError`，因此您的应用可以知道用户是因网络连接问题而离开的。### 完整结果处理示例```dart theme={null}
import 'package:didit_sdk/sdk_flutter.dart';

Future<void> verify(String token) async {
  final result = await DiditSdk.startVerification(token);

  switch (result) {
    case VerificationCompleted(:final session):
      switch (session.status) {
        case VerificationStatus.approved:
          print('Approved! Session: ${session.sessionId}');
          // User is verified — grant access
        case VerificationStatus.pending:
          print('Under review. Session: ${session.sessionId}');
          // Show "verification in progress" UI
        case VerificationStatus.declined:
          print('Declined. Session: ${session.sessionId}');
          // Handle declined verification
      }

    case VerificationCancelled(:final session):
      print('User cancelled.');
      if (session != null) {
        print('Session: ${session.sessionId}');
      }
      // Maybe show retry option

    case VerificationFailed(:final error):
      print('Error [${error.type}]: ${error.message}');
      // Handle error — show retry or contact support
  }
}
```***## 端到端示例 (后端会话 → Flutter SDK → Webhook)这是可用于生产环境的集成方案：您的后端创建会话，您的 Flutter 应用接收 `session_token`，再由 SDK 运行整个流程。最终决策通过 Webhook 发送至您的后端，切勿仅依赖客户端的结果。### 1. 后端创建会话```ts theme={null}
// Node.js / Express example. Run on your server. NEVER ship DIDIT_API_KEY to the device.
import express from "express";

const app = express();
app.use(express.json());

app.post("/api/verification/start", async (req, res) => {
  const response = await fetch("https://verification.didit.me/v3/session/", {
    method: "POST",
    headers: {
      "x-api-key": process.env.DIDIT_API_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      workflow_id: process.env.DIDIT_WORKFLOW_ID,
      vendor_data: req.body.userId, // your stable user id
    }),
  });

  if (!response.ok) {
    return res.status(500).json({ error: `Didit ${response.status}` });
  }

  const { session_id, session_token } = await response.json();
  // Persist session_id <-> userId so you can match the webhook later.
  res.json({ session_id, session_token });
});
```### 2. Flutter 交换令牌并启动 SDK```dart theme={null}
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:didit_sdk/sdk_flutter.dart';

class VerifyButton extends StatefulWidget {
  const VerifyButton({super.key, required this.userId});
  final String userId;

  @override
  State<VerifyButton> createState() => _VerifyButtonState();
}

class _VerifyButtonState extends State<VerifyButton> {
  bool _loading = false;

  Future<void> _verify() async {
    setState(() => _loading = true);
    try {
      final resp = await http.post(
        Uri.parse('https://your-backend.example.com/api/verification/start'),
        headers: const {'Content-Type': 'application/json'},
        body: jsonEncode({'userId': widget.userId}),
      );
      final sessionToken = (jsonDecode(resp.body) as Map)['session_token'] as String;

      final result = await DiditSdk.startVerification(sessionToken);

      switch (result) {
        case VerificationCompleted(:final session):
          if (session.status == VerificationStatus.approved) {
            // Optimistic UI; rely on the webhook delivered to your backend as the source of truth.
          }
        case VerificationCancelled():
          // User dismissed the flow
          break;
        case VerificationFailed(:final error):
          // Show retry — error.type / error.message
          break;
      }
    } finally {
      if (mounted) setState(() => _loading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _loading ? null : _verify,
      child: Text(_loading ? 'Starting...' : 'Verify identity'),
    );
  }
}
```### 3. 后端通过 Webhook 接收最终决策```ts theme={null}
app.post("/api/webhooks/didit", express.raw({ type: "application/json" }), (req, res) => {
  // 1. Verify the X-Signature header (see Webhooks docs)
  // 2. Parse the body
  const event = JSON.parse(req.body.toString());
  if (event.webhook_type === "status.updated") {
    // event.session_id, event.status ("Approved" | "Declined" | "In Review" | ...)
    // Look up the user by session_id and update their verification state.
  }
  res.sendStatus(200);
});
```有关 HMAC 签名验证的详细信息，请参阅 [Webhook 指南](https://docs.didit.me/zh/integration/webhooks.md)。***## 完整示例```dart theme={null}
import 'package:flutter/material.dart';
import 'package:didit_sdk/sdk_flutter.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Didit SDK Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1A1A1A)),
        useMaterial3: true,
      ),
      home: const VerificationScreen(),
    );
  }
}

class VerificationScreen extends StatefulWidget {
  const VerificationScreen({super.key});

  @override
  State<VerificationScreen> createState() => _VerificationScreenState();
}

class _VerificationScreenState extends State<VerificationScreen> {
  final _tokenController = TextEditingController();
  bool _loading = false;

  @override
  void dispose() {
    _tokenController.dispose();
    super.dispose();
  }

  Future<void> _startVerification() async {
    final token = _tokenController.text.trim();
    if (token.isEmpty) {
      _showAlert('Error', 'Please enter a session token.');
      return;
    }

    setState(() => _loading = true);

    try {
      final result = await DiditSdk.startVerification(
        token,
        config: const DiditConfig(loggingEnabled: true),
      );

      switch (result) {
        case VerificationCompleted(:final session):
          _showAlert(
            'Verification Complete',
            'Status: ${session.status.name}\nSession: ${session.sessionId}',
          );
        case VerificationCancelled():
          _showAlert('Cancelled', 'The user cancelled the verification.');
        case VerificationFailed(:final error):
          _showAlert('Failed', '${error.type.name}: ${error.message}');
      }
    } catch (e) {
      _showAlert('Error', 'Unexpected error: $e');
    } finally {
      setState(() => _loading = false);
    }
  }

  void _showAlert(String title, String message) {
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: Text(title),
        content: Text(message),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              TextField(
                controller: _tokenController,
                decoration: InputDecoration(
                  hintText: 'Enter session token...',
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(8),
                  ),
                ),
                autocorrect: false,
              ),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: _loading ? null : _startVerification,
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFF1A1A1A),
                  foregroundColor: Colors.white,
                  padding: const EdgeInsets.symmetric(vertical: 16),
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(8),
                  ),
                ),
                child: _loading
                    ? const SizedBox(
                        height: 20,
                        width: 20,
                        child: CircularProgressIndicator(
                          strokeWidth: 2,
                          color: Colors.white,
                        ),
                      )
                    : const Text(
                        'Start Verification',
                        style: TextStyle(fontWeight: FontWeight.w600),
                      ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
```***


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