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

# Pengiriman Transaksi SDK - KYT Sisi Klien

> Submit transaksi dari aplikasi via Didit dengan token terbatas: intelijen perangkat otomatis, pengikatan subjek anti-spoofing, verifikasi dompet.

Didit SDK dapat mengirimkan [transaksi yang dipantau](https://docs.didit.me/id/transaction-monitoring/transactions.md) langsung dari perangkat pengguna akhir, termasuk transfer [Travel Rule](https://docs.didit.me/id/transaction-monitoring/travel-rule.md) dan skrining kripto.
Backend Anda membuat token dengan cakupan terbatas yang berumur pendek; SDK menangani sisanya.
SDK otomatis melampirkan intelijen perangkat. Ketika transfer Travel Rule memerlukan bukti kontrol dompet, SDK langsung meluncurkan alur tersebut.
Sesi verifikasi yang diperlukan tidak pernah diluncurkan otomatis. Sesi itu dikembalikan dalam respons agar aplikasi Anda meluncurkannya dengan integrasi verifikasi Didit Anda sendiri; lihat [Tindakan pengguna yang diperlukan](#tindakan-yang-diperlukan-pengguna) di bawah.

## Ringkasan

Pengiriman transaksi dari sisi klien dibangun di atas tiga jaminan:

* **Pengikatan subjek yang tahan spoofing.** Token dibatasi ke satu `vendor_data` (ID pengguna Anda) saat dibuat. Identitas subjek dari setiap transaksi yang dikirim dengan token tersebut ditegakkan di sisi server berdasarkan pengikatan ini, sehingga klien yang dimanipulasi tidak dapat mengirim atas nama pengguna lain, dan payload tidak perlu membawa kredensial.
* **Intelijen perangkat otomatis.** SDK mengumpulkan sidik jari perangkat dan mengirimkannya bersama permintaan. Didit menurunkan alamat IP dan user agent di sisi server. Konteks perangkat gabungan dilampirkan ke subjek transaksi tanpa pustaka tambahan, tanpa token perangkat terpisah, dan tanpa integrasi tambahan di pihak Anda. Konteks ini mendukung [aturan](https://docs.didit.me/id/transaction-monitoring/rules.md) berbasis perangkat serta korelasi berdasarkan perangkat lintas sesi dan transaksi.
* **Pembuktian kepemilikan dompet yang diluncurkan otomatis.** Ketika transaksi yang dibuat mengharuskan pengguna akhir membuktikan kontrol atas dompet, respons membawa blok `action_required` bertipe `wallet_ownership`. SDK langsung membuka alur tersebut, lalu menyegarkan transaksi setelah selesai, tanpa loop polling atau pengiriman tautan yang harus Anda bangun sendiri. Tindakan `verification_session` yang diperlukan berbeda: tindakan itu tidak pernah diluncurkan otomatis dan dikembalikan dalam blok yang sama agar aplikasi Anda meluncurkannya dengan integrasi verifikasi Didit miliknya sendiri.

> [!NOTE]
>
> [`POST /v3/transactions/`](https://docs.didit.me/id/transaction-monitoring/transactions) di sisi server tetap menjadi pilihan yang tepat ketika tidak ada pengguna akhir: transfer back-office, impor batch, atau event yang dilaporkan oleh sistem Anda sendiri.
> Kedua jalur membuat rekaman transaksi yang sama, menjalankan aturan yang sama, dan mengirim [webhook](https://docs.didit.me/id/transaction-monitoring/webhooks.md) yang sama.

## Alur

**1. Buat token transaksi**

Backend Anda memanggil `POST /v3/transactions/sdk-token/` dengan kunci API Anda dan `vendor_data` pengguna, lalu menyerahkan `sdk_token` yang dikembalikan ke aplikasi Anda.

**2. Kirim dari perangkat**

Aplikasi memanggil `submitTransaction` pada SDK. Pemanggilan ini mengirim payload ke `POST /v1/transactions/` dengan header `X-Transaction-Token` dan pengayaan perangkat otomatis.

**3. Tangani tindakan yang diperlukan**

Jika `action_required.type` adalah `wallet_ownership`, SDK otomatis meluncurkan widget, lalu menyegarkan transaksi melalui `GET /v1/transactions/{transaction_id}/`.
Jika tipenya `verification_session`, SDK tidak meluncurkan apa pun. Baca `action_required` dan mulai alur verifikasi Anda sendiri dengan `session_token`-nya (atau `url` di web).

## Membuat token transaksi

Buat token dari backend Anda dengan `POST /v3/transactions/sdk-token/`. Autentikasinya menggunakan kunci API yang sama dengan endpoint transaksi `/v3/` lainnya.

```bash theme={null}
curl -X POST https://verification.didit.me/v3/transactions/sdk-token/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "vendor_data": "user-042",
    "ttl_seconds": 900,
    "max_uses": 3
  }'
```

```json theme={null}
{
  "sdk_token": "q7Zt...urlsafe",
  "expires_at": "2026-07-07T12:15:00Z"
}
```

| Kolom | Tipe | Deskripsi |
| - | - | - |
| `vendor_data` | string | **Wajib diisi.** Pengenal internal Anda untuk pengguna akhir yang menjadi cakupan token ini. Identitas subjek setiap transaksi yang dikirim dengan token ini ditegakkan dari nilai ini. |
| `ttl_seconds` | integer | Masa berlaku token dalam detik. Default-nya `900` (15 menit); maksimum `86400` (24 jam). |
| `max_uses` | integer | Opsional. Jumlah maksimum pengiriman berhasil yang diizinkan dengan token ini. Jangan sertakan parameter ini untuk penggunaan tanpa batas selama TTL. |

Alias camelCase (`vendorData`, `ttlSeconds`, `maxUses`) juga diterima, seperti pada API `/v3/` lainnya.

> [!WARNING]
>
> **Jangan pernah menyertakan kunci API Anda di aplikasi.**
> Kunci API tetap berada di backend Anda dan digunakan untuk membuat token. Hanya `sdk_token` berumur pendek yang dikirim ke perangkat.
> Buat token baru untuk setiap sesi pengguna, alih-alih menggunakan ulang token berumur panjang. Batasi `max_uses` ketika jumlah pengiriman yang diharapkan sudah diketahui.

## Mengirim dari perangkat

SDK mengirim ke `POST /v1/transactions/` dengan token di header `X-Transaction-Token`.
Payload wire menggunakan camelCase. Metode `submitTransaction` membangunnya untuk Anda, jadi Anda hanya perlu menanganinya langsung saat memanggil endpoint dari klien HTTP Anda sendiri:

```bash theme={null}
curl -X POST https://verification.didit.me/v1/transactions/ \
  -H "X-Transaction-Token: SDK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "txnId": "wd-2026-07-07-0042",
    "type": "travelRule",
    "info": {
      "direction": "out",
      "amount": "0.25",
      "currency": "ETH",
      "currencyType": "crypto",
      "cryptoParams": { "crypto_chain": "ETH" }
    },
    "subject": { "fullName": "Jane Doe" },
    "counterparty": {
      "fullName": "Ana Diaz",
      "paymentMethod": { "type": "unhosted_wallet", "accountId": "0xBeneficiaryWallet01" }
    },
    "travelRule": {
      "status": "AWAITING_COUNTERPARTY",
      "beneficiaryData": { "wallet_address": "0xBeneficiaryWallet01", "name": "Ana Diaz" }
    },
    "includeCryptoScreening": true
  }'
```

Setiap kolom tingkat atas dipetakan ke padanan [`POST /v3/transactions/`](https://docs.didit.me/id/transaction-monitoring/transactions)-nya:

| Kolom wire | Padanan `/v3/` | Catatan |
| - | - | - |
| `txnId` | `transaction_id` | **Wajib diisi.** Pengenal unik Anda untuk transaksi. |
| `txnDate` | `transaction_at` | Timestamp ISO-8601; secara default menggunakan waktu sekarang. |
| `zoneId` | `time_zone` | ID zona waktu IANA, misalnya `Europe/Madrid`. |
| `type` | `transaction_category` | **Wajib diisi.** `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet`, dan kategori lainnya. |
| `info` | `transaction_details` | **Wajib diisi.** `direction`, `amount`, `currency` (wajib), serta `currencyType`, `amountInDefaultCurrency`, `defaultCurrencyCode`, `paymentDetails`, `paymentTxnId`, `type` (tipe tindakan), `cryptoParams`. |
| `subject`, `counterparty` | `subject`, `counterparty` | Peserta: `type` (tipe entitas), `externalUserId`, `fullName`, `firstName`, `lastName`, `dob`, `address`, `institutionInfo`, `device`, `paymentMethod` (`type`, `accountId`, `issuingCountry`). `paymentMethod.type` pada counterparty dengan nilai `unhosted_wallet` menyatakan tujuan sebagai dompet swakelola - lihat [di bawah](#mendeklarasikan-dompet-swakelola). |
| `props` | `custom_properties` | Pasangan kunci-nilai kustom, yang dapat digunakan dalam aturan sebagai `custom_values.<key>`. |
| `travelRule` | `travel_rule_details` | `status`, `protocol`, `required`, `obligationsCount`, `originatorData`, `beneficiaryData`, `metadata`; `status` wajib ada setiap kali Anda mengirim blok ini, jika tidak permintaan gagal dengan `400`. Saat Travel Rule diaktifkan, Didit mengabaikan `status` apa pun yang Anda kirim dan menjalankan pertukaran itu sendiri. |
| `includeCryptoScreening` | `include_crypto_screening` | Penggantian skrining kripto per transaksi. |

Respons berisi detail transaksi snake\_case yang sama dengan endpoint `/v3/`, ditambah blok `action_required`:

```json theme={null}
{
  "uuid": "22222222-3333-4444-5555-666666666666",
  "txn_id": "wd-2026-07-07-0042",
  "transaction_type": "travelRule",
  "direction": "OUTBOUND",
  "status": "APPROVED",
  "amount": "0.25",
  "currency": "ETH",
  "travel_rule": {
    "uuid": "33333333-4444-5555-6666-777777777777",
    "status": "UNCONFIRMED_OWNERSHIP",
    "required": true
  },
  "action_required": {
    "type": "wallet_ownership",
    "url": "https://verify.didit.me/wallet-ownership/3s9x...urlsafe",
    "widget_session_id": "77777777-8888-9999-aaaa-bbbbbbbbbbbb",
    "expires_at": "2026-07-07T13:00:00Z"
  }
}
```

### Mendeklarasikan dompet swakelola

Contoh di atas menyatakan tujuan sebagai dompet swakelola milik pengguna: `paymentMethod.type` pada counterparty adalah `unhosted_wallet`.
Deklarasi tersebut mengubah cara [pertukaran Travel Rule](https://docs.didit.me/id/transaction-monitoring/travel-rule.md#dompet-swakelola-yang-dideklarasikan) berjalan:

* Didit melewati routing VASP pihak lawan sepenuhnya - tidak ada VASP di balik dompet swakelola - dan membuat transfer langsung dalam status `UNCONFIRMED_OWNERSHIP`.
* Saat `auto_wallet_verification` aktif di [pengaturan Travel Rule](https://docs.didit.me/id/transaction-monitoring/travel-rule.md#pengaturan) Anda (default-nya), sesi [widget kepemilikan dompet](https://docs.didit.me/id/transaction-monitoring/wallet-ownership-widget.md) dibuat otomatis dan dikembalikan dalam `action_required`, persis seperti pada respons di atas.
* SDK meluncurkan widget secara otomatis; setelah pengguna membuktikan kendali atas dompet, transfer menjalankan pencocokan nama dan beralih ke `COMPLETED`, atau ke `COUNTERPARTY_MISMATCHED_DATA` jika nama tidak cocok dengan pemilik yang tercatat di buku alamat dompet.

Tanpa deklarasi, Didit justru mencoba [jalur](https://docs.didit.me/id/transaction-monitoring/travel-rule.md#ikhtisar) secara berurutan dan hanya beralih ke alur kepemilikan jika tidak ada VASP pihak lawan yang ditemukan.
Deklarasikan dompet kapan pun produk Anda sudah mengetahui bahwa target penarikan adalah dompet milik pengguna sendiri. Dengan begitu, Anda menghemat satu kali percobaan routing yang gagal dan pengguna langsung masuk ke alur pembuktian.

### Penegakan subjek

Tiga hal berikut selalu ditentukan oleh token, tidak pernah oleh payload:

* Aplikasi tempat transaksi dibuat.
* Identitas subjek: `subject.externalUserId` dalam payload diabaikan dan digantikan dengan `vendor_data` dari token.
* Konteks perangkat subjek (lihat di bawah); kolom yang diturunkan dari server selalu menang jika terjadi konflik dengan apa pun yang dikirim dalam `subject.device`.

Respons `401` dari endpoint ini berarti token ditolak: token tidak ada, tidak dikenal, dicabut, melebihi `max_uses`-nya, atau kedaluwarsa.
Respons token kedaluwarsa selalu memuat kata `expired` dalam detail error, dan itulah cara SDK membedakan error `expired_token` dari error `invalid_token`.

### Membaca transaksi kembali

`GET /v1/transactions/{transaction_id}/` mengembalikan detail yang sama dengan autentikasi `X-Transaction-Token` yang sama.
SDK memanggilnya secara otomatis setelah alur tindakan yang diluncurkan selesai atau jendela waktunya tertutup, sehingga status akhir tidak pernah bergantung pada satu event atau notifikasi saja.
Anda dapat memanggilnya sendiri untuk memeriksa ulang `status` dan `action_required` kapan pun selama token masih berlaku. Dari backend Anda, [`GET /v3/transactions/{transaction_id}/`](https://docs.didit.me/id/transaction-monitoring/transactions) mengembalikan catatan yang sama.

## Intelijen perangkat otomatis

Setiap pengiriman membawa intelijen perangkat tanpa pekerjaan integrasi apa pun di sisi Anda:

* SDK mengumpulkan sidik jari perangkat yang menjaga privasi (skema `didit-fp-v2`: sinyal platform dan navigator, ID perangkat yang persisten, skor bot, dan hash gabungan) lalu mengirimkannya bersama permintaan.
* Didit menurunkan alamat IP, user agent, dan accept-language di sisi server dari koneksi itu sendiri, sehingga nilai-nilai tersebut tidak dapat dipalsukan lewat payload.

Hasil gabungan dinormalisasi ke dalam konteks perangkat peserta subjek, lalu di-upsert ke dalam graf perangkat, yaitu graf yang sama dengan yang juga diisi oleh sesi KYC.
Hal ini membuat transaksi dapat dikorelasikan berdasarkan perangkat di seluruh basis pengguna Anda ("terkait berdasarkan perangkat"), mengalirkan sinyal perangkat dan jaringan ke dalam [aturan](https://docs.didit.me/id/transaction-monitoring/rules.md) dan [pengayaan IP](https://docs.didit.me/id/transaction-monitoring/ip-enrichment.md), serta muncul di detail transaksi pada [Console](https://docs.didit.me/id/transaction-monitoring/console.md).

Transaksi `/v3/` yang dikirim dari server dapat tetap meneruskan objek `device_context` eksplisit untuk setiap peserta. Pada jalur SDK, Anda mendapatkan versi yang lebih kuat dari data yang sama tanpa biaya tambahan.

## Tindakan yang diperlukan pengguna

Beberapa transaksi memerlukan tindakan pengguna akhir sebelum dapat diselesaikan: sebuah aturan dapat mensyaratkan sesi verifikasi, atau transfer Travel Rule dapat mensyaratkan bukti bahwa dompet tujuan benar-benar milik pengguna.
Kedua kasus ini muncul di tempat yang sama, yaitu blok `action_required` pada respons submit (dan pada setiap pembacaan transaksi berikutnya):

```json theme={null}
"action_required": {
  "type": "verification_session",
  "url": "https://verify.didit.me/session/...",
  "session_id": "11111111-2222-3333-4444-555555555555",
  "session_token": "SESSION_TOKEN",
  "status": "Not Started"
}
```

```json theme={null}
"action_required": {
  "type": "wallet_ownership",
  "url": "https://verify.didit.me/wallet-ownership/3s9x...urlsafe",
  "widget_session_id": "77777777-8888-9999-aaaa-bbbbbbbbbbbb",
  "expires_at": "2026-07-07T13:00:00Z"
}
```

| Tipe | Kapan muncul | Yang dilakukan pengguna |
| - | - | - |
| `verification_session` | Sebuah [tindakan aturan](https://docs.didit.me/id/transaction-monitoring/rules.md) mensyaratkan verifikasi pengguna tambahan dan transaksi berpindah ke `AWAITING_USER`. | Menyelesaikan sesi verifikasi yang di-hosting (misalnya pemeriksaan ulang biometrik), yang diluncurkan oleh aplikasi Anda - lihat [Meluncurkan sesi verifikasi](#meluncurkan-sesi-verifikasi) di bawah. |
| `wallet_ownership` | Transfer Travel Rule memerlukan bukti kendali dompet dari pengguna akhir - tujuan telah [dideklarasikan sebagai dompet swakelola](#mendeklarasikan-dompet-swakelola) (`UNCONFIRMED_OWNERSHIP`), atau transfer keluar tidak menemukan VASP pihak lawan - dan `auto_wallet_verification` diaktifkan di [pengaturan Travel Rule](https://docs.didit.me/id/transaction-monitoring/travel-rule.md#pengaturan) Anda. Didit otomatis menerbitkan sesi [widget kepemilikan dompet](https://docs.didit.me/id/transaction-monitoring/wallet-ownership-widget.md) yang terikat ke transaksi. | Membuktikan kendali atas dompet melalui penandatanganan pesan (pada rantai yang didukung), tangkapan layar, atau deklarasi mandiri, sesuai dengan yang diizinkan pengaturan Travel Rule Anda, tetapi tidak pernah melalui Satoshi test pada sesi yang diterbitkan otomatis - SDK meluncurkannya secara otomatis untuk Anda. |

Jika keduanya dapat berlaku pada transaksi yang sama, `wallet_ownership` didahulukan, karena bukti kepemilikan yang belum ada akan memblokir pertukaran Travel Rule itu sendiri.
`action_required` menjadi `null` setelah tindakan yang tertunda diselesaikan.
Kolom `remediation` yang ada tetap dipertahankan demi kompatibilitas ke belakang; `action_required` adalah blok kanonis.

### Kontrak peluncuran otomatis

`autoLaunchAction` (default `true`) secara otomatis hanya meluncurkan tindakan `wallet_ownership`:

* **Web** - dibuka di modal overlay milik SDK, dengan penyelesaian dideteksi melalui pesan yang divalidasi asal-usulnya ditambah polling transaksi yang terbatas (tidak pernah bergantung pada satu event saja).
* **iOS / Android** (dan pembungkus React Native serta Flutter, yang meneruskan ke SDK native) - ditampilkan sebagai **pengalaman kepemilikan dompet native** milik SDK (SwiftUI / Jetpack Compose) ketika `useNativeWalletOwnership` bernilai `true` (default), dengan cadangan ke `SFSafariViewController` / Chrome Custom Tab, dan terakhir ke browser default. Setelah alur ditutup atau aplikasi kembali ke latar depan, SDK membaca ulang transaksi secara otomatis.

Tindakan `verification_session` tidak pernah diluncurkan otomatis: tindakan itu selalu muncul di `action_required` agar aplikasi Anda memulainya dengan integrasi verifikasi Didit milik Anda sendiri.

> [!WARNING]
>
> Widget kepemilikan dompet tidak boleh pernah dirender di dalam webview seluler tertanam - deep link dompet tidak dapat keluar darinya dan alur pembuktian akan buntu.
> SDK sudah menggunakan tampilan browser yang tepat saat meluncurkan otomatis; pertahankan aturan ini jika Anda menangani `url` sendiri.
> Lihat [panduan widget kepemilikan dompet](https://docs.didit.me/id/transaction-monitoring/wallet-ownership-widget.md#buka-url-yang-dikembalikan) untuk detailnya.

Setelah alur kepemilikan dompet yang diluncurkan otomatis oleh SDK selesai atau ditutup, SDK membaca ulang transaksi dan menyerahkan hasil yang diperbarui ke callback `onActionCompleted` Anda.
Tindakan `verification_session` tidak termasuk dalam siklus itu, karena aplikasi Anda yang meluncurkannya - lihat bagian berikutnya.

### Meluncurkan sesi verifikasi

Ketika `action_required.type` bernilai `verification_session`, baca blok dari `result.actionRequired` dan serahkan ke integrasi verifikasi Didit yang sudah ada:

```typescript Web theme={null}
if (result.actionRequired?.type === "verification_session") {
  DiditSdk.shared.startVerification({ url: result.actionRequired.url });
}
```

```swift iOS theme={null}
if actionRequired.type == "verification_session" {
    try await DiditSdk.shared.startVerification(token: actionRequired.sessionToken)
}
```

```kotlin Android theme={null}
if (actionRequired.type == "verification_session") {
    DiditSdk.startVerification(token = actionRequired.sessionToken) { result ->
        // handle the verification result
    }
}
```

```typescript React Native theme={null}
if (actionRequired.type === "verification_session") {
  await startVerification(actionRequired.sessionToken);
}
```

```dart Flutter theme={null}
if (actionRequired.type == 'verification_session') {
  await DiditSdk.startVerification(actionRequired.sessionToken);
}
```

Setelah alur verifikasi Anda selesai, panggil `GET /v1/transactions/{transaction_id}/` (atau tunggu [webhook transaksi](#webhook)) untuk mengambil status transaksi yang diperbarui.

### Contoh: biometrik tambahan untuk penarikan bernilai tinggi

Ini adalah alur terbaik: batasi transaksi berisiko - jumlah besar, penerima baru, perangkat baru - dengan pemeriksaan ulang biometrik dari orang yang sama yang Anda verifikasi saat onboarding.

Pengambilalihan akun terlihat persis seperti ini: kata sandi benar, kode 2FA benar (hasil phishing atau SIM swap), dan penarikan ke dompet yang belum pernah Anda lihat.
Setiap pertahanan berbasis kredensial sudah gagal pada titik itu.
Namun, wajah tidak demikian: penyerang tidak dapat lolos dari swafoto dengan pemeriksaan liveness yang harus cocok dengan wajah yang direkam selama KYC onboarding pemilik sah.

**1. Buat aturan sekali, di konsol**

**Transactions → Rules → Create Rule**: kondisi `amount gte 5000`, tindakan **Change status → `AWAITING_USER`**, dan pilih alur kerja **Autentikasi Biometrik** Anda pada tindakan tersebut.
Tambahkan kondisi risiko lain yang Anda inginkan - pihak lawan yang baru pertama kali, sidik jari perangkat baru, perubahan negara IP - sebagai kondisi tambahan atau aturan tambahan.

**2. Kirim penarikan seperti biasa**

Tidak ada yang berubah pada kode submit Anda. Ketika aturan terpicu, respons kembali dengan `status: "AWAITING_USER"` dan blok `action_required` bertipe `verification_session`:

```json theme={null}
{
  "status": "AWAITING_USER",
  "action_required": {
    "type": "verification_session",
    "url": "https://verify.didit.me/session/...",
    "session_id": "11111111-2222-3333-4444-555555555555",
    "session_token": "SESSION_TOKEN",
    "status": "Not Started"
  }
}
```

**3. Luncurkan pemeriksaan ulang**

Serahkan `session_token` (atau `url`) ke integrasi verifikasi Didit Anda yang sudah ada, persis seperti di [Meluncurkan sesi verifikasi](#meluncurkan-sesi-verifikasi).
Karena sesi dibuat untuk `vendor_data` yang sama dengan subjek transaksi, Didit sudah memiliki wajah referensi terverifikasi pengguna dari onboarding - swafoto liveness yang disetujui, foto chip paspor, atau potret dokumen.
Pengguna mengambil satu swafoto langsung; Didit menjalankan liveness plus pencocokan wajah 1:1 terhadap referensi yang tersimpan itu. Tidak ada pemindaian ulang dokumen, tidak ada onboarding ulang - proses yang hanya memerlukan beberapa detik, dan hanya pada transaksi yang memang memerlukannya.

**4. Selesaikan berdasarkan hasil**

Ketika sesi selesai, transaksi dievaluasi ulang secara otomatis: `APPROVED` jika pemeriksaan ulang berhasil, `DECLINED` jika gagal, dengan webhook `transaction.status.updated` dalam kedua kasus (`reason: remediation_completed` pada linimasa transaksi).
Aplikasi Anda menerima hasilnya melalui callback bergaya `onActionCompleted`, `GET /v1/transactions/{transaction_id}/`, atau webhook.

Ini berhasil karena onboarding KYC, pemeriksaan ulang biometrik, dan keputusan transaksi berjalan di satu platform: wajah yang diambil saat onboarding menjadi referensi untuk step-up, graf perangkat menghubungkan sesi dengan transaksi, dan mesin aturan menyatukan keduanya. Tidak ada kode integrasi antarvendor, tidak ada ekspor gambar wajah, dan tidak ada kontrak biometrik terpisah.
Pemeriksaan ulang ditagihkan sebagai fitur Autentikasi Biometrik standar ([\$0.10](https://docs.didit.me/id/getting-started/pricing.md)) hanya ketika benar-benar dijalankan.

## SDK Web

`submitTransaction` tersedia di singleton Web SDK saat ini:

```typescript theme={null}
import { DiditSdk } from "@didit-protocol/sdk-web";

const result = await DiditSdk.shared.submitTransaction({
  transactionToken: sdkToken, // minted by your backend via POST /v3/transactions/sdk-token/
  transaction: {
    txnId: "wd-2026-07-07-0042",
    txnDate: new Date().toISOString(),
    zoneId: "Europe/Madrid",
    category: "travelRule",
    details: {
      direction: "out",
      amount: "0.25",
      currency: "ETH",
      currencyType: "crypto",
      paymentDetails: "ETH withdrawal to self-hosted wallet",
      cryptoParams: { crypto_chain: "ETH" }
    },
    subject: {
      type: "individual",
      fullName: "Jane Doe"
      // no externalUserId needed: the subject identity comes from the token
    },
    counterparty: {
      type: "individual",
      fullName: "Ana Diaz",
      paymentMethod: {
        type: "unhosted_wallet",
        accountId: "0xBeneficiaryWallet01"
      }
    },
    travelRule: {
      status: "AWAITING_COUNTERPARTY", // ignored when Travel Rule is enabled: Didit drives the exchange
      beneficiaryData: {
        wallet_address: "0xBeneficiaryWallet01",
        name: "Ana Diaz"
      }
    },
    includeCryptoScreening: true
  },
  autoLaunchAction: true, // default: auto-launches wallet_ownership only; verification_session comes back in result.actionRequired for you to launch
  onActionCompleted: (refreshed) => {
    console.log(refreshed.status, refreshed.travelRuleStatus);
  }
});

console.log(result.transactionId, result.status, result.actionRequired?.type);
```

Hasilnya berupa `{ transactionId, status, travelRuleStatus?, actionRequired? }`.
Di payload tingkat SDK, Anda menulis `category`, `details`, dan `customProperties`; SDK memetakannya ke alias wire (`type`, `info`, `props`) untuk Anda.

Error memiliki tipe `DiditTransactionError`:

```typescript theme={null}
import { DiditSdk, DiditTransactionError } from "@didit-protocol/sdk-web";

try {
  await DiditSdk.shared.submitTransaction({ transactionToken, transaction });
} catch (error) {
  if (error instanceof DiditTransactionError) {
    switch (error.type) {
      case "expired_token": // mint a fresh token and retry
        break;
      case "invalid_token": // rejected: unknown, revoked, or over max_uses
        break;
      case "validation": // inspect error.fieldErrors for per-field details
        break;
      case "network": // retry with backoff
        break;
    }
  }
}
```

## SDK Native

> [!NOTE]
>
> API `submitTransaction` native tersedia di iOS dan Android SDK 4.1.0 (serta rilis React Native dan Flutter yang menggunakan versi tersebut).
> Berbeda dengan Web SDK, payload native langsung menggunakan nama field wire: `type`, `info`, dan `props`.

```swift Swift theme={null}
import DiditSDK

let payload = DiditTransactionPayload(
    txnId: "wd-2026-07-07-0042",
    type: "travelRule",
    info: DiditTransactionInfo(
        direction: "out",
        amount: 0.25,
        currency: "ETH",
        currencyType: "crypto",
        cryptoParams: ["crypto_chain": "ETH"]
    ),
    subject: DiditTransactionParticipant(type: "individual", fullName: "Jane Doe"),
    counterparty: DiditTransactionParticipant(
        type: "individual",
        fullName: "Ana Diaz",
        paymentMethod: DiditTransactionPaymentMethod(type: "unhosted_wallet", accountId: "0xBeneficiaryWallet01")
    ),
    travelRule: DiditTravelRuleInfo(
        status: "AWAITING_COUNTERPARTY", // required by the API; ignored when Travel Rule is enabled
        beneficiaryData: ["wallet_address": "0xBeneficiaryWallet01", "name": "Ana Diaz"]
    ),
    includeCryptoScreening: true
)

let result = try await DiditSdk.shared.submitTransaction(
    transactionToken: sdkToken,
    transaction: payload,
    options: DiditTransactionOptions(
        autoLaunchAction: true,           // default
        useNativeWalletOwnership: true,   // default: native flow, browser fallback
        onTransactionUpdated: { refreshed in
            print(refreshed.status ?? "", refreshed.travelRuleStatus ?? "")
        }
    )
)

print(result.transactionId, result.status ?? "", result.actionRequired?.type ?? "none")
// Errors: DiditTransactionError .invalidToken / .expiredToken / .validation(fieldErrors) / .network
```

```kotlin Kotlin theme={null}
import me.didit.sdk.DiditSdk
import me.didit.sdk.transactions.*

val payload = DiditTransactionPayload(
    txnId = "wd-2026-07-07-0042",
    type = "travelRule",
    info = DiditTransactionInfo(
        direction = "out",
        amount = 0.25,
        currency = "ETH",
        currencyType = "crypto",
        cryptoParams = mapOf("crypto_chain" to "ETH")
    ),
    subject = DiditTransactionParticipant(type = "individual", fullName = "Jane Doe"),
    counterparty = DiditTransactionParticipant(
        type = "individual",
        fullName = "Ana Diaz",
        paymentMethod = DiditTransactionPaymentMethod(
            type = "unhosted_wallet",
            accountId = "0xBeneficiaryWallet01"
        )
    ),
    travelRule = DiditTravelRule(
        status = "AWAITING_COUNTERPARTY", // required by the API; ignored when Travel Rule is enabled
        beneficiaryData = mapOf("wallet_address" to "0xBeneficiaryWallet01", "name" to "Ana Diaz")
    ),
    includeCryptoScreening = true
)

// suspend fun - call from a coroutine; DiditSdk.initialize() must have run first
val result = DiditSdk.submitTransaction(
    transactionToken = sdkToken,
    transaction = payload,
    options = DiditTransactionOptions(
        autoLaunchAction = true,          // default
        useNativeWalletOwnership = true,  // default: native flow, Custom Tab fallback
        onTransactionUpdated = { refreshed ->
            Log.d("Didit", "${refreshed.status} ${refreshed.travelRuleStatus}")
        }
    )
)

Log.d("Didit", "${result.transactionId} ${result.status} ${result.actionRequired?.type}")
// Errors: sealed DiditTransactionException - InvalidToken / ExpiredToken / Validation(fieldErrors) / Network
```

```typescript React Native theme={null}
import {
  submitTransaction,
  getTransaction,
  DiditTransactionError,
} from "@didit-protocol/sdk-react-native";

const result = await submitTransaction(sdkToken, {
  txnId: "wd-2026-07-07-0042",
  type: "travelRule",
  info: {
    direction: "out",
    amount: 0.25,
    currency: "ETH",
    currencyType: "crypto",
    cryptoParams: { crypto_chain: "ETH" },
  },
  subject: { type: "individual", fullName: "Jane Doe" },
  counterparty: {
    type: "individual",
    fullName: "Ana Diaz",
    paymentMethod: { type: "unhosted_wallet", accountId: "0xBeneficiaryWallet01" },
  },
  travelRule: {
    status: "AWAITING_COUNTERPARTY", // required by the API; ignored when Travel Rule is enabled
    beneficiaryData: { wallet_address: "0xBeneficiaryWallet01", name: "Ana Diaz" },
  },
  includeCryptoScreening: true,
}, {
  autoLaunchAction: true, // default
  onTransactionUpdated: (refreshed) => {
    console.log(refreshed.status, refreshed.travelRuleStatus);
  },
});

console.log(result.transactionId, result.status, result.actionRequired?.type);
// Errors: DiditTransactionError with code invalid_token | expired_token | validation | network
```

```dart Flutter theme={null}
import 'package:didit_sdk/sdk_flutter.dart';

final result = await DiditSdk.submitTransaction(
  sdkToken,
  DiditTransactionPayload(
    txnId: 'wd-2026-07-07-0042',
    type: 'travelRule',
    info: DiditTransactionInfo(
      direction: 'out',
      amount: 0.25,
      currency: 'ETH',
      currencyType: 'crypto',
      cryptoParams: {'crypto_chain': 'ETH'},
    ),
    subject: DiditTransactionParticipant(type: 'individual', fullName: 'Jane Doe'),
    counterparty: DiditTransactionParticipant(
      type: 'individual',
      fullName: 'Ana Diaz',
      paymentMethod: DiditTransactionPaymentMethod(
        type: 'unhosted_wallet',
        accountId: '0xBeneficiaryWallet01',
      ),
    ),
    travelRule: DiditTravelRule(
      status: 'AWAITING_COUNTERPARTY', // required by the API; ignored when Travel Rule is enabled
      beneficiaryData: {'wallet_address': '0xBeneficiaryWallet01', 'name': 'Ana Diaz'},
    ),
    includeCryptoScreening: true,
  ),
  options: DiditTransactionOptions(
    autoLaunchAction: true, // default
    onTransactionUpdated: (refreshed) {
      debugPrint('${refreshed.status} ${refreshed.travelRuleStatus}');
    },
  ),
);

debugPrint('${result.transactionId} ${result.status} ${result.actionRequired?.type}');
// Errors: DiditTransactionException with code invalid_token | expired_token | validation | network
```

Setiap SDK juga menyediakan `getTransaction(transactionToken, transactionId)` untuk membaca ulang transaksi dengan token yang sama. SDK memanggilnya untuk Anda setelah alur tindakan dimulai, dan Anda dapat memanggilnya sendiri kapan saja.

Di perangkat mobile, mekanisme pengiriman sidik jari perangkat sudah diterapkan pada semua panggilan jaringan SDK, sehingga pengiriman transaksi otomatis membawa intelijen perangkat yang sama.

## Tanpa SDK

Semua yang dilakukan SDK adalah HTTPS biasa, jadi tidak ada yang mewajibkan penggunaan SDK. Ada dua bentuk integrasi yang setara:

**Hanya sisi server (tanpa token sama sekali).** Kirim dengan [`POST /v3/transactions/`](https://docs.didit.me/id/management-api/transactions/create) dari backend Anda menggunakan kunci API Anda. Respons (dan setiap `GET /v3/transactions/{transaction_id}/` berikutnya) membawa blok `action_required` yang sama. Anda juga dapat menyertakan konteks `device` eksplisit per peserta, karena tidak ada SDK yang mengumpulkannya untuk Anda.

**Sisi klien dengan HTTP stack Anda sendiri.** Buat token dengan [`POST /v3/transactions/sdk-token/`](https://docs.didit.me/id/management-api/transactions/sdk-token), lalu panggil [`POST /v1/transactions/`](https://docs.didit.me/id/management-api/transactions/submit-sdk) dan [`GET /v1/transactions/{transaction_id}/`](https://docs.didit.me/id/management-api/transactions/get-sdk) dari perangkat dengan header `X-Transaction-Token`. Pengikatan subjek yang sama dan penurunan IP/user-agent di sisi server juga berlaku.

Lalu tangani `action_required` sendiri:

| `action_required.type` | Yang harus dilakukan |
| - | - |
| `wallet_ownership` | Kirimkan `url` ke pengguna dan buka di **lingkungan browser nyata**: tab biasa di web, `SFSafariViewController` / Chrome Custom Tabs di mobile, atau secara out-of-band melalui tautan email atau kode QR. Jangan pernah menampilkannya di dalam webview tersemat, karena deep link dompet tidak dapat keluar dari webview. |
| `verification_session` | Arahkan pengguna ke `url`, yaitu halaman verifikasi yang di-hosting standar Didit, atau berikan `session_token` ke integrasi verifikasi Didit apa pun yang sudah Anda jalankan. |

Dan deteksi penyelesaian tanpa callback dari klien:

* **Webhook (direkomendasikan).** `transaction.status.updated` dipicu saat tindakan yang tertunda menyelesaikan transaksi, dan `travel_rule.status.updated` dipicu pada setiap transisi pertukaran. Keduanya memuat informasi yang cukup untuk memperbarui catatan Anda tanpa polling.
* **Polling.** Baca ulang transaksi (`/v3/` dari backend Anda, atau `/v1/` dengan token selama token masih berlaku): `action_required` kembali menjadi `null` setelah diselesaikan, dan `status` / `travel_rule.status` memuat hasilnya.

Desain ini tidak memiliki ketergantungan sekali pakai: widget, sesi verifikasi, dan backend Anda semuanya bertemu di status transaksi persisten yang sama, sehingga tab yang ditutup atau webhook yang terlewat selalu dapat dipulihkan dengan membaca ulang transaksi.

## Webhook

Backend Anda tidak memerlukan aplikasi untuk mengirimkan laporan balik.
Ketika transaksi yang dikirim memerlukan tindakan pengguna, [webhook transaksi](https://docs.didit.me/id/transaction-monitoring/webhooks.md) (`transaction.created` atau `transaction.status.updated`) menyertakan blok `action_required` yang sama, sehingga sistem Anda mengetahui tindakan yang tertunda meskipun aplikasi ditutup sebelum alur selesai.
Tindakan `verification_session` disertai status `AWAITING_USER`, tetapi tindakan `wallet_ownership` tidak mengubah status transaksi dengan sendirinya, jadi baca `action_required` alih-alih status.
Transisi Travel Rule juga mengirimkan [`travel_rule.status.updated`](https://docs.didit.me/id/transaction-monitoring/travel-rule#webhook).

## Langkah berikutnya

> [card] [Mengirim transaksi](https://docs.didit.me/id/transaction-monitoring/transactions.md)
>
> API Create Transaction di sisi server dan referensi payload lengkap.

> [card] [Travel Rule](https://docs.didit.me/id/transaction-monitoring/travel-rule.md)
>
> Pertukaran pengirim-penerima terkelola, pengaturan, dan siklus hidupnya.

> [card] [Widget kepemilikan dompet](https://docs.didit.me/id/transaction-monitoring/wallet-ownership-widget.md)
>
> Alur bukti kontrol yang di-hosting untuk tindakan wallet\_ownership.

> [card] [Webhook](https://docs.didit.me/id/transaction-monitoring/webhooks.md)
>
> transaction.created, transaction.status.updated, dan verifikasi tanda tangan.

> [card] [Aturan](https://docs.didit.me/id/transaction-monitoring/rules.md)
>
> Tindakan aturan, termasuk yang membuat sesi verifikasi.

> [card] [Status](https://docs.didit.me/id/transaction-monitoring/statuses.md)
>
> Arti AWAITING\_USER dalam siklus hidup transaksi.


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