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

# Travel Rule

> FATF-compliant travel rule messaging for crypto VASPs and PSPs: originator/beneficiary data exchange, protocol tracking, auto-remediation. Pay-per-call $0.02.

Didit runs the full FATF/EU Transfer of Funds Regulation (TFR) originator-beneficiary exchange for virtual asset transfers: it resolves the counterparty VASP, negotiates and verifies the required data, tracks the exchange through a 13-state lifecycle, and only then lets you release funds on-chain.

## Overview

The FATF Travel Rule (Recommendation 16), implemented in the EU as the Transfer of Funds Regulation, requires a sending Virtual Asset Service Provider (the **originator VASP**) to collect and transmit originator and beneficiary information to the receiving VASP (the **beneficiary VASP**) before a virtual asset transfer settles.

Unlike most jurisdictions, the EU TFR applies with **no minimum amount** - every crypto transfer is in scope, not just transfers above a reporting threshold.

Didit's Travel Rule product is a managed exchange, not just a data field:

* You submit a transfer with `transaction_category: "travelRule"`.
* Didit resolves a rail to reach the counterparty VASP, exchanges originator and beneficiary data, and verifies the beneficiary's identity against the destination wallet.
* Only once the exchange reaches `COMPLETED` should you broadcast the transfer on-chain.
* You then report the on-chain transaction hash back to Didit to close the exchange out as `FINISHED`.

Seven rails ship in this release, tried in strict priority order - the first one that can reach the counterparty wins:

