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

# 交易监控 Webhook：交易事件与签名验证

> Didit 交易监控 Webhook 事件：触发条件、载荷结构与 HMAC 签名验证。每月包含 1,000 笔交易，之后每笔 $0.001。

Transaction Monitoring 会发出三个 Webhook 事件，覆盖交易的完整生命周期，另外还有一个可选的第四个事件，用于通知新的规则建议。所有事件都基于 Didit 标准的 Webhook 基础设施，并使用 HMAC-SHA256 签名。有关 Webhook 目标、签名和测试的说明，请参阅 [Webhook 参考文档](https://docs.didit.me/zh/integration/webhooks.md)。

## 事件目录

| 事件 | 触发时机 |
| - | - |
| `transaction.created` | 新交易已创建，且初始评估已完成。Didit 为您创建的入站交易也会触发此事件，包括存款后的 [Travel Rule sunrise 注册](https://docs.didit.me/zh/transaction-monitoring/travel-rule.md#存款后的-sunrise-登记)、入站 `TRP` 查询，或来自其他 Didit 客户的 `INTERNAL` 通道转账。 |
| `transaction.status.updated` | 交易状态发生变化，原因可能是分析师审核、整改会话的处理结果，或在 Travel Rule 状态转换后（例如钱包所有权证明完成后），Didit 重新评估您的规则时执行的规则操作。转账过期时，应用您的 `timeout_outcome` 所产生的状态变化不会发送此事件；请改为监听状态为 `EXPIRED` 的 `travel_rule.status.updated` 事件。 |
| `travel_rule.status.updated` | [Travel Rule](https://docs.didit.me/zh/transaction-monitoring/travel-rule.md) 交换状态发生转换，适用于出站转账以及入站转账（`INTERNAL` 通道的入站段、sunrise 注册和入站 `TRP` 查询），包括低于阈值时的 `NOT_APPLICABLE` 转换。对于 Didit 创建的交易，这些事件在其 `transaction.created` 之后发送。 |
| `transaction_rule_suggestions.created` | 规则建议运行为您的应用提出了新的规则变更建议。仅当您在规则建议通知设置中开启 **Webhook** 时才会触发，并且只针对创建了建议的运行。 |

实体级事件（`user.status.updated`、`business.status.updated`）也可能因交易活动而触发，详见[实体 Webhook](https://docs.didit.me/zh/entities/webhooks.md)。

> [!NOTE]
>
> 沙盒应用不会发出 `transaction.created` 或 `transaction.status.updated` Webhook。如需测试这些事件，请使用生产应用，或在控制台中使用 **API & Webhooks → Try Webhook**。Travel Rule 沙盒交换会发出 `environment: sandbox` 的 `travel_rule.status.updated` Webhook。正式上线前，请使用 [Didit Test VASP](https://docs.didit.me/zh/transaction-monitoring/travel-rule-sandbox.md) 测试交换流程。

## 信封结构

Didit 的 Webhook 采用**扁平**结构：事件字段与信封字段并列位于顶层，没有嵌套的 `data` 对象。每次投递都包含以下字段：

| 字段 | 说明 |
| - | - |
| `webhook_type` | 事件名称，例如 `transaction.created`。 |
| `event_id` | 事件的稳定标识符。重试时会复用同一个 `event_id`，因此请据此去重。 |
| `timestamp` / `created_at` | 投递发出时的 Unix 时间戳（秒）。 |
| `application_id` | 该事件所属的应用。 |
| `environment` | `live` 或 `sandbox`。 |

## Payload 结构

### `transaction.created` 和 `transaction.status.updated`

这两个事件共用同一种 payload：一个精简摘要，外加 [`action_required`](https://docs.didit.me/zh/transaction-monitoring/sdk-transaction-submission#用户所需操作) 块（若无则为 `null`）：

```json theme={null}
{
  "webhook_type": "transaction.created",
  "event_id": "9c0c8b8a-1111-4222-9333-666666666666",
  "timestamp": 1774970000,
  "created_at": 1774970000,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "transaction_id": "9c7d2a30-...",
  "txn_id": "tx-0001",
  "status": "IN_REVIEW",
  "score": 65,
  "severity": "MEDIUM",
  "amount": "500.00",
  "currency": "EUR",
  "direction": "OUTBOUND",
  "action_required": null
}
```

该 payload 有意保持精简。收到后，请使用 [`GET /v3/transactions/{transaction_id}/`](https://docs.didit.me/zh/management-api/transactions/get) 获取完整记录。返回内容包括完整详情（参与方、`rule_runs`、`provider_results`、`travel_rule`、`cost_breakdown`、`decision_reason_code`），以及状态发生变化的*原因*。

### `transaction_rule_suggestions.created`

该事件针对的是您的规则集，而非某一笔交易，因此不包含交易 ID。
事件内容绝不包含个人数据，仅包括建议 ID、类型、标题和数量。

```json theme={null}
{
  "webhook_type": "transaction_rule_suggestions.created",
  "run_id": "5b2f0c1e-0000-0000-0000-000000000000",
  "open_count": 3,
  "suggestions": [
    {
      "suggestion_id": "9a7d3e22-0000-0000-0000-000000000000",
      "kind": "...",
      "title": "...",
      "rule_id": "...",
      "expected_impact": {}
    }
  ]
}
```

| 字段 | 说明 |
| - | - |
| `run_id` | 提出这些新建议的运行。 |
| `open_count` | 本次运行结束后，该应用中处于开放状态的建议数量。 |
| `suggestions[]` | 本次运行创建的建议，每条包含 `suggestion_id`、`kind`、`title`、`rule_id`（当建议提议新规则时为 null）和 `expected_impact`。 |

通用信封字段（`event_id`、`timestamp`、`application_id`、`environment`）同样会添加到请求体中。

### `travel_rule.status.updated`

```json theme={null}
{
  "webhook_type": "travel_rule.status.updated",
  "event_id": "9c0c8b8a-1111-4222-9333-777777777777",
  "timestamp": 1774970000,
  "created_at": 1774970000,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "transaction_id": "22222222-3333-4444-5555-666666666666",
  "txn_id": "trv-2026-07-05-001",
  "travel_rule_status": "COMPLETED",
  "rail": "INTERNAL",
  "direction": "OUTBOUND"
}
```

每次状态转换都会触发此事件，包括进入 `FINISHED`、`CANCELLED` 或 `EXPIRED`。转账一旦到达其中任一状态，便不会再为其发送任何 Travel Rule 事件。
事件的状态可能与上一个事件相同，例如重新发送时仍未找到通道，转账保持在 `COUNTERPARTY_VASP_NOT_FOUND`。
`rail` 的取值为 `INTERNAL`、`TRP`、`GTR`、`TRUST`、`VERIFYVASP`、`SYGNA`、`EMAIL`、`TEST_VASP`（由 Didit Test VASP 应答的沙盒转账，同时带有 `test_vasp: true` 和 `test_vasp_scenario`），或在未选定通道时为 `""`（sunrise 注册始终为 `""`）。

在创建交易、sunrise 注册、入站 `TRP` 查询或 `INTERNAL` 通道的入站段中，Didit 会先发送 `transaction.created`，再发送该交易的 `travel_rule.status.updated` 事件。若某条规则在此步骤中改变了交易状态，也可能在 `transaction.created` 之前发送 `transaction.status.updated`。
附加到您已创建的存款交易上的 sunrise 注册，不会发送 `transaction.created`。
每个 Webhook 都独立投递和重试，因此被重试的事件可能晚于之后的事件到达。请以 `transaction_id` 为键处理事件，需要了解当前状态时，重新读取该交易。

## 状态流转示例

### 直接通过

```
transaction.created (APPROVED, score 0)
```

### 先审核后通过

```
transaction.created (IN_REVIEW, score 65)
...analyst reviews...
transaction.status.updated (status: APPROVED)
```

### 升级验证

```
transaction.created (AWAITING_USER, action_required.type: verification_session)
...user completes the biometric re-check...
transaction.status.updated (status: APPROVED, action_required: null)
```

### Travel Rule 转账

```
transaction.created (APPROVED)
travel_rule.status.updated (AWAITING_COUNTERPARTY, rail: EMAIL)
...counterparty accepts the pickup...
travel_rule.status.updated (COMPLETED)
...you broadcast on-chain and PATCH the hash...
travel_rule.status.updated (FINISHED)
```

前两个事件来自同一个请求，其中 `transaction.created` 先入队。每个 Webhook 都独立投递，因此不要依赖到达顺序。
通过 TRUST 发送的转账永远不会收到 `COMPLETED` 事件，因为 TRUST 不会返回 Didit 可读取的任何内容：转账会停留在 `AWAITING_COUNTERPARTY`，直到确认截止时间，然后以 `EXPIRED` 结束。通过 GTR、VerifyVASP 和 Sygna Bridge 发送时，Didit 会从网络中读取交易对手的应答，事件随之发出。

## 签名验证

每次投递都会使用目标的共享密钥进行 HMAC-SHA256 签名，并附带三个签名请求头和一个时间戳：

| 请求头 | 方案 |
| - | - |
| `X-Signature-V2` | 基于规范 JSON（键已排序，保留 Unicode）的 HMAC，**推荐**。 |
| `X-Signature` | 基于 ASCII 转义后的规范 JSON 的 HMAC（旧版）。 |
| `X-Signature-Simple` | 基于 `"{timestamp}:{session_id}:{status}:{webhook_type}"` 的 HMAC，**已弃用**。交易 payload 不包含 `session_id`，因此它签名的是 `"{timestamp}::{status}:{webhook_type}"`：该签名既无法标识交易，也无法标识交易金额或评分。`travel_rule.status.updated` payload 同样不包含 `status`，因此它签名的是 `"{timestamp}:::travel_rule.status.updated"`。请使用 `X-Signature-V2` 进行验证，并参阅[弃用通知](https://docs.didit.me/zh/integration/webhooks.md#x-signature-simple-已弃用)。 |
| `X-Timestamp` | Unix 纪元秒数，拒绝早于当前时间 5 分钟以上的投递。 |

```typescript theme={null}
import { createHmac, timingSafeEqual } from 'crypto';

function verify(rawBody: string, signatureV2: string, secret: string) {
  const canonical = JSON.stringify(
    Object.keys(JSON.parse(rawBody)).sort()
      .reduce((acc, k) => ({ ...acc, [k]: JSON.parse(rawBody)[k] }), {})
  );
  const expected = createHmac('sha256', secret).update(canonical).digest('hex');
  return timingSafeEqual(Buffer.from(signatureV2), Buffer.from(expected));
}
```

完整的验证代码（包括时间戳新鲜度检查）请参阅 [Webhook 参考](https://docs.didit.me/zh/integration/webhooks.md#签名验证)。

## 重试与投递保证

* 遇到 `5xx`、`404`、超时或连接失败时，Didit 最多重试 **2 次**：首次失败约 1 分钟后重试一次，之后再过约 4 分钟重试一次。
* 重试的投递会复用相同的 `event_id`，请在您的处理程序中据此去重。
* 最后一次失败后，该事件的投递停止，并会通知您的组织；Webhook 目标绝不会被静默禁用。
* 请尽快返回 `2xx`（先入队，再处理）。ACK 响应缓慢可能导致超时和重复投递。

## 幂等性

请将 `event_id` 存入您自己的去重存储，以确保每个 Webhook 仅被处理一次。当您的端点响应 ACK 缓慢或在请求过程中断开连接时，同一事件可能会被重新投递。

## 订阅

```bash theme={null}
curl -X POST https://verification.didit.me/v3/webhook/destinations/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "TM events",
    "url": "https://yourapp.com/webhooks/didit-tm",
    "subscribed_events": [
      "transaction.created",
      "transaction.status.updated",
      "travel_rule.status.updated"
    ]
  }'
```

## 后续步骤

> [card] [Webhook 参考](https://docs.didit.me/zh/integration/webhooks.md)
>
> Webhook 目标、签名与重试。

> [card] [状态](https://docs.didit.me/zh/transaction-monitoring/statuses.md)
>
> 完整的状态机。

> [card] [集成指南](https://docs.didit.me/zh/transaction-monitoring/integration-guide.md)
>
> 在生产环境中处理 Webhook。


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