> ## 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 Pemantauan Transaksi KYT: Event & Tanda Tangan

> Event webhook transaksi di Didit: pemicu, payload, dan verifikasi tanda tangan HMAC. 1,000 transaksi/bulan termasuk, lalu $0.001 per transaksi.

Transaction Monitoring mengirim tiga event webhook yang bersama-sama mencakup seluruh siklus hidup transaksi, ditambah satu event opsional yang memberi tahu saran aturan baru. Semuanya menggunakan infrastruktur webhook standar Didit dengan penandatanganan HMAC-SHA256. Lihat [referensi webhook](https://docs.didit.me/id/integration/webhooks.md) untuk tujuan, penandatanganan, dan pengujian.

## Katalog event

| Event | Kapan dipicu |
| - | - |
| `transaction.created` | Sebuah transaksi baru dibuat dan evaluasi awal selesai. Event ini juga dipicu untuk transaksi masuk yang dibuat Didit untuk Anda: pendaftaran [Travel Rule sunrise](https://docs.didit.me/id/transaction-monitoring/travel-rule.md#pendaftaran-sunrise-setelah-deposit) setelah deposit, permintaan `TRP` masuk, atau transfer rail `INTERNAL` dari pelanggan Didit lain. |
| `transaction.status.updated` | Status transaksi berubah - dari tinjauan analis, penyelesaian sesi remediasi, atau tindakan aturan ketika Didit mengevaluasi ulang aturan Anda setelah transisi Travel Rule (misalnya setelah bukti kepemilikan dompet). Perubahan status yang menerapkan `timeout_outcome` Anda ketika transfer kedaluwarsa tidak mengirim event ini; tangani `travel_rule.status.updated` dengan `EXPIRED` sebagai gantinya. |
| `travel_rule.status.updated` | Pertukaran [Travel Rule](https://docs.didit.me/id/transaction-monitoring/travel-rule.md) berpindah status, pada transfer keluar maupun masuk (leg masuk rail `INTERNAL`, pendaftaran sunrise, dan permintaan `TRP` masuk), termasuk transisi `NOT_APPLICABLE` untuk transaksi di bawah ambang batas. Untuk transaksi yang dibuat Didit, event ini dikirim setelah `transaction.created`-nya. |
| `transaction_rule_suggestions.created` | Sebuah proses saran aturan mengusulkan perubahan aturan baru untuk aplikasi Anda. Event ini hanya dipicu jika Anda mengaktifkan **Webhook** di pengaturan notifikasi saran aturan, dan hanya untuk proses yang menghasilkan saran. |

Event tingkat entitas (`user.status.updated`, `business.status.updated`) juga bisa dipicu sebagai akibat dari aktivitas transaksi - lihat [webhook entitas](https://docs.didit.me/id/entities/webhooks.md).

> [!NOTE]
>
> Aplikasi sandbox tidak mengirim webhook `transaction.created` atau `transaction.status.updated`. Untuk menguji event tersebut, gunakan aplikasi live atau **API & Webhooks → Try Webhook** di konsol. Pertukaran Travel Rule di sandbox mengirim webhook `travel_rule.status.updated` dengan `environment: sandbox`; gunakan [Didit Test VASP](https://docs.didit.me/id/transaction-monitoring/travel-rule-sandbox.md) untuk menguji pertukaran sebelum beralih ke produksi.

## Struktur envelope

Body webhook Didit bersifat **datar**: kolom event berada di level teratas bersama kolom envelope, dan tidak ada objek `data` bertingkat.
Setiap pengiriman memuat:

| Kolom | Deskripsi |
| - | - |
| `webhook_type` | Nama event, misalnya `transaction.created`. |
| `event_id` | Identifier stabil untuk event - `event_id` yang sama dipakai ulang pada percobaan ulang, jadi deduplikasi berdasarkan kolom ini. |
| `timestamp` / `created_at` | Detik Unix epoch saat pengiriman dilakukan. |
| `application_id` | Aplikasi yang terkait dengan event tersebut. |
| `environment` | `live` atau `sandbox`. |

## Bentuk payload

### `transaction.created` dan `transaction.status.updated`

Kedua event ini memakai satu payload yang sama - ringkasan singkat ditambah blok [`action_required`](https://docs.didit.me/id/transaction-monitoring/sdk-transaction-submission#tindakan-yang-diperlukan-pengguna) (atau `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 ini sengaja dibuat ringkas: saat menerima, ambil data lengkap dengan [`GET /v3/transactions/{transaction_id}/`](https://docs.didit.me/id/management-api/transactions/get). Endpoint ini mengembalikan detail lengkap (pihak terkait, `rule_runs`, `provider_results`, `travel_rule`, `cost_breakdown`, `decision_reason_code`), termasuk *alasan* perubahan status.

### `transaction_rule_suggestions.created`

Event ini berkaitan dengan kumpulan aturan Anda, bukan satu transaksi, sehingga tidak memuat ID transaksi.
Event ini tidak pernah berisi data pribadi: hanya ID saran, jenis, judul, dan jumlah.

```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": {}
    }
  ]
}
```

| Kolom | Deskripsi |
| - | - |
| `run_id` | Proses saran yang menghasilkan saran baru. |
| `open_count` | Jumlah saran yang masih terbuka untuk aplikasi setelah proses ini. |
| `suggestions[]` | Saran yang dibuat proses ini, masing-masing dengan `suggestion_id`, `kind`, `title`, `rule_id` (bernilai null jika saran mengusulkan aturan baru), dan `expected_impact`. |

Kolom envelope yang biasa (`event_id`, `timestamp`, `application_id`, `environment`) ditambahkan ke body.

### `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"
}
```

Event ini dipicu pada setiap transisi, termasuk perpindahan ke `FINISHED`, `CANCELLED`, atau `EXPIRED`; setelah transfer mencapai salah satu status tersebut, tidak ada event travel rule lagi yang dikirim untuknya.
Sebuah event dapat mengulang status sebelumnya, misalnya ketika pengiriman ulang tidak menemukan rail dan transfer tetap berada di `COUNTERPARTY_VASP_NOT_FOUND`.
`rail` bernilai `INTERNAL`, `TRP`, `GTR`, `TRUST`, `VERIFYVASP`, `SYGNA`, `EMAIL`, `TEST_VASP` (transfer sandbox yang dijawab oleh Didit Test VASP, yang juga memuat `test_vasp: true` dan `test_vasp_scenario`), atau `""` selama belum ada rail yang dipilih (selalu `""` pada pendaftaran sunrise).

Saat transaksi dibuat, untuk pendaftaran sunrise, permintaan `TRP` masuk, atau leg masuk rail `INTERNAL`, Didit mengirim `transaction.created` terlebih dahulu, lalu event `travel_rule.status.updated` untuk transaksi tersebut setelahnya.
Aturan yang mengubah status transaksi pada langkah itu juga dapat mengirim `transaction.status.updated` sebelum `transaction.created`.
Pendaftaran sunrise yang terhubung ke transaksi deposit yang sudah Anda buat tidak mengirim `transaction.created`.
Setiap webhook dikirim dan dicoba ulang secara terpisah, sehingga event yang dicoba ulang bisa tiba setelah event yang lebih baru: gunakan `transaction_id` sebagai kunci handler Anda, dan ambil kembali data transaksi ketika Anda memerlukan status terkini.

## Contoh alur status

### Persetujuan sederhana

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

### Tinjau lalu setujui

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

### Verifikasi tambahan

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

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

Dua event pertama berasal dari permintaan yang sama, dan `transaction.created` masuk antrean lebih dulu; setiap webhook dikirim secara terpisah, jadi jangan mengandalkan urutan kedatangan.
Transfer yang dikirim melalui TRUST tidak pernah menerima event `COMPLETED`, karena TRUST tidak mengirim balasan yang bisa dibaca Didit: transfer tetap di `AWAITING_COUNTERPARTY` hingga batas waktu konfirmasi, lalu berakhir di `EXPIRED`. Melalui GTR, VerifyVASP, dan Sygna Bridge, Didit membaca jawaban pihak lawan dari jaringan, dan event mengikuti jawaban tersebut.

## Verifikasi tanda tangan

Setiap pengiriman ditandatangani dengan HMAC-SHA256 menggunakan rahasia bersama tujuan webhook dan membawa tiga header tanda tangan serta satu timestamp:

| Header | Skema |
| - | - |
| `X-Signature-V2` | HMAC atas JSON kanonis (kunci terurut, Unicode dipertahankan) - **direkomendasikan**. |
| `X-Signature` | HMAC atas JSON kanonis yang di-escape ASCII (lama). |
| `X-Signature-Simple` | HMAC atas `"{timestamp}:{session_id}:{status}:{webhook_type}"` - **tidak digunakan lagi**. Payload transaksi tidak memiliki `session_id`, sehingga ini menandatangani `"{timestamp}::{status}:{webhook_type}"`: ini tidak mengidentifikasi transaksi maupun jumlah atau skornya. Payload `travel_rule.status.updated` juga tidak memiliki `status`, sehingga ini menandatangani `"{timestamp}:::travel_rule.status.updated"`. Verifikasi dengan `X-Signature-V2`, dan lihat [pemberitahuan penghentian](https://docs.didit.me/id/integration/webhooks.md#x-signature-simple-sudah-tidak-digunakan-lagi). |
| `X-Timestamp` | Detik sejak epoch Unix - tolak pengiriman yang berusia lebih dari 5 menit. |

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

Lihat [referensi webhook](https://docs.didit.me/id/integration/webhooks.md#verifikasi-tanda-tangan) untuk kode verifikasi lengkap, termasuk pemeriksaan apakah timestamp masih dalam batas waktu yang diizinkan.

## Coba lagi dan jaminan pengiriman

* Saat `5xx`, `404`, timeout, atau kegagalan koneksi, Didit mencoba lagi hingga **2 kali**: sekitar 1 menit setelah kegagalan awal, lalu sekitar 4 menit lagi setelahnya.
* Pengiriman ulang memakai `event_id` yang sama - lakukan deduplikasi berdasarkan ID tersebut di handler Anda.
* Setelah kegagalan terakhir, pengiriman untuk event tersebut dihentikan dan organisasi Anda diberi tahu; tujuan webhook tidak pernah dinonaktifkan secara diam-diam.
* Kembalikan `2xx` dengan cepat (masukkan ke antrean, lalu proses) - ACK yang lambat berisiko menyebabkan timeout dan pengiriman duplikat.

## Idempotensi

Proses setiap webhook tepat satu kali dengan menyimpan `event_id` di penyimpanan deduplikasi Anda sendiri. Pengiriman ulang event yang sama dapat terjadi jika endpoint Anda mengirim ACK secara lambat atau koneksi terputus di tengah permintaan.

## Berlangganan

```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"
    ]
  }'
```

## Langkah selanjutnya

> [card] [Referensi webhook](https://docs.didit.me/id/integration/webhooks.md)
>
> Tujuan, penandatanganan, coba lagi.

> [card] [Status](https://docs.didit.me/id/transaction-monitoring/statuses.md)
>
> Mesin status lengkap.

> [card] [Panduan integrasi](https://docs.didit.me/id/transaction-monitoring/integration-guide.md)
>
> Menangani webhook di produksi.


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