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

# API 完整流程：端到端 KYC 集成指南

> Didit REST API 端到端集成：获取凭证、创建会话、分享链接、处理 Webhook，几分钟内获取 KYC 决策结果。

本指南将带您完整了解通过 REST API 集成 Didit 身份验证的流程：获取凭证、创建会话、将会话呈现给用户，并接收结果。

***

## 步骤 1: 获取您的凭证

您有两种方式获取 `api_key`：

### 选项 A: 通过编程方式注册 (无需浏览器)

适用于 CI/CD 和 AI 智能体。依次调用两个 API 接口 `POST /programmatic/register/` 和 `POST /programmatic/verify-email/`，即可直接返回 `api_key`。

→ 有关完整流程、密码规则、JWT 有效期和锁定策略，请参阅 [编程方式注册](https://docs.didit.me/zh/integration/programmatic-registration.md)。

### 选项 B: 从 Didit 控制台获取

1. 前往 [Didit 控制台](https://business.didit.me)。
2. 创建或选择您的组织。
3. 进入 **Settings** → **API & Webhooks**。
4. 复制以下内容：
   * **API Key**：用作每次验证调用中的 `x-api-key` 请求头。
   * **Webhook Secret Key**：用于验证 Webhook 的 HMAC 签名。

### 环境变量

```bash theme={null}
# Required
DIDIT_API_KEY=your_api_key_here
DIDIT_WEBHOOK_SECRET=your_webhook_secret_here

# Your workflow ID (from the Console)
DIDIT_WORKFLOW_ID=your_workflow_id_here
```

***

## 步骤 2: 创建工作流

在创建会话之前，您需要一个验证工作流。工作流定义了用户需要完成的验证步骤。

### 在控制台中创建

1. 前往 [Didit 控制台](https://business.didit.me) → **Workflows**
2. 点击 **Create Workflow**
3. 选择基础模板：

| 模板 | 适用场景 |
| - | - |
| **KYC** | 使用证件进行完整身份验证 |
| **自适应年龄验证** | 年龄验证，可选择以证件作为备用验证方式 |
| **生物识别认证** | 对回访用户重新验证 |
| **地址验证** | 地址证明验证 |

4. 添加可选功能：

| 功能 | 说明 |
| - | - |
| NFC 验证 | 读取护照或证件芯片数据 |
| 活体检测 | 通过自拍视频防止欺骗攻击 |
| 人脸比对 | 将自拍照与证件照片进行比对 |
| 电话验证 | 通过短信验证电话号码 |
| 邮箱验证 | 验证邮箱地址 |
| AML 筛查 | 对照制裁名单和 PEP 名单进行检查 |
| 数据库核验 | 与政府数据库进行核验 |
| 设备与 IP 分析 | 检测 VPN、代理和地理位置 |

5. 复制您的 **Workflow ID**

> [!NOTE]
>
> 有关详细配置选项，请参阅 [工作流文档](https://docs.didit.me/zh/console/workflows.md)。

***

## Step 3: 创建验证会话

调用 API 为您的用户创建会话。

### 请求

```bash theme={null}
curl -X POST https://verification.didit.me/v3/session/ \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "workflow_id": "your-workflow-id",
    "callback": "https://yourapp.com/verification-complete",
    "vendor_data": "user-123"
  }'
```

### 请求参数

| 参数 | 是否必填 | 说明 |
| - | - | - |
| `workflow_id` | 是 | 控制台中您的工作流 UUID。 |
| `callback` | 否 | 验证完成后将用户跳转到的 URL。Didit 会在其后附加 `verificationSessionId` 和 `status` 作为查询参数。支持自定义协议（例如 `myapp://`）。省略时回退到工作流中配置的回调 URL。 |
| `callback_method` | 否 | 由哪个设备处理重定向：`initiator`、`completer` 或 `both`。默认值：`initiator`。 |
| `vendor_data` | 否 | 您的内部用户标识符，会在 Webhook 中回传，用于关联。 |
| `metadata` | 否 | 任意 JSON 对象，与会话一同存储，并在 Webhook 中回传。 |
| `language` | 否 | 界面语言提示（ISO 639-1，例如 `en`、`es`）。省略时自动检测浏览器语言。 |
| `contact_details` | 否 | 用户的邮箱/电话，用于预填和邮件通知。 |
| `expected_details` | 否 | 预期的用户信息，用于与已验证数据进行交叉验证。 |
| `portrait_image` | 条件必填 | Base64 编码的人脸图像（最大 2MB；支持 JPEG、PNG、WebP 或 TIFF）。用作生物识别认证以及以人脸比对为先的工作流中的参考人脸。若 `vendor_data` 对应的用户已存有人脸（已通过活体检测的人脸、电子护照照片、证件人像或已录入的资料人脸），则可省略：Didit 会自动复用。若两者均不可用，则返回 `400`。 |
| `sandbox_scenario` | 否 | 仅限沙盒的场景标识（例如 `approve`、`decline_aml_hit`），用于自动为会话填入魔法输入。 |

### 包含全部选项的完整示例

```bash theme={null}
curl -X POST https://verification.didit.me/v3/session/ \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "workflow_id": "11111111-2222-3333-4444-555555555555",
    "callback": "https://yourapp.com/verification-complete",
    "vendor_data": "user-123",
    "metadata": {
      "plan": "premium",
      "signup_source": "mobile-app"
    },
    "contact_details": {
      "email": "alex.sample@example.com",
      "email_lang": "en",
      "send_notification_emails": true,
      "phone": "+15550101000"
    },
    "expected_details": {
      "first_name": "John",
      "last_name": "Doe",
      "date_of_birth": "1990-01-01",
      "expected_document_types": ["P", "ID"]
    }
  }'
```

> [!NOTE]
>
> `expected_details.expected_document_types` 会将身份证件验证步骤限制为特定证件类型，证件选择页面只显示所请求的类型。允许的值：`P`（护照）、`ID`（国民身份证）、`DL`（驾照）、`RP`（居留许可）、`HIC`（医疗保险卡）、`TC`（税卡）、`SSC`（社会保障卡）。值不区分大小写，并会去重；未知值返回 `400`。完整结构请参见 [创建会话参考](https://docs.didit.me/zh/sessions-api/create-session.md)。

### 响应

返回 `201 Created`：

```json theme={null}
{
  "session_id": "11111111-2222-3333-4444-555555555555",
  "session_number": 43762,
  "session_token": "3FaJ9wLqX2Mz",
  "url": "https://verify.didit.me/session/3FaJ9wLqX2Mz",
  "vendor_data": "user-123",
  "metadata": {
    "plan": "premium",
    "signup_source": "mobile-app"
  },
  "status": "Not Started",
  "workflow_id": "11111111-2222-3333-4444-555555555555",
  "workflow_version": 3,
  "callback": "https://yourapp.com/verification-complete"
}
```

### 关键响应字段

| 字段 | 说明 |
| - | - |
| `session_id` | 该会话的唯一 UUID。用于 `GET /v3/session/{sessionId}/decision/` 和 Webhook。 |
| `session_number` | 在 Didit 控制台中显示的可读序号。 |
| `session_token` | 12 位、URL 安全的令牌，用于授权终端用户打开托管流程，并用于初始化原生 SDK（Web SDK 改用 `url`）。请将其视为机密信息。 |
| `url` | 托管验证 URL，用于将用户重定向至该地址，或嵌入 iframe。（字段名为 `url`，而非 `verification_url`。）请求中包含 `language` 时，URL 会包含语言路径段，例如 `https://verify.didit.me/en/session/3FaJ9wLqX2Mz`。 |
| `status` | 当前状态。新创建的会话为 `"Not Started"`。 |
| `workflow_id` | 会话所运行的稳定工作流标识符。 |
| `workflow_version` | 会话创建时固定使用的已发布工作流版本。 |

> [!NOTE]
>
> **幂等性。** 提供 `vendor_data` 时，如果工作流最新已发布版本上已存在具有相同 `vendor_data` 且未完成的会话（状态为 `Not Started`、`In Progress`、`Resubmitted` 或 `Awaiting User`），则直接返回该已有会话（仍为 `201`），而不会创建重复会话。此时其 `callback` 和 `metadata` 会更新为新值，`status` 反映已有会话的状态。

> [!NOTE]
>
> **缺少 `x-api-key` 时返回 `403`**，响应体为 `{"detail": "You do not have permission to perform this action."}`，与拥有但无权限的有效密钥返回的响应体相同。错误、已撤销或已过期的密钥返回 `401`。额度不足时返回 `400`。完整文档请参见 [创建会话 API 参考](https://docs.didit.me/zh/sessions-api/create-session.md)。

***

## Step 4: 向用户展示验证流程

选择向用户展示验证流程的方式：

### 选项 A: 原生 SDK（推荐用于移动端）

将 `session_token` 与原生 SDK 配合使用，以集成摄像头、NFC 和生物识别功能。

→ [iOS SDK](https://docs.didit.me/zh/integration/native-sdks/ios-sdk.md) · [Android SDK](https://docs.didit.me/zh/integration/native-sdks/android-sdk.md) · [React Native](https://docs.didit.me/zh/integration/native-sdks/react-native-sdk.md) · [Flutter](https://docs.didit.me/zh/integration/native-sdks/flutter-sdk.md)

### 选项 B: InContext iframe（推荐用于 Web）

使用步骤 3 返回的 `url`，将验证嵌入您的页面。

```html theme={null}
<iframe
  src="{url}"
  style="width: 100%; height: 700px; border: none;"
  allow="camera; microphone; geolocation; clipboard-write; fullscreen; autoplay; encrypted-media; digital-credentials-get"
></iframe>
```

→ [InContext iframe](https://docs.didit.me/zh/integration/web-sdks/incontext-iframe.md)

### 选项 C: 重定向

将用户重定向至验证 URL。

```javascript theme={null}
window.location.href = session.url;
```

→ [Web 重定向](https://docs.didit.me/zh/integration/web-sdks/web-redirect.md)

### 选项 D: 移动端 WebView

适用于没有原生 SDK 的移动应用。

```jsx theme={null}
// React Native
<WebView source={{ uri: session.url }} />
```

→ [iOS/Android 中的 WebView](https://docs.didit.me/zh/integration/web-sdks/webview-in-ios-android.md)

***

## 步骤 5: 接收结果

### 通过 Webhook (推荐)

Webhook 会在验证状态变化时实时通知您的服务器。

**设置：**

1. 在[控制台](https://business.didit.me)（**API 和 Webhook**）中创建 Webhook 目标，或通过 [`POST /v3/webhook/destinations/`](https://docs.didit.me/zh/management-api/webhook/create-destination) 创建，并设置 `webhook_version: "v3"` 以及 `subscribed_events` 数组（例如 `["status.updated"]`）
2. 保存创建时返回的 `secret_shared_key`，它是所有签名变体通用的 HMAC 密钥
3. 在您的服务器上实现 Webhook endpoint

**Webhook payload 示例：**

在 `decision` 内部，每项功能的结果都以**复数数组**形式返回（`id_verifications[]`、`nfc_verifications[]`、`liveness_checks[]`、`face_matches[]` 等），工作流中的每个节点对应一个条目。这是 V3 协议约定，不得更改：永远不要基于单数的 `nfc` / `id_verification` / `liveness` 键编写代码。

```json theme={null}
{
  "event_id": "9c0c8b8a-1111-4222-9333-444444444444",
  "webhook_type": "status.updated",
  "timestamp": 1774970000,
  "created_at": 1774969994,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "session_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "status": "Approved",
  "workflow_id": "22222222-3333-4444-5555-666666666666",
  "workflow_version": 4,
  "vendor_data": "user-123",
  "metadata": { "plan": "premium" },
  "decision": {
    "session_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "status": "Approved",
    "id_verifications": [
      { "node_id": "id_verification_1", "status": "Approved", "document_type": "Passport", "warnings": [] }
    ],
    "liveness_checks": [
      { "node_id": "liveness_1", "status": "Approved", "method": "PASSIVE", "score": 95.4, "warnings": [] }
    ],
    "face_matches": [
      { "node_id": "face_match_1", "status": "Approved", "score": 96.1, "warnings": [] }
    ],
    "reviews": []
  }
}
```

只有当 `status` 为 `Approved`、`Declined`、`In Review` 或 `Abandoned` 时才会包含 `decision` 对象（`Resubmitted` 事件则携带 `resubmit_info`）。Didit 在重试和分发到多个目标时会复用同一个 `event_id`，请以它作为幂等性的判断依据。有关所有复数数组和字段，请参阅[数据模型](https://docs.didit.me/zh/reference/data-models.md)。

**验证签名（X-Signature-V2，推荐）：**

`X-Signature-V2` 是对 JSON 请求体的规范化重新编码结果计算的 HMAC-SHA256：值为整数的浮点数会被转换为整数，键按递归方式排序，使用紧凑分隔符，并保留 Unicode 字符。

```javascript theme={null}
const crypto = require("crypto");

// Match Didit's float normalisation: whole-valued floats serialise as ints.
function shortenFloats(data) {
  if (Array.isArray(data)) return data.map(shortenFloats);
  if (data !== null && typeof data === "object") {
    return Object.fromEntries(
      Object.entries(data).map(([key, value]) => [key, shortenFloats(value)])
    );
  }
  return data;
}

// Sort object keys recursively before re-stringifying.
function sortKeys(obj) {
  if (Array.isArray(obj)) return obj.map(sortKeys);
  if (obj !== null && typeof obj === "object") {
    return Object.keys(obj).sort().reduce((acc, key) => {
      acc[key] = sortKeys(obj[key]);
      return acc;
    }, {});
  }
  return obj;
}

function verifyWebhook(rawBody, signatureV2, timestamp, secret) {
  // 1. Reject anything older than 5 minutes
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) return false;

  // 2. Recompute HMAC-SHA256 against the canonical JSON Didit signs
  const canonical = JSON.stringify(sortKeys(shortenFloats(JSON.parse(rawBody))));
  const expected = crypto.createHmac("sha256", secret).update(canonical, "utf8").digest("hex");

  // 3. Constant-time compare
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signatureV2, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

→ [完整 Webhook 文档](https://docs.didit.me/zh/integration/webhooks.md)，包括基于原始字节的 `X-Signature`、已弃用的仅包含信封的 `X-Signature-Simple`，以及如何选择签名方式。Didit 会对失败的投递（`5xx`、`404`、超时）重试最多 2 次（约 1 分钟后，再约 4 分钟后），超时时间为 **5 秒**，因此请尽快返回 `2xx`。

### 通过 API (可选)

您可以随时获取相同的决策 payload：

```bash theme={null}
curl https://verification.didit.me/v3/session/{session_id}/decision/ \
  -H "x-api-key: YOUR_API_KEY"
```

该响应结构（复数数组）与 Webhook payload 中的 `decision` 对象相同，并且可以在会话生命周期的任意时间点调用此 endpoint。实时更新请使用 Webhook，API 调用仅用于数据回填、审计和重建。`/decision/` 端点的限流为每个 API 密钥每分钟 600 次 GET 请求（参见[速率限制](https://docs.didit.me/zh/integration/rate-limiting.md)）。错误语义如下：未知的 `session_id` 返回 `404` 和 `{"detail": "Not found."}`；认证失败时返回 `403`，此端点绝不返回 `401`。响应中的媒体 URL 是短期有效的预签名链接，请及时获取，不要持久保存。

→ [获取会话 API 参考](https://docs.didit.me/zh/sessions-api/retrieve-session.md)

***

## 完整代码示例

### Node.js / Express

```javascript theme={null}
const express = require("express");
const crypto = require("crypto");
const app = express();

// Create verification session
app.post("/api/verify", async (req, res) => {
  const { userId } = req.body;

  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,
      callback: `${process.env.APP_URL}/verification-complete`,
      vendor_data: userId,
    }),
  });

  const session = await response.json();

  // Store session_id associated with user
  await db.users.update({
    where: { id: userId },
    data: { verificationSessionId: session.session_id },
  });

  res.json({
    verificationUrl: session.url,             // hosted verification URL
    sessionToken: session.session_token,      // for native/web SDKs
  });
});

// Handle webhook — uses verifyWebhook / shortenFloats / sortKeys from Step 5
app.post(
  "/api/webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.get("X-Signature-V2");
    const timestamp = req.get("X-Timestamp");

    if (
      !verifyWebhook(
        req.body.toString("utf8"),
        signature,
        timestamp,
        process.env.DIDIT_WEBHOOK_SECRET,
      )
    ) {
      return res.status(401).json({ error: "Invalid signature" });
    }

    const payload = JSON.parse(req.body);
    const { event_id, session_id, status, vendor_data } = payload;

    // Idempotency: the same event_id is reused on retries and fan-out.
    // Skip events you have already processed (e.g. a processed_events table).

    // Update user's verification status (status values are case-sensitive strings)
    if (status === "Approved") {
      db.users.update({
        where: { id: vendor_data },
        data: { isVerified: true, verifiedAt: new Date() },
      });
    }

    // Return 2xx quickly (Didit's delivery timeout is 5s); do heavy work asynchronously
    res.json({ received: true });
  },
);

app.listen(3000);
```

### Python / FastAPI

```python theme={null}
from fastapi import FastAPI, Request, HTTPException
import httpx
import hashlib
import hmac
import json
import os
import time

app = FastAPI()

@app.post("/api/verify")
async def create_verification(user_id: str):
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://verification.didit.me/v3/session/",
            headers={
                "x-api-key": os.environ["DIDIT_API_KEY"],
                "Content-Type": "application/json"
            },
            json={
                "workflow_id": os.environ["DIDIT_WORKFLOW_ID"],
                "callback": f"{os.environ['APP_URL']}/verification-complete",
                "vendor_data": user_id
            }
        )

    session = response.json()
    return {
        "verification_url": session["url"],            # hosted verification URL
        "session_token": session["session_token"],     # for native/web SDKs
    }

def shorten_floats(data):
    """Match Didit: whole-valued floats serialise as ints."""
    if isinstance(data, dict):
        return {k: shorten_floats(v) for k, v in data.items()}
    if isinstance(data, list):
        return [shorten_floats(x) for x in data]
    if isinstance(data, float) and data.is_integer():
        return int(data)
    return data

@app.post("/api/webhook")
async def handle_webhook(request: Request):
    payload = await request.json()
    signature = request.headers.get("X-Signature-V2")
    timestamp = request.headers.get("X-Timestamp")

    # Verify timestamp freshness
    if not signature or not timestamp or abs(int(time.time()) - int(timestamp)) > 300:
        raise HTTPException(status_code=401, detail="Request too old")

    # Recompute Didit's V2 canonical JSON: shortened floats, sorted keys,
    # compact separators, Unicode preserved (ensure_ascii=False)
    canonical = json.dumps(
        shorten_floats(payload),
        sort_keys=True,
        separators=(",", ":"),
        ensure_ascii=False,
    )
    expected = hmac.new(
        os.environ["DIDIT_WEBHOOK_SECRET"].encode("utf-8"),
        canonical.encode("utf-8"),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        raise HTTPException(status_code=401, detail="Invalid signature")

    # Idempotency: dedupe on payload["event_id"] (reused on retries)

    if payload["status"] == "Approved":
        # Update user in database
        pass

    return {"received": True}
```

***

## 会话状态

以下是 `status` 的 10 个可能值，均为区分大小写的精确字符串（注意 `Kyc Expired` 中的 `K` 为大写）：

| 状态 | 描述 |
| - | - |
| `Not Started` | 会话已创建，但用户尚未打开链接。 |
| `In Progress` | 用户正在完成验证。 |
| `In Review` | 验证已被标记，需要人工审核。 |
| `Approved` | 所有检查均已通过。 |
| `Declined` | 一项或多项检查未通过。 |
| `Resubmitted` | 审核员要求重新完成特定步骤。 |
| `Awaiting User` | KYB 父会话正在等待子 KYC 当事人完成验证。 |
| `Abandoned` | 用户已开始但未在规定时间内完成。 |
| `Expired` | 会话在用户打开链接之前已超时。 |
| `Kyc Expired` | 此前已通过的会话，其证件已超过有效期。 |

有关完整的生命周期、状态转换和 Mermaid 图，请参阅[验证状态](https://docs.didit.me/zh/integration/verification-statuses.md)。

***

## 后续步骤

1. **选择您的集成方式**：
   * 移动应用 → [原生 SDK](https://docs.didit.me/zh/integration/native-sdks/overview.md)
   * Web 应用 → [Web SDK](https://docs.didit.me/zh/integration/web-sdks/overview.md)
2. **配置 Webhook**：[Webhook](https://docs.didit.me/zh/integration/webhooks.md)
3. **规划容量**：[速率限制](https://docs.didit.me/zh/integration/rate-limiting.md)
4. **处理每一种状态**：[验证状态](https://docs.didit.me/zh/integration/verification-statuses.md)
5. **自定义您的工作流**：[工作流](https://docs.didit.me/zh/console/workflows.md)
6. **验证 API 是否可访问**：[健康检查](https://docs.didit.me/zh/api-reference/healthcheck.md)

## 需要帮助?

* **支持**：[support@didit.me](mailto:support@didit.me)
* **程序化入驻**：[程序化注册](https://docs.didit.me/zh/integration/programmatic-registration.md)


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