| Priority | Rail         | How it works                                                                                                                                                                                                                                                                                                                                                                              |
| -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1        | `INTERNAL`   | Used when the beneficiary wallet address is already registered in another Didit customer's [wallet address book](#wallet-address-book). Didit creates a matching inbound transfer on the beneficiary's application and negotiates the exchange entirely within the platform - no outbound network call. Instant.                                                                          |
| 2        | `TRP`        | Native integration with the open Travel Rule Protocol (TRP / OpenVASP). Used when the transfer carries a [travel address](#travel-addresses), the destination wallet's address-book entry stores one, or the counterparty VASP resolves in the [directory](#directory) with a TRP endpoint. Didit exchanges originator and beneficiary data machine-to-machine, with no email round-trip. |
| 3        | `GTR`        | Global Travel Rule (GTR) network. The counterparty is resolved by an explicit `counterparty_gtr_vasp_code` or a directory entry that carries one.                                                                                                                                                                                                                                         |
| 4        | `TRUST`      | TRUST network. The counterparty is resolved by `counterparty_trust_membership_id` or a directory entry that carries one.                                                                                                                                                                                                                                                                  |
| 5        | `VERIFYVASP` | VerifyVASP network. The counterparty is resolved by `counterparty_verifyvasp_id` or a directory entry that carries one.                                                                                                                                                                                                                                                                   |
| 6        | `SYGNA`      | Sygna Bridge network (acquired by VerifyVASP in 2026, still operating for existing members). Native implementation of the Sygna protocol - ECIES-encrypted IVMS101 with signed permission requests. The counterparty is resolved by `counterparty_sygna_vasp_code` or a directory entry that carries one.                                                                                 |
| 7        | `EMAIL`      | Fallback used when the counterparty exposes no supported protocol channel. Didit emails a compliance contact a one-time pickup link so they can confirm or decline the exchange without a Didit account. This is the workhorse for the sunrise problem - counterparties that are not yet reachable on any network.                                                                        |

The network rails (`GTR`, `TRUST`, `VERIFYVASP`, `SYGNA`) run in one of two modes, per network:

* **Didit's platform membership** - when Didit's own membership for a network is active, every Travel Rule application routes over it automatically, with no setup on your side. You can opt out of individual networks with the `disabled_networks` setting (see [Setup](#setup)).
* **Your own membership** - connect your own network membership credentials in the console **Marketplace** (Travel Rule networks category). A connected membership always takes precedence over Didit's platform membership, and your transfers travel under your own network identity.

The settings endpoint's `networks` object reports each network's effective mode (`didit`, `own`, `off`, or `unavailable`), so you can see exactly which rails can fire for your application.

Further networks - CODE (the Korean alliance), TRISA, Notabene, and Veriscope - are listed in the console **Marketplace** as on-request integrations: their onboarding runs through each network's own registration or due-diligence process, so request access there and Didit coordinates the connection with you.

When none of the seven rails can resolve a counterparty, Didit runs one more resolution step before giving up: blockchain analytics attribution.
The destination wallet is looked up against blockchain analytics data; when the wallet belongs to a known exchange or custodian, Didit records the attributed VASP on the transfer, adds it to the [directory](#directory), and re-runs the rails once with the new name.
See [Blockchain analytics attribution](#blockchain-analytics-attribution) for the full behavior and pricing.

A transfer whose destination is [declared as a self-hosted wallet](#declared-self-hosted-wallets) skips all seven rails - there is no counterparty VASP to exchange with - and goes straight to end-user wallet-ownership proof.

<Note>
  Didit also operates the counterparty-facing side of the exchange: public TRP identity and inquiry endpoints other VASPs call to reach you, callback endpoints, and the email pickup flow.
  You never call those yourself - they are managed infrastructure - but the [Travel Rule interoperability](/transaction-monitoring/travel-rule-interoperability) page documents every one of them so counterparty VASPs can connect to you.
  You interact with Travel Rule only through the `/v3/` endpoints described on this page.
</Note>

## What executes when you submit a transfer

Everything below runs synchronously inside the one `POST /v3/transactions/` call - by the time you receive the response, screening has run, routing has been attempted, rules have been evaluated, and the transfer already has a definitive status.
There is no "pending pipeline" to poll for; the only asynchronous parts are the counterparty's answer and any end-user action.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant You as Your platform
    participant Didit as Didit API
    participant CP as Counterparty VASP
    participant User as Your customer

    You->>Didit: POST /v3/transactions/ (travelRule)
    Note over Didit: Validate + bind subject + blocklist check
    Note over Didit: Device & IP enrichment
    Note over Didit: Crypto screening (when crypto + monitoring on)
    Note over Didit: Threshold check -> rails in priority order -> analytics attribution
    Didit->>CP: Exchange over the first reachable rail
    Note over Didit: Rules engine (EU TFR bundle + your rules)
    Didit-->>You: Transaction + travel_rule.status + action_required
    Didit--)You: transaction.created webhook
    User->>Didit: Proves wallet control (when action_required)
    CP-->>Didit: Confirms beneficiary data
    Didit--)You: travel_rule.status.updated (COMPLETED)
    You->>You: Broadcast on-chain
    You->>Didit: PATCH .../travel-rule/ { payment_txn_id }
    Didit--)You: travel_rule.status.updated (FINISHED)
```

Step by step:

1. **Validation and identity binding.** The payload is validated (`txn_id` deduplicated - resubmitting the same id returns the existing transaction), and the `subject` is resolved to your user or business profile by `vendor_data`, linking the transfer to the identity you verified at onboarding. Blocklisted subjects, counterparties, wallets, IPs, and devices are declined immediately.
2. **Device and network enrichment.** Party IPs are geo-enriched, and any device context (automatic on [SDK submission](/transaction-monitoring/sdk-transaction-submission)) is normalized into the device graph - the same graph KYC sessions feed, so "related by device" works across onboarding and transactions.
3. **Crypto screening.** For crypto transfers with monitoring enabled, the relevant wallet (or on-chain hash) is screened against blockchain analytics before anything is exchanged - sanctions exposure, mixer proximity, stolen funds. See [Crypto & wallet screening](/transaction-monitoring/aml-screening).
4. **Travel Rule routing.** The exchange engine checks the threshold, honors a [self-hosted wallet declaration](#declared-self-hosted-wallets), then walks the [seven rails](#overview) in priority order; if none resolve, [blockchain analytics attribution](#blockchain-analytics-attribution) identifies the counterparty and the rails re-run once. Whatever the outcome, the transfer leaves this step with a concrete status - `AWAITING_COUNTERPARTY`, `COMPLETED` (INTERNAL rail), `UNCONFIRMED_OWNERSHIP`, or `COUNTERPARTY_VASP_NOT_FOUND`.
5. **Wallet-ownership minting.** If the transfer needs end-user proof of control and `auto_wallet_verification` is on, a [wallet-ownership widget](/transaction-monitoring/wallet-ownership-widget) session is minted and returned in `action_required`.
6. **Rules evaluation.** Every active rule runs - the [EU TFR bundle](#eu-tfr-rules-bundle) reacts to the transfer status set in step 4, your custom rules see the screening output of step 3, and a rule can hold the transaction (`IN_REVIEW`), decline it, or demand [step-up verification](/transaction-monitoring/sdk-transaction-submission#required-user-actions) (`AWAITING_USER`).
7. **Billing and notification.** The [\$0.02 Travel Rule line](#pricing) is recorded, the response returns with the full picture, and `transaction.created` fires to your webhook destinations.

### How each step maps to your obligations

| Pipeline step                                          | Obligation it discharges                                                                                                                     |
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Identity binding to your verified KYC profile          | Accurate, **verified** originator information (FATF R.16; EU TFR Art. 14) - the data transmitted is the identity you verified, not free text |
| Rails exchange before settlement                       | Originator and beneficiary information transmitted to the counterparty VASP **before** the transfer settles (FATF R.16; EU TFR Art. 14-16)   |
| Beneficiary name matching                              | Verification that the beneficiary data matches the destination (EU TFR Art. 16 obligations of the beneficiary CASP)                          |
| Self-hosted wallet ownership proof                     | The EU TFR self-hosted wallet obligation - verify ownership of an unhosted address instead of exchanging with a VASP                         |
| Crypto screening + EU TFR rules bundle                 | Risk-based procedures for transfers with missing or incomplete information, and sanctions exposure controls                                  |
| `COMPLETED` gate before broadcast                      | Funds are not released until the exchange concludes - the core of the rule                                                                   |
| `FINISHED` close-out with the on-chain hash + timeline | Recordkeeping: every transfer carries its full negotiation timeline for audit                                                                |

<Note>
  **Testing note.** Sandbox applications skip rule evaluation, webhooks, and billing entirely - sandbox transactions stay `APPROVED` and nothing is charged.
  The Travel Rule exchange itself still executes on sandbox applications when enabled, including real rail dispatch, so use test counterparty data there.
</Note>

## Lifecycle

Every transfer has a `status` drawn from 13 values.
The exchange starts at `AWAITING_COUNTERPARTY` in the normal case, at `NOT_APPLICABLE` when the amount is below your `threshold_amount`, or at `UNCONFIRMED_OWNERSHIP` when the destination was [declared a self-hosted wallet](#declared-self-hosted-wallets), and ends in one of the terminal states: `FINISHED`, `CANCELLED`, or `EXPIRED`.

The transfer runs its own state machine, **separate from** the underlying transaction's review status. Whenever the exchange transitions, Didit also nudges the transaction: `ON_HOLD` surfaces the transaction as `AWAITING_USER`, an expiry applies your configured `timeout_outcome`, and the [EU TFR rules](#eu-tfr-rules-bundle) can push it to `IN_REVIEW`. Read the exchange state from `travel_rule.status`; read the allow / hold / decline verdict from the transaction's own `status`.

Every transition (and every passively reported status) is also mirrored onto the transaction's custom values so [rules](/transaction-monitoring/rules) can key on it: `custom_values.travel_rule_status` (canonical status), `custom_values.travel_rule_rail` (the rail that carried the exchange, empty until one is selected), and `custom_values.travel_rule_regime` (derived from your [settings](#setup) `jurisdiction`: `SG` → `singapore`, `GB` → `uk`, and any EU or EEA member code such as `DE` or `FR` → `eu`; unmapped codes stamp lowercased, e.g. `US` → `us`. A `props.travel_rule_regime` you submit yourself always wins over the derived value).

| Status                              | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AWAITING_COUNTERPARTY`             | The exchange was sent to the counterparty (INTERNAL handoff or EMAIL pickup link) and Didit is waiting on a response.                                                                                                                                                                                                                                                                                                                                                                                    |
| `UNCONFIRMED_OWNERSHIP`             | The transfer is waiting on proof of wallet control. Reached when the counterparty wallet was [declared self-hosted](#declared-self-hosted-wallets) at submission (outbound or inbound), or when an inbound transfer resolves to an [address book entry](#wallet-address-book) whose ownership is not verified yet. Advances via the [wallet-ownership widget](/transaction-monitoring/wallet-ownership-widget) or [`POST .../travel-rule/ownership/`](#confirming-wallet-ownership).                     |
| `ON_HOLD`                           | Held for manual or analyst action, surfaced on the underlying transaction as `AWAITING_USER`.                                                                                                                                                                                                                                                                                                                                                                                                            |
| `COMPLETED`                         | Originator and beneficiary data matched (and ownership was confirmed, where required). Safe to broadcast the transfer on-chain.                                                                                                                                                                                                                                                                                                                                                                          |
| `FINISHED`                          | Outbound only. The on-chain transaction hash was reported via [`PATCH .../travel-rule/`](#sending-a-transfer) and the exchange is closed out.                                                                                                                                                                                                                                                                                                                                                            |
| `CANCELLED`                         | Manually cancelled with `action: "cancel"` from any non-terminal state.                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `EXPIRED`                           | `confirmation_timeout_hours` elapsed while `AWAITING_COUNTERPARTY`. A sweeper (every 15 minutes) expires the transfer and applies your configured `timeout_outcome` to the underlying transaction: `REJECT` declines it, `HOLD` moves it to review, `PROCEED` leaves it untouched.                                                                                                                                                                                                                       |
| `COUNTERPARTY_MISMATCHED_DATA`      | The beneficiary name returned by the counterparty did not satisfy the configured `name_matching_strictness`.                                                                                                                                                                                                                                                                                                                                                                                             |
| `COUNTERPARTY_VASP_NOT_FOUND`       | None of the seven rails - nor the [blockchain analytics attribution](#blockchain-analytics-attribution) fallback - could resolve a reachable counterparty VASP for this transfer. Any attributed entity is still recorded on the transfer and in the directory. On outbound transfers Didit auto-mints a [wallet-ownership widget](/transaction-monitoring/wallet-ownership-widget) session (when `auto_wallet_verification` is on) so the transfer can still complete as a self-hosted-wallet transfer. |
| `COUNTERPARTY_VASP_NOT_REACHABLE`   | A counterparty VASP was identified (travel address or directory entry) but its `TRP` endpoint could not be reached. Retry with `action: "resend"` once it is reachable again.                                                                                                                                                                                                                                                                                                                            |
| `COUNTERPARTY_VASP_GENERAL_DECLINE` | The counterparty declined the exchange (ownership denied, or email pickup declined).                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `NOT_ENOUGH_COUNTERPARTY_DATA`      | The counterparty responded (over `TRP` or `EMAIL`) without enough data to complete the exchange. Repairable - update the counterparty data and re-run routing with `action: "resend"`.                                                                                                                                                                                                                                                                                                                   |
| `NOT_APPLICABLE`                    | Travel Rule is disabled for the sending application, the transfer amount was below the configured `threshold_amount`, or a legacy status string was recorded passively (see below).                                                                                                                                                                                                                                                                                                                      |

<Warning>
  **Never broadcast before `COMPLETED`.**
  Releasing funds on-chain while a transfer is `AWAITING_COUNTERPARTY`, `UNCONFIRMED_OWNERSHIP`, or in any mismatch/decline state defeats the purpose of the exchange.
  Gate your broadcast step on `status == "COMPLETED"`, then call `PATCH .../travel-rule/` with the resulting `payment_txn_id` to move to `FINISHED`.
</Warning>

The transitions at a glance (`CANCELLED` is additionally reachable from every non-terminal state with `action: "cancel"`):

```mermaid theme={null}
stateDiagram-v2
    [*] --> NOT_APPLICABLE: below threshold or disabled
    [*] --> UNCONFIRMED_OWNERSHIP: self-hosted wallet declared
    [*] --> AWAITING_COUNTERPARTY: rail dispatched
    [*] --> COMPLETED: INTERNAL rail, verified entry + name match
    [*] --> ON_HOLD: inbound with no matching wallet entry
    [*] --> COUNTERPARTY_VASP_NOT_FOUND: no rail resolved
    [*] --> COUNTERPARTY_VASP_NOT_REACHABLE: rail delivery failed
    AWAITING_COUNTERPARTY --> COMPLETED: counterparty confirmed, names match
    AWAITING_COUNTERPARTY --> COUNTERPARTY_MISMATCHED_DATA: name match failed
    AWAITING_COUNTERPARTY --> COUNTERPARTY_VASP_GENERAL_DECLINE: counterparty declined
    AWAITING_COUNTERPARTY --> NOT_ENOUGH_COUNTERPARTY_DATA: incomplete response
    AWAITING_COUNTERPARTY --> EXPIRED: confirmation timeout
    UNCONFIRMED_OWNERSHIP --> COMPLETED: ownership proven, names match
    UNCONFIRMED_OWNERSHIP --> COUNTERPARTY_MISMATCHED_DATA: name match failed
    UNCONFIRMED_OWNERSHIP --> COUNTERPARTY_VASP_GENERAL_DECLINE: ownership denied
    COUNTERPARTY_VASP_NOT_FOUND --> AWAITING_COUNTERPARTY: action resend
    COUNTERPARTY_VASP_NOT_REACHABLE --> AWAITING_COUNTERPARTY: action resend
    NOT_ENOUGH_COUNTERPARTY_DATA --> AWAITING_COUNTERPARTY: action resend
    COMPLETED --> FINISHED: payment_txn_id reported
    FINISHED --> [*]
    EXPIRED --> [*]
```

### Legacy status values

If you were already sending `travel_rule_details.status` before this release, those values still work and are mapped automatically.
camelCase spellings of any status (for example `counterpartyVaspNotFound`) are accepted too, both on submission and in [rule](/transaction-monitoring/rules) condition values, and normalize to the canonical form:

| Legacy value           | Mapped to               |
| ---------------------- | ----------------------- |
| `UNKNOWN`              | `NOT_APPLICABLE`        |
| `COMPLIANT`            | `COMPLETED`             |
| `PENDING_ACTION`       | `ON_HOLD`               |
| `PENDING_COUNTERPARTY` | `AWAITING_COUNTERPARTY` |
| `FAILED`               | `CANCELLED`             |
| `EXEMPT`               | `NOT_APPLICABLE`        |

## Setup

Configure your VASP profile and negotiation policy once per application with `GET`/`PUT /v3/travel-rule/settings/`.

```bash theme={null}
curl -X GET https://verification.didit.me/v3/travel-rule/settings/ \
  -H "x-api-key: YOUR_API_KEY"
```

```json theme={null}
{
  "is_enabled": false,
  "jurisdiction": "EU",
  "name_matching_strictness": "DEFAULT",
  "confirmation_timeout_hours": 48,
  "timeout_outcome": "HOLD",
  "threshold_amount": "0.00",
  "inbound_auto_accept": true,
  "allow_self_declaration": true,
  "allow_screenshot_proof": true,
  "auto_wallet_verification": true,
  "legal_name": "",
  "lei": "",
  "compliance_email": "",
  "is_discoverable": true,
  "travel_address": ""
}
```

```bash theme={null}
curl -X PUT https://verification.didit.me/v3/travel-rule/settings/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "is_enabled": true,
    "legal_name": "Origin CASP SL",
    "jurisdiction": "EU",
    "name_matching_strictness": "DEFAULT",
    "confirmation_timeout_hours": 48,
    "timeout_outcome": "HOLD",
    "is_discoverable": true
  }'
```

`PUT` is a partial update - send only the fields you want to change.

| Field                        | Type             | Description                                                                                                                                                                                                                                                                                                                                                                                                   |
| ---------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `is_enabled`                 | boolean          | Turns on the managed negotiation flow for outbound `travelRule` transactions from this application. Requires a non-blank `legal_name` (already stored, or included in the same request) - otherwise the request returns `400`.                                                                                                                                                                                |
| `legal_name`                 | string           | Your VASP's legal name. Shown to the counterparty on the email pickup page and returned in the [VASP directory](#directory).                                                                                                                                                                                                                                                                                  |
| `lei`                        | string           | Your Legal Entity Identifier, returned in the VASP directory.                                                                                                                                                                                                                                                                                                                                                 |
| `jurisdiction`               | string           | Your VASP's operating jurisdiction code, up to 8 characters (default `"EU"`).                                                                                                                                                                                                                                                                                                                                 |
| `name_matching_strictness`   | string           | `NONE`, `STRICT`, `DEFAULT`, or `FUZZY` - see below.                                                                                                                                                                                                                                                                                                                                                          |
| `confirmation_timeout_hours` | integer          | Hours an `AWAITING_COUNTERPARTY` transfer is allowed to sit before it auto-expires. Minimum `1`, default `48`.                                                                                                                                                                                                                                                                                                |
| `timeout_outcome`            | string           | What happens to the underlying transaction when a transfer expires: `HOLD` moves it to review, `REJECT` declines it, `PROCEED` leaves its status untouched. Default `HOLD`.                                                                                                                                                                                                                                   |
| `compliance_email`           | string           | Compliance contact email for your VASP profile.                                                                                                                                                                                                                                                                                                                                                               |
| `is_discoverable`            | boolean          | Whether your VASP profile appears in `GET /v3/travel-rule/vasps/` search results for other Didit customers, and whether the INTERNAL rail can route to you at all. Default `true`.                                                                                                                                                                                                                            |
| `inbound_auto_accept`        | boolean          | When `true`, inbound transfers whose beneficiary wallet is a verified entry in your address book and whose name matches are accepted automatically, without a manual review step. Default `true`.                                                                                                                                                                                                             |
| `allow_self_declaration`     | boolean          | When `true`, the [wallet-ownership widget](/transaction-monitoring/wallet-ownership-widget) offers self-declaration as a fallback proof method for wallets that cannot be verified another way. Default `true`.                                                                                                                                                                                               |
| `allow_screenshot_proof`     | boolean          | When `true`, the [wallet-ownership widget](/transaction-monitoring/wallet-ownership-widget) offers screenshot upload as an ownership evidence method. Weaker than cryptographic signing - disable it where your policy requires signature-grade proof. Default `true`.                                                                                                                                        |
| `auto_wallet_verification`   | boolean          | When `true`, an outbound transfer that needs end-user proof of wallet control auto-mints a [wallet-ownership widget](/transaction-monitoring/wallet-ownership-widget) session bound to the transaction and surfaces it in the transaction's `action_required` block - the Didit SDKs launch it automatically on [client-side submission](/transaction-monitoring/sdk-transaction-submission). Default `true`. |
| `vasp_attribution_enabled`   | boolean          | When `true`, an outbound transfer that no rail can route is attributed through [blockchain analytics](#blockchain-analytics-attribution) - the destination wallet is resolved to its owning VASP, the entity is added to the directory, and the rails are re-run once with the attributed name. Default `true`.                                                                                               |
| `threshold_amount`           | string (decimal) | Minimum transfer amount, in the transaction's currency, that triggers a managed exchange. Transfers below it resolve straight to `NOT_APPLICABLE` and are never negotiated. Defaults to `0.00` - matching the EU TFR's no-minimum-amount framing, so every transfer is checked regardless of size.                                                                                                            |
| `disabled_networks`          | string\[]        | Network rails (`GTR`, `TRUST`, `VERIFYVASP`, `SYGNA`) your transfers must NOT route over via Didit's platform membership. A network you connected your own membership for (console Marketplace) is never affected by this list. Default `[]`.                                                                                                                                                                 |
| `networks`                   | object           | Read-only. Per-network status keyed by `gtr` / `trust` / `verifyvasp` / `sygna`: `{rail, mode, platform_available, connected}` where `mode` is `own` (your connected membership), `didit` (Didit's platform membership), `off` (disabled by you), or `unavailable`.                                                                                                                                           |
| `travel_address`             | string           | Read-only. Your VASP's own travel address - the encoded inbound `TRP` inquiry endpoint other VASPs use to reach you. Populated once your VASP profile is complete.                                                                                                                                                                                                                                            |

### Name matching strictness

| Value     | Behaviour                                                                                                |
| --------- | -------------------------------------------------------------------------------------------------------- |
| `NONE`    | Any beneficiary name is accepted - no comparison is made.                                                |
| `STRICT`  | The normalized token sets of both names must be exactly equal.                                           |
| `DEFAULT` | Every token of the shorter name must appear in the longer name (subset match), with at least two tokens. |
| `FUZZY`   | A similarity ratio of the full normalized strings must be at least `0.85`.                               |

### Thresholds by jurisdiction

`threshold_amount` exists because Travel Rule scope varies by regulator.
Didit defaults to `0.00` - the EU position - so every transfer is exchanged unless you raise it deliberately:

| Regime                                               | Threshold            | Notes                                                                                                                             |
| ---------------------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **EU** - Transfer of Funds Regulation (EU) 2023/1113 | **No minimum**       | Every CASP-to-CASP crypto transfer is in scope. Ownership of a self-hosted wallet must be verified for transfers above EUR 1,000. |
| **FATF Recommendation 16** (global baseline)         | USD/EUR 1,000        | Below it, FATF permits a reduced data set (names and wallet addresses, without verification).                                     |
| **United States** - BSA Travel Rule                  | USD 3,000            | The long-standing FinCEN funds-transfer threshold, applied to convertible virtual currency.                                       |
| **United Kingdom**                                   | EUR 1,000 equivalent | Full information above; reduced data set below (MLRs Part 7A).                                                                    |
| **Singapore** - MAS PSN02                            | SGD 1,500            | Reduced requirements below the threshold.                                                                                         |
| **Switzerland** - FINMA                              | No minimum           | Information required for all transfers; proof of ownership expected for self-hosted wallets.                                      |

Set `threshold_amount` to your regulator's figure; transfers below it resolve to `NOT_APPLICABLE` without exchanging any identity data.
Thresholds evolve - confirm the current figure with your compliance counsel rather than treating this table as legal advice.

### IVMS 101 data model

The industry standard for the data exchanged under the Travel Rule is **IVMS 101** (interVASP Messaging Standard).
`travel_rule_details.originator_data` and `travel_rule_details.beneficiary_data` are IVMS 101-style person payloads: Didit wraps them in each rail's native envelope - the TRP rail exchanges `IVMS101` blocks (`ivms101.2023`, reported in the transfer's `ivms_version` field), the Sygna rail its ECIES-encrypted IVMS variant, and the network rails their own signed envelopes - so one payload shape reaches every counterparty in the format their protocol expects.

Two practical rules:

* **Latin script.** IVMS 101 requires Latin-script values; transliterate names that are natively written in another script. Didit's [name matching](#name-matching-strictness) compares normalized token sets, so ordering and casing differences don't fail an exchange - but different scripts on the two sides will.
* **You send data you have verified.** Because the subject is bound to your Didit KYC profile by `vendor_data`, the originator identity you transmit is the one Didit verified at onboarding - document-extracted, liveness-checked, and face-matched - not an unverified form field. That distinction is exactly what the FATF's "required and **accurate** originator information" wording demands.

### Travel addresses

A **travel address** is a compact, encoded string that points at a VASP's inbound `TRP` inquiry endpoint - it never exposes a raw URL or any wallet address.
Once your VASP profile is complete, Didit derives your own `travel_address` (returned read-only in settings) and publishes it so other VASPs can reach you on the `TRP` rail.
When you know a counterparty's travel address, store it on the destination [wallet address book entry](#wallet-address-book) so Didit routes that beneficiary's transfers straight over `TRP` instead of falling back to email.

## Sending a transfer

Include the transfer on [`POST /v3/transactions/`](/transaction-monitoring/transactions) by setting `transaction_category` to `travelRule` (or `travel_rule`) and attaching a `travel_rule_details` object (alias: `travelRule`).

When Travel Rule is **enabled** for your application, Didit ignores any `status` you send and drives the exchange itself:

1. If the amount is below your configured `threshold_amount`, the transfer resolves immediately to `NOT_APPLICABLE` and nothing is negotiated.
2. If the counterparty's payment method declares the destination as a self-hosted wallet (`payment_method.method_type: "unhosted_wallet"`), no rail is tried - the transfer goes straight to `UNCONFIRMED_OWNERSHIP` and the wallet-ownership widget is auto-minted (see [Declared self-hosted wallets](#declared-self-hosted-wallets)).
3. Otherwise the rails run in priority order, and the first one that can reach the counterparty carries the exchange:
   * `INTERNAL` - `beneficiary_data.wallet_address` is registered in another Didit customer's wallet address book.
   * `TRP` - the transfer's asset has a Digital Token Identifier mapping (`BTC` and `ETH` today), and the payload carries `beneficiary_data.travel_address`, the destination's own address-book entry stores one, or the counterparty resolves in the [directory](#directory) (by `counterparty_vasp_lei` or `counterparty_vasp_name`) with a TRP endpoint.
   * `GTR`, `TRUST`, `VERIFYVASP`, `SYGNA` - the network is active for your application (your own connected membership, or Didit's platform membership when the network is not in `disabled_networks`), and the counterparty resolves to a network id (explicit field or directory entry - see [Counterparty resolution fields](#counterparty-resolution-fields)).
   * `EMAIL` - `beneficiary_data.counterparty_vasp_email` was supplied, or `counterparty_vasp_name` resolves to a directory entry with a compliance email.
4. If no rail can resolve a counterparty and `vasp_attribution_enabled` is on, Didit runs [blockchain analytics attribution](#blockchain-analytics-attribution): the destination wallet is resolved to its owning VASP, the attributed name is written to `beneficiary_data.counterparty_vasp_name` (when empty) and to the [directory](#directory), and the rails re-run once - so a wallet attributed to a VASP whose directory entry carries a TRP endpoint, network id, or compliance email routes on this second pass.
5. If the counterparty is still unresolved, the transfer is created in `COUNTERPARTY_VASP_NOT_FOUND`. On outbound transfers Didit auto-mints the [wallet-ownership widget](/transaction-monitoring/wallet-ownership-widget) (when `auto_wallet_verification` is on) so the transfer can still complete as a self-hosted-wallet transfer, and you can re-run routing later with `action: "resend"` once you have better counterparty data.

### Counterparty resolution fields

Everything Didit knows about the counterparty comes from `travel_rule_details.beneficiary_data`.
These fields drive rail resolution; all are optional:

| Field                              | Drives                                                                                                                                                                                                                |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wallet_address`                   | `INTERNAL` address-book match, `TRP` address-book lookup, and the wallet-ownership flows.                                                                                                                             |
| `travel_address`                   | Routes directly over `TRP` - the strongest signal you can pass.                                                                                                                                                       |
| `counterparty_vasp_lei`            | Directory lookup by LEI for `TRP`, `GTR`, `TRUST`, `VERIFYVASP`, and `SYGNA`. Also cross-checked against the identity the TRP endpoint reports - a mismatch aborts the exchange as `COUNTERPARTY_VASP_NOT_REACHABLE`. |
| `counterparty_vasp_name`           | Directory lookup by exact name for `TRP`, `GTR`, `TRUST`, `VERIFYVASP`, `SYGNA`, and `EMAIL`. Fuzzy-checked against the TRP identity when both are present.                                                           |
| `counterparty_gtr_vasp_code`       | Explicit `GTR` network id - skips the directory.                                                                                                                                                                      |
| `counterparty_trust_membership_id` | Explicit `TRUST` network id - skips the directory.                                                                                                                                                                    |
| `counterparty_verifyvasp_id`       | Explicit `VerifyVASP` network id - skips the directory.                                                                                                                                                               |
| `counterparty_sygna_vasp_code`     | Explicit `Sygna Bridge` VASP code - skips the directory.                                                                                                                                                              |
| `counterparty_vasp_email`          | Explicit compliance contact for the `EMAIL` rail - skips the directory.                                                                                                                                               |

### Blockchain analytics attribution

When you pass nothing but a wallet address, Didit can still find out who is on the other side.
If no rail can route the transfer, Didit resolves the destination wallet against blockchain analytics attribution data - the same analysis that powers [crypto transaction screening](/transaction-monitoring/aml-screening) knows which wallets belong to which exchanges and custodians.

When the wallet is attributed to a known entity, Didit:

1. Records the attribution on the transfer: `beneficiary_data.counterparty_vasp_name` is filled (when you did not supply one), and the transfer's metadata carries a `vasp_attribution` block with the entity name, entity type, country, and `source: "blockchain_analytics"`.
2. Adds the entity to your [VASP directory](#directory) (or matches the existing entry), so the attribution is stable across transfers and you can enrich the entry with a travel address, network ids, or a compliance email.
3. Re-runs the rails once with the attributed name - a directory entry that already carries a TRP endpoint, network id, or compliance email makes the transfer route immediately instead of landing in `COUNTERPARTY_VASP_NOT_FOUND`.

The attribution call doubles as a full crypto screening: the transaction receives the complete screening output - risk score, wallet risk category, network snapshot - exactly as if crypto monitoring had run, and the transaction's `custom_values.aml_screening_trigger` is set to `travel_rule_attribution` so you can see the screening ran because Travel Rule routing required it.
When crypto monitoring already screened the same wallet on this transaction, the stored result is reused - no second analytics call, no second charge.
Attribution never blocks transaction creation: if the analytics lookup fails, the transfer simply proceeds to `COUNTERPARTY_VASP_NOT_FOUND` and nothing is charged for the attempt.
On `action: "resend"`, a previously recorded attribution is reused instead of re-queried.

Disable the whole step with `vasp_attribution_enabled: false` in [settings](#setup).
See [Pricing](#pricing) for how the attribution call is billed.

When Travel Rule is **disabled**, `travel_rule_details` (including a caller-supplied `status`, legacy or current) is still recorded on the transaction, but passively - no rail is dispatched.
Use this to backfill compliance data you already tracked with your own process.

```bash theme={null}
curl -X POST https://verification.didit.me/v3/transactions/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "trv-2026-07-05-001",
    "transaction_category": "travelRule",
    "transaction_details": {
      "direction": "outbound",
      "action_type": "transfer",
      "amount": "1500.00",
      "currency": "EUR"
    },
    "subject": {
      "entity_type": "person",
      "vendor_data": "user-042",
      "full_name": "Jane Doe"
    },
    "travel_rule_details": {
      "beneficiary_data": {
        "wallet_address": "0xBeneficiaryWallet01",
        "name": "Ana Diaz"
      }
    }
  }'
```

```json theme={null}
{
  "uuid": "22222222-3333-4444-5555-666666666666",
  "txn_id": "trv-2026-07-05-001",
  "transaction_type": "travelRule",
  "direction": "OUTBOUND",
  "status": "APPROVED",
  "amount": "1500",
  "currency": "EUR",
  "travel_rule": {
    "uuid": "33333333-4444-5555-6666-777777777777",
    "status": "AWAITING_COUNTERPARTY",
    "protocol": "EMAIL",
    "required": true,
    "obligations_count": 0,
    "originator_data": {},
    "beneficiary_data": {
      "wallet_address": "0xBeneficiaryWallet01",
      "name": "Ana Diaz"
    },
    "metadata": {}
  }
}
```

If the beneficiary wallet is already a verified entry in another Didit customer's address book and its `holder_name` matches, the `INTERNAL` rail can resolve the whole exchange synchronously - the response above would show `"status": "COMPLETED"` instead.

<Note>
  The transaction's embedded `travel_rule` object is intentionally short (`uuid`, `status`, `protocol`, `required`, `obligations_count`, `originator_data`, `beneficiary_data`, `metadata`).
  The dedicated transfer endpoints below return a fuller object that also includes `direction`, `rail`, `ivms_version`, `ownership_confirmed`, `deadline_at`, `timeline`, and `counterparty_vasp`.
</Note>

To close out a `COMPLETED` outbound transfer once you've broadcast it on-chain:

```bash theme={null}
curl -X PATCH https://verification.didit.me/v3/transactions/22222222-3333-4444-5555-666666666666/travel-rule/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"payment_txn_id": "0xchainhash"}'
```

This requires the transfer to be an **outbound** transfer in `COMPLETED` status - otherwise the API returns `409`.
On success both the outbound transfer and its `INTERNAL`-rail counterpart move to `FINISHED`, and the transaction's `payment_txn_id` is set to the hash you provided.

To cancel a transfer that hasn't reached a terminal state yet, send `{"action": "cancel"}` to the same endpoint instead.

To retry an outbound transfer that could not reach its counterparty - one currently in `COUNTERPARTY_VASP_NOT_FOUND`, `COUNTERPARTY_VASP_NOT_REACHABLE`, or `NOT_ENOUGH_COUNTERPARTY_DATA` - first improve the counterparty data (for example add a `travel_address` to the destination wallet entry, or catalogue the VASP in the [directory](#directory)), then send `{"action": "resend"}` to the same endpoint. Didit re-runs the routing ladder from the start. Any other status returns `409`.

```bash theme={null}
curl -X PATCH https://verification.didit.me/v3/transactions/22222222-3333-4444-5555-666666666666/travel-rule/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action": "resend"}'
```

### Declared self-hosted wallets

When your customer tells you the destination is their own self-hosted (unhosted) wallet, declare it on the transaction instead of letting routing discover there is no VASP behind it: set the counterparty's payment method type to `unhosted_wallet`.

```bash theme={null}
curl -X POST https://verification.didit.me/v3/transactions/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "trv-2026-07-07-002",
    "transaction_category": "travelRule",
    "transaction_details": {
      "direction": "outbound",
      "action_type": "transfer",
      "amount": "0.25",
      "currency": "ETH",
      "currency_type": "crypto"
    },
    "subject": {
      "entity_type": "person",
      "vendor_data": "user-042",
      "full_name": "Jane Doe"
    },
    "counterparty": {
      "entity_type": "external",
      "full_name": "Jane Doe",
      "payment_method": {
        "method_type": "unhosted_wallet",
        "account_id": "0xJanesColdWallet01"
      }
    },
    "travel_rule_details": {
      "beneficiary_data": {
        "wallet_address": "0xJanesColdWallet01",
        "name": "Jane Doe"
      }
    }
  }'
```

The declaration short-circuits routing entirely:

* No rail is tried - there is no counterparty VASP to exchange with - and the transfer is created directly in `UNCONFIRMED_OWNERSHIP`.
* When `auto_wallet_verification` is on (the default), Didit auto-mints a [wallet-ownership widget](/transaction-monitoring/wallet-ownership-widget) session bound to the transaction and returns it in the transaction's `action_required` block; the SDKs launch it automatically on [client-side submission](/transaction-monitoring/sdk-transaction-submission).
* Once the customer proves control of the wallet, the transfer runs the beneficiary name match and advances to `COMPLETED` (or `COUNTERPARTY_MISMATCHED_DATA` on a failed match).

This is the EU TFR self-hosted wallet obligation in API form: verify ownership of the unhosted wallet instead of exchanging data with a counterparty VASP.
The declaration is read from the payment method on the **destination** side of an outbound transfer and the **source** side of an inbound one - in both cases, the counterparty participant.
A declared-unhosted transfer is already on its final rail-free path, so `UNCONFIRMED_OWNERSHIP` is not a resendable state - complete the ownership proof (or cancel) instead of calling `action: "resend"`.

## Receiving

### Confirming wallet ownership

When the `INTERNAL` rail resolves a transfer to a wallet address whose address-book entry hasn't been ownership-verified, the beneficiary side is created in `UNCONFIRMED_OWNERSHIP`.
The beneficiary's application confirms or denies it:

```bash theme={null}
curl -X POST https://verification.didit.me/v3/transactions/{transaction_uuid}/travel-rule/ownership/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"confirmed": true}'
```

* `confirmed: true` marks the matching wallet address book entry as ownership-verified, then runs the name match on both sides - `COMPLETED` if it passes, `COUNTERPARTY_MISMATCHED_DATA` if it doesn't.
* `confirmed: false` moves both sides of the transfer to `COUNTERPARTY_VASP_GENERAL_DECLINE`.
* Returns `409` if the transfer is not currently `UNCONFIRMED_OWNERSHIP`, or if the wallet address book entry it needs was deleted in the meantime.

The response is the fuller transfer object described above; `originator_data`/`beneficiary_data` are omitted entirely until `ownership_confirmed` is `true` on an inbound transfer, to avoid leaking counterparty PII before ownership is proven.

### Wallet address book

Register the wallets you control so inbound `INTERNAL`-rail transfers can resolve against them, with `POST`/`GET /v3/travel-rule/wallet-addresses/` and `PATCH`/`DELETE /v3/travel-rule/wallet-addresses/{entry_uuid}/`.

```bash theme={null}
curl -X POST https://verification.didit.me/v3/travel-rule/wallet-addresses/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "address": "0xBeneficiaryWallet01",
    "chain": "ethereum",
    "holder_name": "Ana Diaz",
    "holder_vendor_data": "user-042",
    "self_declared": true
  }'
```

| Field                   | Type    | Description                                                                                                                                                                                                             |
| ----------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`               | string  | The wallet address. Unique per application and `chain` among non-deleted entries - a duplicate returns `400`.                                                                                                           |
| `chain`                 | string  | Free-text chain identifier (for example `ethereum`, `bitcoin`). Optional.                                                                                                                                               |
| `holder_name`           | string  | Name of the wallet holder, used for the beneficiary name match.                                                                                                                                                         |
| `holder_vendor_data`    | string  | Your internal identifier for the holder.                                                                                                                                                                                |
| `entity_type`           | string  | Free text, defaults to `"individual"`.                                                                                                                                                                                  |
| `travel_address`        | string  | Optional. The counterparty VASP's travel address for this wallet, if you know it. Lets Didit route transfers to this beneficiary over the `TRP` rail instead of email.                                                  |
| `is_ownership_verified` | boolean | Read-only. Whether ownership of this address has been proven (via the [wallet-ownership widget](/transaction-monitoring/wallet-ownership-widget) or self-declaration).                                                  |
| `self_declared`         | boolean | Write-only, `POST` only. When `true`, the entry is created already verified via a `SELF_DECLARATION` proof - inbound transfers to this address skip `UNCONFIRMED_OWNERSHIP` entirely and go straight to the name match. |

`PATCH` accepts `holder_name`, `holder_vendor_data`, `entity_type`, and `travel_address`, and returns only those four fields.
`DELETE` soft-deletes the entry (`204`) - its address is freed up for re-registration and its ownership proofs are retained for audit, but it immediately stops matching new transfers.

<Note>
  Matching is currently by address only, not scoped by `chain`.
  Avoid registering the same address string for two different chains under one application.
</Note>

### Email pickup for unreachable counterparties

When no protocol rail can reach the counterparty, Didit falls back to `EMAIL`: it sends a one-time pickup link to `beneficiary_data.counterparty_vasp_email` (or an email resolved from `beneficiary_data.counterparty_vasp_name` against the VASP directory registry).
The recipient does not need a Didit account or an API key - the link's token is the authentication.

```bash theme={null}
curl https://verification.didit.me/v3/travel-rule/pickup/{token}/
```

```json theme={null}
{
  "originator_vasp_name": "Origin CASP SL",
  "amount": "1500.00",
  "currency": "EUR",
  "beneficiary_wallet": "bc1qexternalwallet01",
  "status": "AWAITING_COUNTERPARTY"
}
```

```bash theme={null}
curl -X POST https://verification.didit.me/v3/travel-rule/pickup/{token}/respond/ \
  -H "Content-Type: application/json" \
  -d '{"decision": "accept", "beneficiary_name": "Carlos Ruiz"}'
```

`decision` is `accept` or `decline`.
On `accept`, the supplied `beneficiary_name` is run through the originator's `name_matching_strictness` policy - `COMPLETED` on a match, `COUNTERPARTY_MISMATCHED_DATA` otherwise.
On `decline`, the transfer moves to `COUNTERPARTY_VASP_GENERAL_DECLINE`.
Both endpoints return `404` for an unknown token and `410` once the window closes - either the transfer left `AWAITING_COUNTERPARTY`, or `pickup_expires_at` (the same deadline as `confirmation_timeout_hours`) has passed.

### After-deposit sunrise registration

When a crypto deposit settles on-chain **before** any Travel Rule exchange took place - the sending VASP was unreachable, or you are backfilling historical deposits - register it after the fact with `POST /v3/travel-rule/inbound/`.

```bash theme={null}
curl -X POST https://verification.didit.me/v3/travel-rule/inbound/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chain": "ethereum",
    "txid": "0xdeposit-hash",
    "wallet_address": "0xYourDepositWallet",
    "amount": "500.00",
    "currency": "USDC",
    "originator_data": { "name": "Origin CASP SL" },
    "beneficiary_data": { "name": "Ana Diaz" },
    "originating_vasp": { "name": "Origin CASP SL", "lei": "", "travel_address": "ta..." }
  }'
```

| Field              | Type             | Description                                                                                                                                                                                                                                          |
| ------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chain`            | string           | Chain the deposit settled on.                                                                                                                                                                                                                        |
| `txid`             | string           | The on-chain transaction hash. Used to dedupe - the same `txid` + `wallet_address` returns the existing transfer instead of creating a duplicate.                                                                                                    |
| `wallet_address`   | string           | The destination wallet you received the deposit on.                                                                                                                                                                                                  |
| `amount`           | string (decimal) | Deposit amount.                                                                                                                                                                                                                                      |
| `currency`         | string           | Deposit asset.                                                                                                                                                                                                                                       |
| `originator_data`  | object           | IVMS-101 originator payload from the sending VASP, if known.                                                                                                                                                                                         |
| `beneficiary_data` | object           | IVMS-101 beneficiary payload (your customer).                                                                                                                                                                                                        |
| `originating_vasp` | object           | Optional `{ name, lei, travel_address }` describing the sender. A `travel_address` lets Didit reach them on the `TRP` rail for any follow-up, and the VASP is catalogued in the [directory](#how-entries-get-into-the-directory) for future routing. |

Didit creates an inbound `travelRule` transaction (or reuses an unclaimed deposit it already recorded) and resolves the transfer against your [wallet address book](#wallet-address-book):

* No matching entry -> `ON_HOLD`.
* Entry exists but ownership is unproven -> `UNCONFIRMED_OWNERSHIP`.
* Beneficiary name does not match the entry's `holder_name` -> `COUNTERPARTY_MISMATCHED_DATA`.
* Verified entry and the name matches -> `COMPLETED`, then immediately `FINISHED` (the deposit has already settled).

```json theme={null}
{
  "transaction_id": "44444444-5555-6666-7777-888888888888",
  "txn_id": "sunrise-...",
  "created": true,
  "travel_rule": {
    "uuid": "55555555-6666-7777-8888-999999999999",
    "status": "FINISHED",
    "direction": "INBOUND",
    "required": true
  }
}
```

`created` is `true` when a new transaction was minted (HTTP `201`) and `false` when an existing one was reused (HTTP `200`).
On the verified-entry path both transitions happen before the endpoint responds, so the synchronous response already shows the terminal `FINISHED` status - the intermediate `COMPLETED` step is still recorded in the transfer's `timeline`.
When a new transaction is created, a [`transaction.created`](/integration/webhooks#event-types) webhook fires for it, followed by a `travel_rule.status.updated` for each transition (`COMPLETED` and `FINISHED` each fire one).
If crypto monitoring is enabled for your application, the deposit is also AML-screened and billed at the standard monitoring rate.

### Wallet ownership widget

To move a transfer out of `UNCONFIRMED_OWNERSHIP` - or to pre-verify a wallet before any transfer arrives - mint a hosted wallet-ownership widget with `POST /v3/travel-rule/widget-session/` and send your customer to the returned `url`. The widget proves control of the wallet by message signing, a Satoshi test, a screenshot, or self-declaration, then marks the matching address-book entry verified so the exchange can continue.

See [Wallet ownership widget](/transaction-monitoring/wallet-ownership-widget) for the full flow, the proof methods, and the important note that the widget must be opened in a real browser - never inside a mobile SDK webview.

## Directory

`GET /v3/travel-rule/vasps/?search=<name>` searches a combined directory of discoverable Didit customers and VASPs your compliance team has catalogued in the internal counterparty registry.

```bash theme={null}
curl "https://verification.didit.me/v3/travel-rule/vasps/?search=Bene" \
  -H "x-api-key: YOUR_API_KEY"
```

```json theme={null}
{
  "count": 2,
  "next": null,
  "previous": null,
  "results": [
    {
      "name": "Bene CASP GmbH",
      "lei": "",
      "jurisdiction": "EU",
      "dd_status": "COMPLETED",
      "dd_score": 0,
      "dd_score_breakdown": {},
      "dd_assessed_at": null,
      "reachable_rails": ["INTERNAL"],
      "is_didit_customer": true
    },
    {
      "name": "Northwind Digital Assets Ltd",
      "lei": "5493001KJTIIGC8Y1R12",
      "jurisdiction": "MT",
      "dd_status": "COMPLETED",
      "dd_score": 34,
      "dd_score_breakdown": {
        "licensing": 5,
        "sanctions_pep": 0,
        "country_risk": 14,
        "analytics_risk": 15
      },
      "dd_assessed_at": "2026-06-30T09:12:00Z",
      "reachable_rails": ["EMAIL"],
      "is_didit_customer": false
    }
  ]
}
```

Results are paginated with `limit` (default `50`) and `offset` query parameters; `count`, `next`, and `previous` describe the page window.

`is_didit_customer: true` entries are other Didit applications with Travel Rule enabled and `is_discoverable: true` - reachable on the `INTERNAL` rail. Their `dd_status` is always `COMPLETED` and `dd_score` always `0`.
`is_didit_customer: false` entries come from the internal counterparty registry and carry a real `dd_status`/`dd_score`.
The `dd_score` is a single point-banded risk figure (higher is riskier); `dd_score_breakdown` shows the per-component contributions - licensing and registry standing, sanctions and PEP exposure, jurisdiction risk, and on-chain analytics risk - all derived from checks Didit already runs in-house, with no third-party lookup in the request path. `dd_assessed_at` is when that assessment last ran.
Gate on the score with the [EU TFR rules bundle](#eu-tfr-rules-bundle) rather than branching on it inline.
Your own VASP profile never appears in your own directory results.

### How entries get into the directory

There is no pre-seeded global VASP list - the directory grows from real activity:

* Didit customers appear automatically once they enable Travel Rule and keep `is_discoverable: true`.
* An inbound `TRP` inquiry upserts the initiating VASP (name, LEI, TRP endpoint) into the counterparty registry.
* An outbound `TRP` exchange records the counterparty identity and endpoint it verified during the handshake.
* An [after-deposit sunrise registration](#after-deposit-sunrise-registration) that names `originating_vasp` upserts that VASP, including its `travel_address` when supplied.
* Didit can catalogue a counterparty for you ahead of any exchange - routing identifiers, jurisdiction, and compliance contact - through support.

Entries are deduplicated by LEI first, then by exact name, and existing entries are enriched rather than duplicated when new identifiers arrive.

### Routing identifiers

A directory entry can carry one routing identifier per network.
Outbound routing consults them whenever your transfer names the counterparty by `counterparty_vasp_lei` or `counterparty_vasp_name`:

| Identifier on the entry | Used by rail |
| ----------------------- | ------------ |
| `trp_endpoint_url`      | `TRP`        |
| `gtr_vasp_code`         | `GTR`        |
| `trust_membership_id`   | `TRUST`      |
| `verifyvasp_id`         | `VERIFYVASP` |
| `sygna_vasp_code`       | `SYGNA`      |
| `compliance_email`      | `EMAIL`      |

You can always bypass the directory on a single transfer by passing the explicit fields in [Counterparty resolution fields](#counterparty-resolution-fields).

## Webhooks

Subscribe a destination to `travel_rule.status.updated` to get notified on every status transition, on both the outbound and any `INTERNAL`-rail inbound leg.

```json theme={null}
{
  "event_id": "9c0c8b8a-1111-4222-9333-666666666666",
  "webhook_type": "travel_rule.status.updated",
  "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"
}
```

See the [webhooks guide](/integration/webhooks#event-types) for the full event catalog, signature verification, and retry policy.

## Pricing

Outbound Travel Rule exchanges are billed **\$0.02 per transaction**, charged once at creation regardless of how many status transitions the exchange goes through afterward.

* The inbound leg Didit creates on the beneficiary's application via the `INTERNAL` rail is **not** billed.
* If crypto AML/wallet screening also runs on the same transaction (for example an inbound deposit with `currency_kind: "crypto"` and monitoring enabled), that's billed separately at the standard AML monitoring rate - it is not bundled into the Travel Rule fee.
* Travel Rule transactions use this dedicated line item instead of standard transaction-monitoring pricing, not both.

### Attribution pricing

[Blockchain analytics attribution](#blockchain-analytics-attribution) needs one wallet lookup against the analytics provider, and you always get the full value of that lookup:

* **Crypto monitoring already screened the wallet on this transaction** - the stored result is reused. One analytics call, one AML monitoring charge (the one crypto monitoring already made). Attribution adds nothing.
* **Crypto monitoring did not run** - Didit makes the required analytics call once and passes its full value on: the transaction is charged the standard crypto AML monitoring rate (\$0.15 with Didit-managed analytics) and receives the complete crypto screening output in return - risk score, wallet risk category, network snapshot - not just the VASP name. The usage line item is marked with the `travel_rule_attribution` trigger so the charge is explainable.
* **Bring-your-own analytics key** - the same platform fee that applies to crypto monitoring with your own key applies here (\$0.02 per call). Your provider bills you for the underlying lookup directly.
* **Failed lookups are never charged**, resends reuse the recorded attribution without a new call or charge, and sandbox applications are never charged.

So an outbound transfer that needed attribution costs \$0.02 (Travel Rule) + one crypto AML monitoring charge - and when both products are enabled, the analytics call and its charge happen only once.

## EU TFR rules bundle

Four preset rules ship in the `travel_rule_eu_tfr` bundle, purpose-built for EU TFR obligations.
Like every other [preset bundle](/transaction-monitoring/rules-library), enable it from the Business Console under **Transactions → Rules → Library**.

| Rule (`library_key`)                 | Trigger                                                                                       | Action                                      |
| ------------------------------------ | --------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `tr_eu_mismatch_hold`                | Transfer status is `COUNTERPARTY_MISMATCHED_DATA`                                             | `+50` score, transaction set to `IN_REVIEW` |
| `tr_eu_unverified_selfhosted_review` | Transfer status is `UNCONFIRMED_OWNERSHIP` and the default-currency amount is at least `1000` | Transaction set to `IN_REVIEW`              |
| `tr_eu_vasp_not_found_score`         | Transfer status is `COUNTERPARTY_VASP_NOT_FOUND`                                              | `+25` score, tagged `travel-rule-sunrise`   |
| `tr_eu_expired_review`               | Transfer status is `EXPIRED`                                                                  | Transaction set to `IN_REVIEW`              |

These fire on every relevant transition - at transfer creation, on ownership confirmation, on email pickup response, and from the expiry sweeper - so a transfer that never resolves still surfaces for compliance review instead of settling silently.

## Why this works better on one platform

Travel Rule compliance is an identity problem wearing a payments costume - and Didit already holds the identity:

* **Verified data on the wire.** The originator information you transmit is the KYC profile Didit verified at onboarding (document OCR, liveness, face match) - bound by `vendor_data`, not retyped into a form. Standalone Travel Rule vendors transmit whatever you pass them.
* **Step-up biometrics for risky transfers.** A [rule](/transaction-monitoring/rules) can hold any high-value transfer until the user passes a biometric re-check against the face captured at onboarding - the strongest possible answer to account takeover at the moment of withdrawal. See the [worked example](/transaction-monitoring/sdk-transaction-submission#example-step-up-biometrics-for-a-high-value-withdrawal).
* **One device graph.** SDK-submitted transfers carry the same device intelligence as KYC sessions, so "new device + high amount + first-time beneficiary" is a single rule, not a data-warehouse join.
* **Screening, rules, cases, and the exchange in one place.** The wallet screening that attributes the counterparty VASP is the same screening your rules and analysts see - one charge, one console, one audit trail.
* **Published pricing.** [\$0.02 per outbound transfer](#pricing), no volume tiers, no sales call.

## FAQ

### How the exchange works

<AccordionGroup>
  <Accordion title="Does Didit run the VASP-to-VASP exchange itself, or does it only record Travel Rule data after another network completes the handshake?">
    Didit runs the exchange itself. When you submit a `travelRule` transfer, Didit performs live counterparty discovery, negotiates and verifies the originator and beneficiary data with the receiving VASP, and only clears the transfer once the exchange reaches `COMPLETED` - it is a managed handshake, not a post-settlement log. Didit also keeps the full record and compliance status of every exchange (a 13-state [lifecycle](#lifecycle), [webhooks](#webhooks), and an audit trail). If you prefer to run Travel Rule through your own process, Didit can instead record the data passively as your single system of record (see the passive-recording question below). Both modes are available; the active exchange is the default.
  </Accordion>

  <Accordion title="Is the exchange native to Didit, or does it depend on an external interoperability provider (Notabene, Sygna, VerifyVASP, TRP)?">
    It is native. Didit operates its own Travel Rule Protocol (TRP) infrastructure at `trp.didit.me` and implements the industry network protocols directly - `GTR` (Global Travel Rule), `TRUST`, `VerifyVASP`, and `Sygna Bridge` (spoken natively, including Sygna's ECIES-encrypted IVMS 101 and signed permission requests). Didit is not a reseller of a single third-party interoperability layer. Reaching a counterparty that lives on a specific membership network necessarily happens over that network - there is no single universal channel in the Travel Rule - but the discovery, handshake, verification, and recordkeeping are all executed by Didit.
  </Accordion>

  <Accordion title="Which Travel Rule networks and protocols does Didit support?">
    Natively implemented, tried in strict priority order: `INTERNAL` (both VASPs on Didit), `TRP` (OpenVASP Travel Rule Protocol), `GTR`, `TRUST`, `VerifyVASP`, `Sygna Bridge`, and an `EMAIL` fallback for counterparties with no tooling. CODE, TRISA, Notabene, and Veriscope are available on request through Didit's Marketplace. See [How Didit routes an exchange](/transaction-monitoring/travel-rule-interoperability#how-didit-routes-an-exchange) for the full routing table.
  </Accordion>

  <Accordion title="Which network should I use, and do I need my own memberships?">
    `TRP` is the universal baseline we recommend enabling: it needs no shared membership - publishing a [travel address](#travel-addresses) or TRP endpoint lets any Didit VASP reach you - and it is backed by [blockchain-analytics attribution](#blockchain-analytics-attribution) when a counterparty is known only by wallet address. For membership networks, choose by where your counterparties operate: GTR offers broad global coverage, TRUST is oriented to US VASPs, and VerifyVASP (which now also operates Sygna Bridge) has strong coverage in Asia. You can route over Didit's platform membership where available, or connect your own - both are supported, and you are not locked to a single network.
  </Accordion>

  <Accordion title="How does Didit find the counterparty VASP when I only have a wallet address?">
    Through a resolution chain: your [wallet address book](#wallet-address-book), then the protocol and network rails, then [blockchain-analytics attribution](#blockchain-analytics-attribution). If the transfer carries only a destination wallet, Didit resolves it against blockchain-analytics data to attribute the counterparty exchange or custody service, catalogues it in the [VASP directory](#directory), and routes automatically - so later transfers to that counterparty reach it directly.
  </Accordion>
</AccordionGroup>

### Pricing and licensing

<AccordionGroup>
  <Accordion title="How much does Travel Rule cost, and is it included in my KYC or verification plan?">
    Travel Rule is a separate, usage-based product - it is not bundled into identity verification. It is billed \*\*$0.02 per outbound transfer** ([published pricing](#pricing), no volume tiers, no sales call). When routing needs a blockchain-analytics lookup to identify the counterparty VASP, that lookup is charged once at the [attribution rate](#attribution-pricing) - from $0.15 managed, or \$0.02 with your own provider key (BYOK) - and is never double-billed if crypto monitoring already screened the same wallet. Transfers below your threshold and the inbound legs Didit mints on the beneficiary side are not charged the Travel Rule fee.
  </Accordion>

  <Accordion title="Will I be double-charged when Travel Rule and crypto monitoring both run?">
    No. The Travel Rule fee (\$0.02, outbound only) replaces the standard transaction-screening fee for `travelRule` transactions, and the blockchain-analytics lookup is charged once: if crypto monitoring already screened the destination wallet on the transaction, [attribution](#blockchain-analytics-attribution) reuses that stored result at no extra cost. Inbound legs Didit mints on the beneficiary side are never billed the Travel Rule fee.
  </Accordion>

  <Accordion title="Can Didit provide documentation or a capability statement for my VASP licensing or regulator file?">
    Yes. Didit's KYT and transaction monitoring, AML and sanctions screening, blockchain-analytics wallet risk, and Travel Rule capabilities are all documented publicly on docs.didit.me, and you are free to cite these pages in a regulatory file. For a formal product capability statement tailored to a specific submission - for example an AIFC/AFSA, MiCA, or other VASP licensing application - contact your Didit account team with the format your regulator requires.
  </Accordion>
</AccordionGroup>

### Operations and data

<AccordionGroup>
  <Accordion title="What happens to transfers below my threshold?">
    They resolve immediately to `NOT_APPLICABLE`: no rail is tried, no identity data leaves Didit, and no Travel Rule fee is charged beyond the transfer's own creation. The EU TFR has no minimum, so EU CASPs should keep `threshold_amount` at `0.00`.
  </Accordion>

  <Accordion title="What if the counterparty never responds?">
    The transfer sits in `AWAITING_COUNTERPARTY` until `confirmation_timeout_hours` elapses, then a sweeper moves it to `EXPIRED` and applies your `timeout_outcome` to the underlying transaction - `HOLD` for review, `REJECT` to decline, or `PROCEED` to leave it untouched. The `tr_eu_expired_review` preset rule additionally surfaces every expiry to your analysts.
  </Accordion>

  <Accordion title="My counterparty has no Travel Rule tooling at all. Can I still comply?">
    Yes - that is the `EMAIL` rail. Their compliance contact receives a one-time pickup link and can confirm or decline the exchange from a hosted page with no Didit account, no API key, and no integration. For deposits that already settled before any exchange was possible, use [after-deposit sunrise registration](#after-deposit-sunrise-registration).
  </Accordion>

  <Accordion title="How do withdrawals to the user's own wallet work?">
    Declare the destination as self-hosted (`payment_method.method_type: "unhosted_wallet"`). Didit skips VASP routing and instead collects proof that your customer controls the wallet - message signing, Satoshi test, screenshot, or self-declaration - through the [wallet-ownership widget](/transaction-monitoring/wallet-ownership-widget). That is the EU TFR self-hosted wallet obligation in API form.
  </Accordion>

  <Accordion title="Is counterparty PII protected during the exchange?">
    Yes. Inbound transfers omit `originator_data`/`beneficiary_data` entirely until wallet ownership is confirmed on your side, email pickup pages show only the transfer summary (originator VASP name, amount, destination wallet) - never the person payloads - and rail exchanges run over authenticated HTTPS, with the Sygna rail additionally end-to-end encrypting the IVMS payload.
  </Accordion>

  <Accordion title="I already track Travel Rule compliance with my own process. Can I just record it?">
    Yes. With Travel Rule disabled, any `travel_rule_details` you submit (including a caller-supplied `status`, current or legacy) is recorded passively on the transaction - no rail is dispatched, nothing is negotiated. Use this to backfill or run your own process while keeping one system of record.
  </Accordion>

  <Accordion title="How do I test this before going live?">
    Sandbox applications skip rules, webhooks, and billing, but the exchange engine itself still runs - including real rail dispatch when networks are configured - so use test counterparty data. The `INTERNAL` rail is the easiest full-loop test: register a wallet in a second application's address book and watch both sides transition.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Wallet ownership widget" icon="wallet" href="/transaction-monitoring/wallet-ownership-widget">
    Prove a customer controls a wallet by signing, Satoshi test, screenshot, or self-declaration.
  </Card>

  <Card title="Interoperability" icon="handshake" href="/transaction-monitoring/travel-rule-interoperability">
    The counterparty-facing endpoints other VASPs use to reach you: TRP, callbacks, email pickup, sunrise.
  </Card>

  <Card title="Submitting transactions" icon="arrow-right-to-bracket" href="/transaction-monitoring/transactions">
    How to include travel rule data in the Create Transaction payload.
  </Card>

  <Card title="Rules library" icon="scale-balanced" href="/transaction-monitoring/rules-library">
    The full preset rule catalog, including the Travel Rule bundle.
  </Card>

  <Card title="Webhooks" icon="bell" href="/integration/webhooks">
    Subscribe to travel\_rule.status.updated and every other event family.
  </Card>

  <Card title="Console guide" icon="gauge" href="/transaction-monitoring/console">
    Viewing travel rule status in the transaction detail view.
  </Card>
</CardGroup>
