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 tries the rails in priority order to reach the counterparty VASP, runs the exchange over the first one that reaches it, and, where the other side returns a beneficiary name, matches it against the one you sent.
- Only once the exchange reaches
COMPLETEDshould you broadcast the transfer on-chain. - You then report the on-chain transaction hash back to Didit to close the exchange out as
FINISHED.
GTR, TRUST, VERIFYVASP, SYGNA) need a network membership on the sending side, and today that membership has to be your own.
Connect your membership credentials in the console Marketplace (Travel Rule networks category), and your transfers travel under your own network identity.
Didit holds no platform membership for any of the four networks today, and is onboarding as a provider on them.
Until that onboarding completes, platform_available is false for all four, the settings didit mode (routing over Didit’s platform membership) is not available for any network, and disabled_networks has nothing to switch off.
A membership you connect yourself always takes precedence over a platform membership.
No network membership is included by default, including GTR.
Enabling Travel Rule does not activate GTR, TRUST, VerifyVASP, or Sygna.
Check Get Travel Rule Settings for your application’s effective networks modes.
If all four report unavailable, none can route an exchange: connect your own membership in Marketplace, or use a reachable INTERNAL, TRP, or EMAIL counterparty.
Even with your own membership, a network rail routes a transfer only when its beneficiary_data carries the counterparty’s id on that network (counterparty_gtr_vasp_code, counterparty_trust_membership_id, counterparty_verifyvasp_id, or counterparty_sygna_vasp_code), or the counterparty’s directory entry lists it on that network (see Routes).
platform_available reports whether platform membership is configured, not a live network health check. Similarly, connected means your membership credentials are saved and have passed local validation; the first live exchange verifies them against the network. A didit or own mode does not guarantee counterparty reachability or successful delivery.
Even with your own membership, GTR, TRUST, and VERIFYVASP only send today: Didit delivers your message over the network, but it does not process the counterparty’s reply yet.
A transfer sent over one of them stays in AWAITING_COUNTERPARTY until confirmation_timeout_hours elapses, then moves to EXPIRED and your timeout_outcome applies.
Among the network rails, only SYGNA processes the counterparty’s answer today, through Didit’s Sygna callback.
Further networks - CODE (the Korean alliance), TRISA, Notabene, and Veriscope - are listed in the console Marketplace as request-access tiles.
Didit has no integration with them yet, so no transfer can route over them today.
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, and re-runs the rails once with the new name.
See Blockchain analytics attribution for the full behavior and pricing.
A transfer whose destination is declared as a self-hosted wallet skips all seven rails - there is no counterparty VASP to exchange with - and goes straight to end-user wallet-ownership proof.
/v3/ endpoints described on this page.What executes when you submit a transfer
Everything below runs synchronously inside the onePOST /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.
Step by step:
- Validation and subject binding. The payload is validated.
A
transaction_idalready used in your application is rejected with400, and when two requests with the same id race, one of them gets409. Thesubjectis linked to your user or business profile byvendor_data, so the transfer shows up on that profile for monitoring. A blocklisted subject or counterpartyvendor_datais rejected with400, and no transaction is created. Blocklisted wallets, bank accounts, IPs, and devices are checked in step 6 and decline the transaction there, after routing has run. - Device and network enrichment. Party IPs are geo-enriched, and any device context (automatic on SDK submission) is normalized into the device graph - the same graph KYC sessions feed, so “related by device” works across onboarding and transactions.
- 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.
- Travel Rule routing. The exchange engine checks the threshold, honors a self-hosted wallet declaration, then walks the seven rails in priority order; if none resolve, 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,NOT_APPLICABLE(below threshold),UNCONFIRMED_OWNERSHIP,COMPLETEDorCOUNTERPARTY_MISMATCHED_DATA(INTERNALrail),COUNTERPARTY_VASP_NOT_REACHABLE, orCOUNTERPARTY_VASP_NOT_FOUND. Each status change queues atravel_rule.status.updatedevent beforetransaction.createdis queued, so these events usually arrive first, but delivery order is not guaranteed. - Wallet-ownership minting. If the transfer needs end-user proof of control and
auto_wallet_verificationis on, a wallet-ownership widget session is minted and returned inaction_required. - Rules evaluation. Every active rule runs - the EU TFR 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 (AWAITING_USER). - Billing and notification. For an outbound transfer, the $0.02 Travel Rule line is recorded, whatever the routing outcome.
The response returns with the full picture, and
transaction.createdfires to your webhook destinations.
How each step maps to your obligations
APPROVED (until a transfer expires, see below) and nothing is charged.
The Travel Rule exchange engine itself still runs on sandbox applications when enabled - the transfer is created and moves through its statuses - but outbound dispatch is simulated.
No rail is selected and no compliance message ever reaches a real counterparty VASP, so an outbound transfer that reaches routing parks at AWAITING_COUNTERPARTY and then expires after its confirmation deadline, which applies your timeout_outcome (by default the transaction moves to IN_REVIEW).
Inbound registration (POST /v3/travel-rule/inbound/) never dispatches, so sandbox resolves it against your wallet address book and settles it for real - ON_HOLD, UNCONFIRMED_OWNERSHIP, COUNTERPARTY_MISMATCHED_DATA, or COMPLETED then FINISHED.
See Transaction Monitoring in sandbox.Lifecycle
Every transfer has astatus 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, 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.
An exchange status never changes the transaction status by itself, with one exception: an expiry applies your configured timeout_outcome (or a sunrise or non-responsive policy when one applies).
Your rules re-run on every transition instead, so the EU TFR rules or any rule keyed on travel_rule_status can move the transaction, for example to IN_REVIEW.
A transfer that needs end-user proof of wallet control adds an action_required block to the transaction, but its status stays as your rules left it.
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 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 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).
CANCELLED is additionally reachable from every non-terminal state with action: "cancel"):
Legacy status values
If you were already sendingtravel_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 condition values, and normalize to the canonical form:
Setup
Configure your VASP profile and negotiation policy once per application withGET/PUT /v3/travel-rule/settings/.
PUT is a partial update - send only the fields you want to change.
Name matching strictness
Ownership validation modes
ownership_validation_mode decides how much proof a wallet’s ownership takes before a transfer relies on it:
FULL and SIMPLIFIED, a later transfer reuses it only when a proof the mode accepts for that transfer backs it.
When inbound_auto_accept has no address-book wallet to match, a crypto wallet you recorded on exactly one end user’s transactions identifies the holder.
Didit adds that wallet to the end user’s address book as your self-declaration, verified unless the mode is FULL.
Data sharing
data_sharing_fields chooses which fields Didit sends to the counterparty about your own users, separately for individuals and companies:
- Individuals can share
name,wallet_address,address,national_id,customer_identification,date_of_birth, andplace_of_birth. Companies use the first five. nameandwallet_addressare always sent.- An entity type you never chose shares every field. An empty list is a choice too: it shares the name and the wallet only.
- A
PUTmerges per entity type, so sendingindividualleaves the storedcompanychoice alone. - A choice that cannot meet the originator obligations of your
jurisdictionregime (EU TFR or FATF Recommendation 16), an empty list included, is refused with400. The errors sit underdata_sharing_fields, keyed by entity type.
Per-field matching
matching_rules sets how strictly each field of the counterparty’s data is compared with what you hold:
NONE, STRICT, CONTAINS, DEFAULT, or FUZZY.
Defaults: name follows name_matching_strictness, date_of_birth and address are NONE, and account is STRICT.
A field left out keeps its default, and a field the entity type does not compare (a company has no date_of_birth) is refused with 400.
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:
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 GTR, TRUST, and VERIFYVASP rails their own encrypted envelopes - so one payload shape reaches every counterparty in the format their protocol expects.
Didit reads these keys from each person payload when it builds the IVMS 101 message: first_name and last_name (or a single name or full_name), date_of_birth, place_of_birth, residence_country (or country), vendor_data (or customer_identification), and wallet_address.
Two practical rules:
- Latin script. IVMS 101 requires Latin-script values; transliterate names that are natively written in another script. Didit’s name matching compares normalized token sets, so ordering and casing differences don’t fail an exchange - but different scripts on the two sides will.
- Send data you have verified. On the rails that carry person data (
INTERNAL,TRP, and the network rails), Didit transmits exactly theoriginator_datayou send; it does not fill it in from the KYC profile behindvendor_data. Build the payload from the identity you verified at onboarding (for example the document-extracted name, date of birth, and address of a Didit-verified user), not from an unverified form field. That is what the FATF’s “required and accurate originator information” wording demands. On theINTERNALrail, the beneficiary’s application also receives thesubjectyou sent (name,vendor_data, date of birth, country, and address) as the counterparty party of its inbound transaction.
Travel addresses
A travel address is a compact, encoded string that points at a VASP’s inboundTRP inquiry endpoint - it never exposes a raw URL or any wallet address.
Once Travel Rule is enabled, Didit derives your own travel_address (returned read-only in settings) so you can share it with other VASPs that reach you on the TRP rail.
When you know a counterparty’s travel address, store it on the destination wallet address book entry so Didit routes that beneficiary’s transfers straight over TRP instead of falling back to email.
Didit validates the travel addresses you store on a wallet address book entry or send in originating_vasp: each must start with ta and decode to an inquiry URL carrying t=i, so a placeholder such as ta... returns 400.
A beneficiary_data.travel_address on a transaction is not validated: if it does not decode, the TRP rail is skipped.
The encoding is Base58Check with a ta prefix: the endpoint host and path (which always carries the query pair t=i) plus a four-byte double-SHA-256 checksum, base58-encoded.
A real address therefore looks like this, and runs to roughly 100 characters:
400 with "Invalid travel address.".
Take a real address from your own settings response, or from the counterparty itself.
Testing without network credentials
You do not need a GTR, TRUST, VerifyVASP, or Sygna membership to test the membership-free paths. You still need a Didit API key for the application under test.- Sandbox inbound: enable Travel Rule with a legal name, register a destination wallet in the address book, then call Register Inbound Travel Rule Transfer. Test an unknown wallet, an unverified wallet, a beneficiary-name mismatch, and a verified matching wallet. These exercise real inbound decisions without contacting a network.
- Sandbox outbound: submit a
travelRuletransaction. After threshold and self-hosted-wallet checks, dispatch is simulated and ordinarily waits atAWAITING_COUNTERPARTY. No network, email, or internal counterparty receives it; this does not prove delivery or completion of a two-party exchange. - Two controlled live applications: use the
INTERNALrail between your own applications, or a controlled TRP counterparty, to test the exchange lifecycle without commercial network credentials. Live applications can incur normal charges and notifications; use test identities and counterparties you control.
Sending a transfer
Include the transfer onPOST /v3/transactions/ by setting transaction_category to travelRule (or travel_rule) and attaching a travel_rule_details object (alias: travelRule).
The travel_rule_details block itself is optional, but once you attach it, travel_rule_details.status is required: a block without it returns 400.
When Travel Rule is enabled for your application, Didit ignores the status value you send and drives the exchange itself:
- If the amount is below your configured
threshold_amount, the transfer resolves immediately toNOT_APPLICABLEand nothing is negotiated. - 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 toUNCONFIRMED_OWNERSHIPand the wallet-ownership widget is auto-minted (see Declared self-hosted wallets). - Otherwise the rails run in priority order, and the first one that can reach the counterparty carries the exchange:
INTERNAL-beneficiary_data.wallet_addressis registered in the wallet address book of another Didit customer that is a member of the Didit network.TRP- the transfer’s asset has a Digital Token Identifier mapping (BTCandETHtoday), and the payload carriesbeneficiary_data.travel_address, the destination’s own address-book entry stores one, or the counterparty resolves in the directory (bycounterparty_vasp_leiorcounterparty_vasp_name) with a TRP endpoint.GTR,TRUST,VERIFYVASP,SYGNA- you connected your own membership for that network in Marketplace, and the counterparty resolves to a network id (explicit field or directory entry - see Counterparty resolution fields). Didit holds no platform membership on these networks yet (it is onboarding as a provider), sodisabled_networksdoes not change this step today.EMAIL-beneficiary_data.counterparty_vasp_emailwas supplied, orcounterparty_vasp_nameresolves to a directory entry with a compliance email.
- If no rail can resolve a counterparty and
vasp_attribution_enabledis on, Didit runs blockchain analytics attribution: the destination wallet is resolved to its owning VASP, the attributed name is written tobeneficiary_data.counterparty_vasp_name(when empty) and to the 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. - 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 (whenauto_wallet_verificationis on) so the transfer can still complete as a self-hosted-wallet transfer, and you can re-run routing later withaction: "resend"once you have better counterparty data.
Counterparty resolution fields
Everything Didit knows about the counterparty comes fromtravel_rule_details.beneficiary_data.
These fields drive rail resolution; all are optional:
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 knows which wallets belong to which exchanges and custodians. Attribution runs only on outbound transfers that carry both a destination wallet and achain (see Counterparty resolution fields); without a chain it is skipped.
When the wallet is attributed to a known entity, Didit:
- Records the attribution on the transfer:
beneficiary_data.counterparty_vasp_nameis filled (when you did not supply one), and the transfer’s metadata carries avasp_attributionblock with the entity name, entity type, country, andsource: "blockchain_analytics". - Adds the entity to the VASP directory (or matches the existing entry), so the attribution is stable across transfers.
- Re-runs the rails once with the attributed name - a directory entry that already carries a TRP endpoint makes the transfer route immediately instead of landing in
COUNTERPARTY_VASP_NOT_FOUND.
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 or your balance cannot cover it, 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.
See 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.
An outbound passive transfer is still billed the Travel Rule line.
With Travel Rule enabled, this request routes over the EMAIL rail, because it names the counterparty’s compliance email and no earlier rail can reach the beneficiary:
originator_datais stored exactly as you sent it; Didit does not fill it from the KYC profile. TheEMAILrail does not show it to the recipient: the pickup endpoint returns only the transfer summary.beneficiary_data.chainisETHbecause the request named no chain, so Didit took it from the currency.statusisAPPROVEDbecause no rule acted on the transaction; your own rules may set another status.- The compliance contact receives the pickup link, and the transfer completes, fails the name match, or declines when they answer, or expires if they do not (see Email pickup).
holder_name matches, the INTERNAL rail resolves the whole exchange synchronously instead, and the response shows "status": "COMPLETED" with "protocol": "INTERNAL".
Without counterparty_vasp_email, and when neither the rails nor attribution can reach the beneficiary, the same request ends in COUNTERPARTY_VASP_NOT_FOUND with a wallet_ownership action.
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, counterparty_vasp, and created_at.
That object carries no obligations_count or metadata.COMPLETED outbound transfer once you’ve broadcast it on-chain:
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.
On the TRP rail, Didit also posts the hash to the counterparty’s confirmation callback.
To cancel a transfer that hasn’t reached a terminal state yet, send {"action": "cancel"} to the same endpoint instead.
The INTERNAL-rail counterpart is cancelled too, and on the TRP rail, once the counterparty has approved the inquiry, Didit posts the cancellation to its confirmation callback.
A transfer already in FINISHED, CANCELLED, or EXPIRED returns 409.
A request with neither payment_txn_id nor a supported action returns 400.
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 by adding a travel_address to the destination wallet entry.
Then send {"action": "resend"} to the same endpoint.
Didit restarts the confirmation deadline and re-runs the routing ladder from the start, using the data stored on the transfer.
Any other status, or an inbound transfer, returns 409.
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 tounhosted_wallet.
- No rail is tried - there is no counterparty VASP to exchange with - and the transfer is created directly in
UNCONFIRMED_OWNERSHIP. - When
auto_wallet_verificationis on (the default), Didit auto-mints a wallet-ownership widget session bound to the transaction and returns it in the transaction’saction_requiredblock; the SDKs launch it automatically on client-side submission. - Once the customer proves control of the wallet, the transfer runs the beneficiary name match and advances to
COMPLETED(orCOUNTERPARTY_MISMATCHED_DATAon a failed match).
UNCONFIRMED_OWNERSHIP is not a resendable state - complete the ownership proof (or cancel) instead of calling action: "resend".
Receiving
Confirming wallet ownership
When theINTERNAL 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:
confirmed (required, boolean) and an optional wallet_address.
The endpoint accepts a transfer in any of these states, and returns 409 with {"detail": "Transfer is not awaiting ownership confirmation."} for anything else:
UNCONFIRMED_OWNERSHIP, in either direction.ON_HOLDon an inbound transfer.UNCONFIRMED_OWNERSHIP,ON_HOLDorCOUNTERPARTY_MISMATCHED_DATAon an inboundTRPtransfer, so a compliance officer can resolve a counterparty inquiry by hand.
INTERNAL rail, sunrise registration, or a declared self-hosted wallet):
confirmed: truemarks the matching wallet address book entry as ownership-verified, then runs the name match against that entry’sholder_name-COMPLETEDif it passes,COUNTERPARTY_MISMATCHED_DATAif it doesn’t. TheINTERNAL-rail counterpart, when there is one, gets the same outcome. An inbound transfer whose deposit has already settled then moves straight on toFINISHED.confirmed: falsemoves both sides of the transfer toCOUNTERPARTY_VASP_GENERAL_DECLINE.- The matching entry is looked up in your own address book by wallet address alone, without the chain.
Resolving an inbound
ON_HOLDwith no matching entry creates the entry from the transfer’sbeneficiary_data.wallet_addressand marks it verified. In every other case, a missing entry returns409with{"detail": "wallet address entry no longer available"}, which is also what a deleted entry returns. On an outbound declared self-hosted transfer, that means the call succeeds only if the destination wallet is already in your own address book; the wallet-ownership widget creates the entry itself.
TRP transfer the same call answers the counterparty’s inquiry:
confirmed: trueneeds a payment address to return, taken from the request’swallet_address, then the matched entry, thenbeneficiary_data.wallet_address. With none of the three the request returns400with{"wallet_address": ["wallet_address is required to approve."]}. Didit creates or verifies the entry, sends the inquiry-resolution callback to the counterparty, setsownership_confirmed, and moves the transfer toCOMPLETED.confirmed: falsesends a declined resolution and moves the transfer toCOUNTERPARTY_VASP_GENERAL_DECLINE.- If the counterparty’s callback endpoint cannot be reached, the request returns
502and the transfer keeps its current status. On an approval, the wallet address book entry is still created or marked verified.
INTERNAL transfer from another Didit customer also needs a control proof (a signed message or a test deposit) before it can be confirmed, and answers 409 until it has one.
Masked counterparty data
Until wallet ownership is confirmed, the counterparty’s personal data on an inbound transfer comes back masked rather than left out. The first letter of each word is kept, and the wallet, chain, and VASP routing stay readable.data_masked is true, and the counterparty party in the transaction detail, list, and CSV export is masked the same way.
Once ownership is confirmed the real values appear and data_unmasked records the audit entry:
reason is ownership_confirmed, wallet_proof, address_book_match, own_verified_wallet, or inquiry_approved.
actor is the console user’s email or the API key label, and blank for an automatic decision.
Mismatch feedback
ACOUNTERPARTY_MISMATCHED_DATA transfer now names the fields that differ.
data_match holds the comparison and mismatched_fields lists the fields.
Both carry field names and results only, never values, so they stay readable while the data is masked:
TRP rejection after a mismatch tells the originating VASP which fields differ, for example “Beneficiary data does not match the wallet holder: name”.
Messages and document requests
Two VASPs can resolve a disputed transfer inside Didit instead of by email.GET and POST /v3/transactions/{transaction_id}/travel-rule/messages/ list and send messages on a transfer:
kindisMESSAGE(default) orDOCUMENT_REQUEST. A document request needs at least one entry inrequested_documents(up to 10); a message needs abody(up to 5000 characters) or a file.reply_tonames an earlier message of the same transfer. A reply that carries a document fulfils the document request it answers, andfulfilled_atis set on it.- Send multipart with
filesto attach up to 5 files of 10 MB each. - A transfer holds up to 100 messages; beyond that the request answers
400. - Messages work over the
INTERNALandEMAILrails. OnINTERNAL, the other Didit customer sees the message on its side of the pair. OnEMAIL, the counterparty is emailed and answers in the pickup portal. - The request answers
409with a reason indetailfor a transfer overTRP,GTR,TRUST,VerifyVASP, orSygna(those protocols carry no messages), a transfer with no counterparty yet, anINTERNALtransfer whose data did not match the wallet holder (nothing reached the recipient), and anEMAILtransfer whose pickup link has expired or whose transfer is closed. - No webhook fires when the counterparty writes back. Read the messages endpoint.
- The email telling the counterparty about a new message goes out in English.
Wallet address book
Register the wallets you control so inbound transfers (INTERNAL rail, inbound TRP inquiries, and after-deposit registrations) can resolve against them, with POST/GET /v3/travel-rule/wallet-addresses/ and PATCH/DELETE /v3/travel-rule/wallet-addresses/{entry_uuid}/.
PATCH accepts holder_name, holder_vendor_data, entity_type, and travel_address, and returns only those four fields.
DELETE deletes the entry (204) - its address is freed up for re-registration and it immediately stops matching new transfers.
chain.
When a transfer carries a chain, an entry matches only if its chain is the same value (case-insensitive) or empty.
Chain names are compared as text, not translated, so an entry stored as ethereum does not match a transfer whose chain is ETH.
Outbound transfers take their chain from beneficiary_data.chain, then transaction_details.crypto_params.crypto_chain, then custom_properties.crypto_chain, then the currency (for example ETH, or for USDT and USDC the chain inferred from payment_reference_id), set only when it is a chain Didit recognizes, and after-deposit registrations from their chain field, lowercased.
Inbound TRP inquiries ignore chain: they match by address, or, when the inquiry names no wallet, by beneficiary name against your verified entries.
POST .../travel-rule/ownership/ also matches by address alone.
When several entries match, the INTERNAL rail and after-deposit registrations prefer an ownership-verified entry, then the oldest, and the outbound TRP lookup takes the oldest entry that stores a travel address.
Inbound TRP inquiries and the ownership endpoint take any one of the matching entries, so keep one entry per address where you can.
The INTERNAL rail matches no entry at all when verified entries for the address exist in more than one other application.Email pickup for unreachable counterparties
When no protocol rail can reach the counterparty, Didit falls back toEMAIL: 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.
The link opens a hosted page that shows the pending exchange and calls these two public endpoints for the recipient.
For the example transfer above, the pickup details are:
decision is accept or decline.
On accept, the supplied beneficiary_name is compared with the transfer’s beneficiary_data.name under 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.
The response is the resulting {"status": "..."}.
Both endpoints return 404 for an unknown token and 410 once the window closes - either the transfer left AWAITING_COUNTERPARTY, or its confirmation deadline (confirmation_timeout_hours after creation or the last resend) has passed.
The counterparty reads and answers through the pickup link with GET and POST /v3/travel-rule/pickup/{token}/messages/.
It needs no API key, accepts PDF, PNG, JPEG, or WebP attachments only, and answers 410 once the link expires or the transfer closes.
A message you sent shows your VASP name as author and as the attachment’s uploaded_by, never the person or API key behind it.
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 withPOST /v3/travel-rule/inbound/.
originating_vasp.travel_address must be a real, checksum-valid travel address: a placeholder returns 400 with "Invalid travel address.".
originating_vasp.lei must be empty or 20 characters in the LEI format (18 letters or digits, then 2 digits); anything else returns 400 with "Invalid LEI: expected 20 alphanumeric characters (ISO 17442).".
Didit checks this format only, not the ISO 17442 check digits.
travelRule transaction (or reuses an inbound deposit you already submitted with the same hash and wallet, if it has no Travel Rule transfer yet) and resolves the transfer against your wallet address book:
- No matching entry ->
ON_HOLD. Resolve it withPOST .../travel-rule/ownership/:confirmed: trueadds the wallet to your address book as verified and runs the name match; on a match the transfer moves toCOMPLETED, thenFINISHED. - 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 immediatelyFINISHED(the deposit has already settled).
txn_id is tr-in-, the first 80 characters of the txid, and a 12-character hash of the txid and wallet.
created is true (HTTP 201) when this call registered the deposit, and false (HTTP 200) when the same txid and wallet_address were already registered, in which case the existing transfer is returned unchanged.
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.
Each transition fires a travel_rule.status.updated webhook (COMPLETED and FINISHED each fire one), and when Didit created a new transaction, a transaction.created webhook follows them.
Registration carries no Travel Rule or monitoring fee.
When crypto monitoring is enabled and the chain is one Didit can screen, a newly created deposit is also screened and billed once at the Wallet Verification & Screening rate.
Wallet ownership widget
To move a transfer out ofUNCONFIRMED_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.
A screenshot is the exception: it never verifies the wallet by itself, so your compliance team reviews it and confirms a transfer in UNCONFIRMED_OWNERSHIP with POST .../travel-rule/ownership/.
See 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/ searches the VASP directory: the shared directory entries plus the members of the Didit network. Every row says how your application can reach that VASP today.
search matches a name or alias fragment, an exact LEI, or a domain (counterparty.example or a URL on it). Narrow the results with:
risk and responsiveness return directory entries only. Results are sorted by name and paginated with limit (default 50) and offset. An unknown network, country code, risk band or responsiveness status answers 400 naming the field. GET /v3/travel-rule/vasps/{vasp_id}/ returns one row by its id. Your own VASP profile never appears in your own results.
dd_status, dd_score, dd_score_breakdown, dd_assessed_at, dd_score_semantic, risk_level, responsiveness and essential_checks carry the shared due-diligence assessment of a directory entry. decision, entity_screening and questionnaires are your application’s own records on the VASP, and no other customer sees them. See Counterparty due diligence for what each one means, and gate on them with the counterparty rule fields rather than branching on them inline. Didit network members are not scored: dd_status is COMPLETED, dd_score is null, risk_level is empty and the breakdown is empty.
Routes
routes lists the ways your application can deliver to the VASP, in the order the routing ladder tries them, and reachable is true when at least one is available:
TRPwhen the entry carries a TRP endpoint the directory verified.GTR,TRUST,VERIFYVASP,SYGNAwhen a network lists the VASP under a code. The route isavailableonly when your application can send over that network today (via: own_membershipfor your own connected membership,via: didit_membershipfor Didit’s). Otherwise it isunavailablewithreason: no_membershiporreason: network_switched_off, never shown as a way to reach the VASP. Didit’s own network memberships are not live yet, so today a network route is available only through a membership you connected in the Marketplace.EMAILwhen there is a compliance contact for the entry.INTERNALfor a Didit network member.
TRISA listing appears under networks and tells you who the VASP is on TRISA, but Didit has no TRISA rail, so it is never a route.
Brands and their entities
A brand that operates through several regulated entities is one row whoseentities are the legal entities that receive its transfers; an entity row points back at its brand in umbrella. A transfer that names the brand goes to the reachable entity in the jurisdiction it states in beneficiary_data.counterparty_vasp_jurisdiction, else to the entity blockchain analytics attribution linked to the destination wallet, else to the brand’s only reachable entity when no jurisdiction is stated. Two entities that fit equally, or a stated jurisdiction none of them is in, never pick one: which legal entity receives the originator’s data is never a guess. The brand’s own route is used then, and without one the transfer is COUNTERPARTY_VASP_NOT_FOUND.
Contacts
contacts are the addresses the EMAIL rail writes to. scope: platform contacts serve every Didit customer: Didit curates them (source: staff), or an address at one of the VASP’s own domains answered a request through its response link (source: learned). scope: application contacts are private to your application. A transfer emails your application’s own contact first, then a platform contact that has answered before (verified_at set), then the others.
How entries get into the directory
- The ESMA interim MiCA register of crypto-asset service providers, read every week: each provider’s name, LEI and home member state.
- The public TRISA directory, read every week: who each member is, its country and its TRISA listing. A listing the directory stops showing for three weeks is removed.
- The global LEI register fills in the LEI, other legal names and the country of an entry, and finds the LEI of an entry by its exact legal name in its country.
- Blockchain analytics attribution adds or matches the VASP that owns a destination wallet, and keeps the attributed name as an alias.
- Didit staff curate entries: routing identifiers and network listings they verified, compliance contacts, aliases, brands and their entities, merged duplicates and hidden entries.
originating_vasp or a travel address, stays on that customer’s transfer and never changes a shared entry. Entries are matched by LEI first, then by exact name or alias, and enriched rather than duplicated.
The Didit network
Your VASP profile is a member of the Didit network, so other Didit customers find it in the directory and reach you on theINTERNAL rail, only while all of these hold:
- your application is live (not sandbox) and Travel Rule is on;
is_discoverableistrueandlegal_nameis set;compliance_emailis set and confirmed: send the confirmation withPOST /v3/travel-rule/settings/compliance-email/confirmation/and open the emailed link. Changing the address needs a new confirmation;- Didit has not marked the application as a test, demo or internal one.
GET /v3/travel-rule/settings/ returns didit_network: listed, compliance_email_confirmed, and in missing each condition still unmet (travel_rule_off, sandbox_application, not_discoverable, legal_name_missing, compliance_email_missing, compliance_email_unconfirmed, excluded_by_staff, internal_organization).
Routing identifiers
Outbound routing consults a directory entry whenever your transfer names the counterparty bycounterparty_vasp_lei or counterparty_vasp_name (an alias works too). An entry carries one identifier per network it can be reached on, a verified TRP endpoint and its compliance contacts, and routes shows which of them your application can use. You can always bypass the directory on a single transfer by passing the explicit fields in Counterparty resolution fields.
Counterparty due diligence
Before your customers’ funds reach another VASP, you need to know who that VASP is and whether you accept it. Due diligence on a counterparty has two layers:- The shared assessment. Every directory entry carries a due-diligence score, its risk level, a responsiveness status and an essential-checks report. Didit computes them from the platform’s own data, so every customer sees the same values.
- Your own records. Your decision on the VASP, your sanctions and adverse-media screening of it, and the due-diligence questionnaires you exchanged with it belong to your application alone. Another customer never sees them, and they never change the shared entry.
id the directory shows.
Score and risk level
dd_score runs from 0 to 100 and higher is safer. Every result says so in dd_score_semantic, which is always higher_is_safer. risk_level is the band the score falls in:
dd_score_breakdown shows each one as {points, max, reason, detail}: max is the component’s weight, reason a machine-readable code you can translate, and detail a one-line English explanation.
history reads the same requests as responsiveness, with no time window: a COMPLETED or FINISHED exchange is a success, and an EXPIRED, COUNTERPARTY_MISMATCHED_DATA or COUNTERPARTY_VASP_GENERAL_DECLINE exchange a failure. A cancellation is the sender’s own act and never counts.
Didit recomputes the assessment when an exchange with the entry completes, finishes, expires, mismatches or is declined, when it refreshes or curates the entry, and for every entry in the weekly directory refresh. dd_assessed_at says when it last ran; an entry not assessed yet has no dd_assessed_at, a dd_score of 0 and an empty risk_level, so read risk_level rather than the raw score. Didit network members are not scored: their dd_score is null, their risk_level is empty and their dd_score_breakdown is empty.
Essential checks
essential_checks is a short report on four checks Didit runs on every directory entry, from the platform’s verified sources only, never from data a customer reported:
checks reports its status (pass, fail, or unknown when no source on record can decide it), a machine-readable reason and a one-line English detail. passed, failed, unknown and total count them, level is essential, and checked_at says when the report was built. The essentials component scores the share of decided checks that pass, so an unknown check counts neither for nor against the entry. The report is empty until the weekly directory refresh first checks an entry, and always empty for a Didit network member.
Responsiveness
responsiveness says whether the VASP answers Travel Rule requests:
total counts the requests Didit customers started towards the VASP in the last 90 days whose outcome is decided, and answered the ones it answered:
- Counted: requests sent over a network or protocol rail, and requests emailed to a shared address, which is the entry’s compliance address or a platform contact.
- Never counted: a request emailed to an address one customer typed, or to a customer’s private contact. That exchange stays in the customer’s own transfer history and never labels the VASP for everyone else.
- Answered:
COMPLETED,FINISHED,COUNTERPARTY_VASP_GENERAL_DECLINEorCOUNTERPARTY_MISMATCHED_DATA. A decline or a mismatch is an answer: the VASP engaged. - Unanswered:
EXPIRED. - Neither: every other status. A
CANCELLEDrequest is the sender’s own act,COUNTERPARTY_VASP_NOT_REACHABLEcan be a failure on the sender’s side, and a request still in progress is not decided yet.
UNKNOWN with zero counts. Act on an unresponsive counterparty with the non-responsive policy or the travel-rule-counterparty-unresponsive preset.
Your decision on a counterparty
Record your compliance team’s verdict on a counterparty withPUT /v3/travel-rule/vasps/{vasp_id}/decision/:
- The decision is private to your application: another customer never sees it, and it never changes the shared entry.
- A decision changes nothing on its own and never changes how a transfer is routed. It reaches your transactions through the
counterparty_dd_statusrule field: install the presets, or write your own rule on it. - A decision on a brand covers each of its entities that has no decision of its own, and the entity shows the brand in
inherited_from. A decision on the entity overrides the brand’s for that entity. GETreturns the decision that applies, or{"decision": null}.DELETEclears your own decision and returns what applies afterwards: the brand’s decision for an entity, otherwisenull. It answers404when your application has no decision of its own on the counterparty.reasonis optional, up to 255 characters, and eachPUTreplaces it.decided_byis the console user who made or last changed the decision, and is empty when an API key did.
Counterparty screening
Counterparty screening checks the counterparty VASP itself, as a company, against sanctions and adverse media. It is opt-in and billed: turn it on withcounterparty_screening_enabled in your settings. While it is off, nothing is screened or billed.
- Billing. Each new screening costs your organization’s Company AML Screening price.
GET /v3/travel-rule/vasps/{vasp_id}/screening/returns thatprice,fresh_daysand whether screening isenabled, at no charge. A screening younger than 30 days is fresh:POSTreturns it again with200andoutcome: reusedand bills nothing, unless you sendrefresh: true. A new screening answers201withoutcome: screened. Sandbox applications are never screened or billed. - Automatic screening. With screening on, Didit also screens the counterparty an outbound exchange reaches when the exchange starts, before your rules evaluate the transaction, with the same reuse and billing. A refusal or failure there never fails the transfer.
- Result.
statusisApprovedwhen no possible match needs attention, andIn RevieworDeclinedfor a possible sanctions or adverse-media match.flaggedistruewhile a fresh screening isIn RevieworDeclined. - Effect. While your latest screening of a counterparty is flagged and screening stays on,
counterparty_risk_levelisHIGHin your rules, so thetravel-rule-counterparty-high-riskpreset, once installed, holds its transfers. The sharedrisk_levelin the directory does not change. - Privacy. The screening is private to your application: another customer never sees it, and it never changes the shared entry. The directory shows your latest one as
entity_screening. - Refusals.
409when screening is off, for a sandbox application, or while a screening of the same counterparty is already running.403withcode: insufficient_creditswhen your balance cannot cover the price.502when the screening could not be completed. None of them bills anything.
Due-diligence questionnaire
Ask a counterparty for its due-diligence answers with Didit’s standard questionnaire.POST /v3/travel-rule/vasps/{vasp_id}/questionnaires/ emails it to a compliance address on record for the counterparty, with a single-use link to an answer page:
- Where it goes.
sent_tomust be a compliance address on record: for a directory entry, its compliance address or a contact your application sees incontacts; for a Didit network member, its confirmed compliance email. Leave it out to use the address a Travel Rule email to the counterparty goes to: your own contact first, then a platform contact, then the entry’s compliance address. Didit never sends the questionnaire anywhere else, and answers400when the counterparty has no compliance address on record. - Answering. The link opens
/v3/travel-rule/questionnaires/{token}/, a page for a person at the counterparty that needs no account or API key. They answer every question, up to 2,000 characters each (N/Awhere one does not apply), and submit once. A counterparty can also post{"answers": {"<question id>": "<answer>"}}to the same link. A link that was used, replaced or is past its deadline shows an expiry page instead. The email and the page are translated; the questions themselves are always in English, so their meaning never changes between languages. - Deadlines. The counterparty has 30 days to answer (
expires_at). Answers stay current for 365 days (valid_until), andcurrentistrueuntil then. An unanswered questionnaire past its deadline, and answers past their validity, becomeEXPIRED. - One open questionnaire per counterparty. A new send replaces your earlier unanswered questionnaire to the same counterparty: it becomes
EXPIREDand its link stops working. One email per minute can go to an address; a faster send answers429. - Reading answers.
GET /v3/travel-rule/vasps/{vasp_id}/questionnaires/lists your questionnaires with the counterparty, newest first, withanswerskeyed by question id once answered. The directory shows them asquestionnaires. They are private to your application.
1 asks these twelve questions:
Transfers with one counterparty
GET /v3/travel-rule/transfers/?counterparty={vasp_id} lists your application’s Travel Rule transfers with one counterparty, newest first, by the id the directory shows: your transfers whose counterparty is that directory entry, or your INTERNAL exchanges with that Didit network member. A value that is not a valid id answers 400.
Sunrise and non-responsive policies
When a transfer expires (confirmation_timeout_hours passed while it was AWAITING_COUNTERPARTY), timeout_outcome decides what happens to its transaction. Two settings let you treat two common reasons for a timeout differently:
HOLD, REJECT or PROCEED, with the same meaning as for timeout_outcome, or an empty string to follow timeout_outcome. Both are empty by default, so nothing changes until you set one:
sunrise_policy, when it is set and the counterparty’s jurisdiction is insunrise_jurisdictions.non_responsive_policy, when it is set and the counterparty isUNRESPONSIVE.timeout_outcomein every other case, including a transfer to a Didit network member or to a counterparty with no directory entry.
sunrise_jurisdictions is read-only and currently ["CO", "KH", "VN"]. Only these countries count as in a sunrise period: any other jurisdiction, and an empty or unknown one, follows timeout_outcome, so a counterparty never gets your sunrise policy by accident. The expired transfer’s timeline names the outcome that applied, for example sunrise_policy=PROCEED (counterparty jurisdiction CO does not enforce the Travel Rule yet). Your rules still run after the expiry, so a rule such as the EU TFR tr_eu_expired_review can still move the transaction to review under PROCEED.
Rule fields and presets
Four rule fields carry what your application knows about the counterparty a transaction’s Travel Rule exchange reached: its directory entry, or for anINTERNAL exchange the Didit customer that received it. Each field is empty when the exchange reached no known counterparty.
travel_rule bundle act on them. Install them from Transactions → Rules → Library in the Business Console or with the rule library API:
Webhooks
Subscribe a destination totravel_rule.status.updated to get notified on every status transition of every transfer in your application, outbound and inbound.
rail is empty until a rail is selected.
The first events for a new transfer are queued while Didit processes the request, before transaction.created for the same transaction, but delivery order is not guaranteed; key your handler on transaction_id rather than on arrival order.
A transfer sent over GTR, TRUST, or VERIFYVASP gets no COMPLETED event today: unless the send fails or you cancel it, its last event is EXPIRED.
Sandbox applications send no webhooks.
See the webhooks guide for the full event catalog, signature verification, and retry policy.
Pricing
Every outboundtravelRule transaction is billed $0.02, charged once at creation regardless of how many status transitions the exchange goes through afterward.
The fee applies whatever happens to the exchange: it is also charged when the amount is below your threshold_amount, when Travel Rule is disabled and the data is only recorded, and when no rail can reach the counterparty.
Sandbox applications are never charged.
- Inbound
travelRuletransactions carry no Travel Rule or monitoring fee: the inbound leg Didit creates on the beneficiary’s application via theINTERNALrail, inboundTRPinquiries, and after-deposit registrations. - 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 Wallet Verification & Screening 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.
- Counterparty screening is opt-in and billed separately, at the Company AML Screening price per screening; a screening younger than 30 days is reused at no charge. Decisions, questionnaires and the directory’s due-diligence assessment cost nothing.
Attribution pricing
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 Wallet Verification & Screening 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 Wallet Verification & Screening 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_attributiontrigger 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.
EU TFR rules bundle
Four preset rules ship in thetravel_rule_eu_tfr bundle, purpose-built for EU TFR obligations.
Like every other preset bundle, enable it from the Business Console under Transactions → Rules → Library.
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 at hand. Each transfer’s subject is linked by
vendor_datato the KYC profile Didit verified at onboarding (document OCR, liveness, face match), so you can buildoriginator_datafrom verified fields on the same platform. Didit transmits exactly theoriginator_datayou send. - Step-up biometrics for risky transfers. A rule 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.
- 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, no volume tiers, no sales call.
FAQ
How the exchange works
Does Didit run the VASP-to-VASP exchange itself, or does it only record Travel Rule data after another network completes the handshake?
Does Didit run the VASP-to-VASP exchange itself, or does it only record Travel Rule data after another network completes the handshake?
travelRule transfer, Didit performs live counterparty discovery, sends the originator and beneficiary data to the receiving VASP on the rails that carry it, and marks the transfer COMPLETED only once the counterparty side confirms it - it is a managed handshake, not a post-settlement log.
Today a counterparty answer is processed on the INTERNAL, TRP, SYGNA, and EMAIL rails; GTR, TRUST, and VERIFYVASP only send, so transfers over them expire instead of completing.
Didit also keeps the full record and compliance status of every exchange (a 13-state lifecycle, 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 runs once you set is_enabled: true in settings (it is false by default), and until then Didit records the data passively.Is the exchange native to Didit, or does it depend on an external interoperability provider (Notabene, Sygna, VerifyVASP, TRP)?
Is the exchange native to Didit, or does it depend on an external interoperability provider (Notabene, Sygna, VerifyVASP, TRP)?
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.
Today every network rail needs your own membership, and GTR, TRUST, and VerifyVASP only send: see Overview.Which Travel Rule networks and protocols does Didit support?
Which Travel Rule networks and protocols does Didit support?
INTERNAL (both VASPs on Didit), TRP (OpenVASP Travel Rule Protocol), GTR, TRUST, VerifyVASP, Sygna Bridge, and an EMAIL fallback for counterparties with no tooling.
The network rails need your own membership today, and GTR, TRUST, and VerifyVASP only send for now.
CODE, TRISA, Notabene, and Veriscope appear in Didit’s Marketplace as request-access tiles, with no integration yet.
See How Didit routes an exchange for the full routing table.Which network should I use, and do I need my own memberships?
Which network should I use, and do I need my own memberships?
TRP is the baseline we recommend: it needs no shared membership - sharing your travel address lets any VASP that speaks TRP reach you - and when a counterparty is known only by wallet address, blockchain-analytics attribution routes it over TRP only if the attributed directory entry already has a TRP endpoint.
Outbound, Didit sends BTC and ETH transfers over it.
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.
Yes, today you need your own membership: Didit holds no platform membership on these networks yet and is onboarding as a provider.
Connect your membership in Marketplace; you are not locked to a single network.
Remember that GTR, TRUST, and VerifyVASP only send today, so a transfer over them does not complete.How does Didit find the counterparty VASP when I only have a wallet address?
How does Didit find the counterparty VASP when I only have a wallet address?
INTERNAL rail, your own for a stored travel address), then the protocol and network rails, then 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 and catalogues it in the VASP directory.
The transfer then routes automatically only if that directory entry already has a route, such as a TRP endpoint; otherwise it ends in COUNTERPARTY_VASP_NOT_FOUND with the counterparty’s name recorded, and you can resend once a route exists, for example after you store the counterparty’s travel_address on your address book entry for that wallet (BTC and ETH).Pricing and licensing
How much does Travel Rule cost, and is it included in my KYC or verification plan?
How much does Travel Rule cost, and is it included in my KYC or verification plan?
travelRule transaction, including transfers below your threshold; inbound transfers, such as the legs Didit mints on the beneficiary side, are not charged it.Will I be double-charged when Travel Rule and crypto monitoring both run?
Will I be double-charged when Travel Rule and crypto monitoring both run?
travelRule transactions, and the blockchain-analytics lookup is charged once: if crypto monitoring already screened the destination wallet on the transaction, attribution reuses that stored result at no extra cost.
Inbound legs Didit mints on the beneficiary side are never billed the Travel Rule fee.Can Didit provide documentation or a capability statement for my VASP licensing or regulator file?
Can Didit provide documentation or a capability statement for my VASP licensing or regulator file?
Operations and data
What happens to transfers below my threshold?
What happens to transfers below my threshold?
NOT_APPLICABLE: no rail is tried and no identity data leaves Didit.
An outbound transfer is still billed the $0.02 Travel Rule fee at creation.
Didit compares the threshold with the amount converted to your application’s default currency.
If Didit cannot convert the amount, the threshold is not applied and the transfer is exchanged.
The EU TFR has no minimum, so EU CASPs should keep threshold_amount at 0.00.What if the counterparty never responds?
What if the counterparty never responds?
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.My counterparty has no Travel Rule tooling at all. Can I still comply?
My counterparty has no Travel Rule tooling at all. Can I still comply?
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.How do withdrawals to the user's own wallet work?
How do withdrawals to the user's own wallet work?
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. That is the EU TFR self-hosted wallet obligation in API form.Is counterparty PII protected during the exchange?
Is counterparty PII protected during the exchange?
GTR, TRUST, VERIFYVASP, SYGNA) additionally encrypt the IVMS payload to the receiving VASP’s public key.I already track Travel Rule compliance with my own process. Can I just record it?
I already track Travel Rule compliance with my own process. Can I just record it?
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.How do I test this before going live?
How do I test this before going live?
AWAITING_COUNTERPARTY instead of completing, then expires after its confirmation deadline and applies your timeout_outcome: use it to check that a transfer is created with the payload you expect.
Inbound registration (POST /v3/travel-rule/inbound/) does not dispatch in the first place, so sandbox runs it for real against your wallet address book and you can exercise every outcome end to end - ON_HOLD, UNCONFIRMED_OWNERSHIP, COUNTERPARTY_MISMATCHED_DATA, or COMPLETED then FINISHED.
For a full two-sided outbound loop, run it between two live applications on the INTERNAL rail: register a wallet in the second application’s address book and watch both sides transition.