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

# KYT Webhook：取引イベントとHMAC署名検証

> DiditのKYT Webhookは、取引イベントのトリガー、ペイロード形式、HMAC署名検証に対応しています。毎月1,000件の取引が含まれ、超過分は1件あたり$0.001です。

トランザクションモニタリングは、取引のライフサイクル全体をカバーする3つのWebhookイベントと、新しいルール提案を通知する任意の4つ目のイベントを送信します。いずれもHMAC-SHA256署名を備えたDiditの標準Webhook基盤で配信されます。送信先、署名、テストについては[Webhookリファレンス](https://docs.didit.me/ja/integration/webhooks.md)を参照してください。

## イベント一覧

| イベント | 発火するタイミング |
| - | - |
| `transaction.created` | 新しい取引が作成され、初回評価が完了したとき。Diditがお客様に代わって作成する受信取引でも発火します。対象は、入金後の[トラベルルールのsunrise登録](https://docs.didit.me/ja/transaction-monitoring/travel-rule.md#入金後のサンライズ登録)、受信した`TRP`照会、他のDidit顧客からの`INTERNAL`レールの送金です。 |
| `transaction.status.updated` | 取引のステータスが変更されたとき。変更の要因は、アナリストによるレビュー、是正セッションの解決、またはトラベルルールの遷移後（たとえばウォレット所有の証明後）にDiditがルールを再評価した際のルールアクションです。送金が期限切れになり`timeout_outcome`が適用される際のステータス変更では、このイベントは送信されません。代わりに、`EXPIRED` の `travel_rule.status.updated` を処理してください。 |
| `travel_rule.status.updated` | [トラベルルール](https://docs.didit.me/ja/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/ja/entities/webhooks.md)を参照してください。

> [!NOTE]
>
> サンドボックスのアプリケーションでは、`transaction.created`および`transaction.status.updated`のWebhookは送信されません。これらのイベントをテストするには、本番環境のアプリケーションを使用するか、コンソールの**API & Webhooks → Try Webhook**を使用してください。トラベルルールのサンドボックス環境での情報交換では、`environment: sandbox`付きの`travel_rule.status.updated`のWebhookが送信されます。本番運用の前に情報交換をテストするには、[Didit Test VASP](https://docs.didit.me/ja/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`。 |

## ペイロードの構造

### `transaction.created`と`transaction.status.updated`

どちらのイベントも同じペイロードを共有します。簡潔なサマリーと[`action_required`](https://docs.didit.me/ja/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
}
```

ペイロードは意図的に最小限にしています。受信したら、[`GET /v3/transactions/{transaction_id}/`](https://docs.didit.me/ja/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`への移行を含め、すべての遷移で発火します。送金がこれらのいずれかに達すると、その送金についてトラベルルールのイベントはそれ以上送信されません。
イベントが直前のイベントと同じステータスを繰り返すことがあります。たとえば、再送時に再びレールが見つからず、送金が`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)
```

### トラベルルールの送金

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

最初の2つのイベントは同じリクエストから発生し、`transaction.created`が先にキューに入ります。各Webhookは個別に配信されるため、到着順序に依存しないでください。
TRUST経由で送信された送金には`COMPLETED`イベントが送信されません。TRUSTはDiditが読み取れる応答を返さないためです。送金は確認期限まで`AWAITING_COUNTERPARTY`のままとなり、その後`EXPIRED`で終了します。GTR、VerifyVASP、Sygna Bridge経由の場合、Diditはネットワークから取引相手の応答を読み取り、イベントはその応答に応じて発生します。

## 署名検証

すべての配信は送信先の共有シークレットを使用してHMAC-SHA256で署名され、3つの署名ヘッダーとタイムスタンプが付与されます。

| ヘッダー | 方式 |
| - | - |
| `X-Signature-V2` | 正規化JSON（キーをソート、Unicodeを保持）に対するHMAC。**推奨**。 |
| `X-Signature` | ASCIIエスケープした正規化JSONに対するHMAC（レガシー）。 |
| `X-Signature-Simple` | `"{timestamp}:{session_id}:{status}:{webhook_type}"`に対するHMAC。**非推奨**。取引のペイロードには`session_id`が含まれないため、実際の署名対象は`"{timestamp}::{status}:{webhook_type}"`となり、取引も、その金額やスコアも特定できません。`travel_rule.status.updated`のペイロードには`status`も含まれないため、署名対象は`"{timestamp}:::travel_rule.status.updated"`となります。検証には`X-Signature-V2`を使用してください。詳しくは[非推奨のお知らせ](https://docs.didit.me/ja/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/ja/integration/webhooks.md#署名検証)を参照してください。

## 再試行と配信保証

* `5xx`、`404`、タイムアウト、または接続失敗が発生した場合、Diditは最大**2回**再試行します。1回目は最初の失敗から約1分後、2回目はそこからさらに約4分後です。
* 再試行される配信では同じ`event_id`が使われます。ハンドラーではこの値で重複を排除してください。
* 最後の再試行も失敗すると、そのイベントの配信は停止し、組織に通知されます。送信先が通知なしに無効化されることはありません。
* `2xx`をすばやく返してください（キューに入れてから処理してください）。ACKが遅いと、タイムアウトや重複配信が発生するおそれがあります。

## 冪等性

`event_id`を独自の重複排除ストアに保存し、各Webhookを確実に1回だけ処理してください。エンドポイントのACKが遅い場合やリクエストの途中で接続が切れた場合、同じイベントが再配信されることがあります。

## Webhookの購読

```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/ja/integration/webhooks.md)
>
> 送信先、署名、再試行。

> [card] [ステータス](https://docs.didit.me/ja/transaction-monitoring/statuses.md)
>
> ステートマシンの全体像。

> [card] [連携ガイド](https://docs.didit.me/ja/transaction-monitoring/integration-guide.md)
>
> 本番環境でのWebhookの処理。


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