Platform-hosted Remediation

Build your own Travel Rule information recovery experience using zerohash APIs, with full control over the UI and the end-to-end flow.

Overview

On this path zerohash does not contact your end customer at all. Your Platform is notified that information is missing, collects it in your own UI, and posts it to zerohash. You own the wording, the design, the error states and the reminders. That control is the reason to choose it, and the cost is that your Platform has to encode which fields are required. The requirements vary by wallet type, by who owns the wallet and by the amount moved in the last 12 hours, and they will change as the regulation develops. The tables under Travel Rule requirements below are the reference for that logic.

Requirements

What has to be collected varies by wallet type, by who owns the wallet, and by the amount moved in the last 12 hours. The two tables below are the reference for what is required.

Deposits: Sending Wallet

RequirementExchange walletSelf-custody wallet
What is the wallet type?
Determines whether the sending wallet is hosted by an exchange or self-custodied by the sender.
Name of the exchange, submitted as its casp_did.Deposit source address.
Who owns the sending wallet?Confirms whose identity information is required to release a deposit sent from a self-custody wallet. Not required when the sending wallet is hosted by an exchange.Not required.

If the end user owns the wallet, confirm their KYC or KYB data. If someone else owns it, collect that person or entity's data.

Individuals: full name, legal address, date of birth, place of birth.

Businesses: business name, business legal address, LEI.

Proof of ownership
Verifies control before releasing funds.
Not required.Required only when more than €1,000 has been received from the wallet in the last 12 hours. The end user uploads a screenshot of the source wallet showing the address in the image.

Withdrawals: Receiving Wallet

Withdrawals need less identity information than deposits: a full name for individuals, or a business name and LEI for businesses. Deposits from self-custody wallets additionally need a legal address, date of birth and place of birth.

RequirementExchange walletSelf-custody wallet

What is the wallet type?

Determines whether the receiving wallet is hosted by an exchange or self-custodied by the recipient.

Name of the exchange, submitted as its casp_did.Receiving wallet address.

Who owns the receiving wallet?

Confirms whose identity information is required to initiate a withdrawal. Required for both self-custody and exchange wallets.

Individuals: full name.


Businesses: business name, LEI.

Individuals: full name.


Businesses: business name, LEI.

Proof of ownership

Verifies control before sending funds.

Not required.Required only when more than €1,000 has been sent to the wallet in the last 12 hours. The end user uploads a screenshot of the receiving wallet showing the address in the image.

Withdrawals need less identity information than deposits: a full name for individuals, or a business name and LEI for businesses. Deposits from self-custody wallets additionally need a legal address, date of birth and place of birth.

Deposits

How it works

  1. The customer sends crypto to zerohash.
  2. zerohash checks whether the sending wallet's Travel Rule information is available and meets TFR requirements, against Notabene, TRUST and the zerohash allow list.
    1. If it passes, the deposit is credited and converted to fiat where applicable.
    2. If it fails, the deposit is held in collateral, the 48-hour timer begins, and your Platform receives a webhook indicating a deposit was received missing travel rule information.
  3. Platform notifies the customer and collects the missing information in your own UI.
  4. Your Platform sends it to zerohash with POST /travel-rule/submit, referencing the deposit_id.
  5. On POST /travel-rule/submit, or at the end of the 48-hour window, zerohash re-checks whether sufficient TFR data is available for the sending wallet.
    1. If it is, the deposit is credited and your Platform receives a webhook indicated travel rule information was received and the deposit was processed.
    2. If it is not, the deposit is credited to a quarantine account for recovery and your Platform receives a webhook notification.

Submit as soon as you have the information rather than batching at the end of the window. The re-check happens when the timer expires, and a submission that lands after it does not release the deposit.

Avoiding the hold entirely

Self-custody wallets can be whitelisted with their Travel Rule information at customer onboarding. A deposit from an address that is on the internal allow list with sufficient information does not undergo additional Travel Rule screening, so it never enters remediation.

Only self-custody (unhosted) wallets are eligible. Exchange-hosted wallets cannot be whitelisted, for two reasons: exchanges reuse deposit addresses across customers, so a whitelisted address would not reliably identify one end customer, and exchange-originated deposits should not need whitelisting anyway because their Travel Rule information is exchanged automatically through Notabene or TRUST once the deposit arrives.

The E2E sequence is:

  1. Create the customer and their zerohash participant code via POST /participants/customers/new
  2. Submit the sending wallet's information with POST /travel-rule/submit, referencing that participant code, and leaving deposit_id empty.

Webhooks

Deposit received missing travel rule information

A deposit is held in collateral because Travel Rule information is missing. The 48-hour timer starts here.

{
      "account_id": "da885ef0-49f0-5cfd-adcf-08488b8d04b3",
      "amount": "500",
      "asset": "ETH",
      "deposit_id": "3af60e44-be66-4c7a-ab07-e324ec1760e3",
      "participant_code": "G5JD0S",
      "platform_code": "MERCH1",
      "received_address": "0xdff7a4d40869F420fC21e04f02d8597A70c11726",
      "source_address": "0xe3940dFC61e7E097b72c66892aC01aBa0Ae6a391",
      "state": "pending_compliance_review",
      "state_reason": "required_travel_rule_info",
      "timestamp": 1790871269101,
      "transaction_hash": "0xb863467b6eade708f804ddf64ecc8ef25b01fb2824fa8abad046dfda14c00b8b"
}

Travel rule information submitted & received

Travel rule information has been submitted through POST /travel-rule/submit

{
      "account_id": "b970f560-8d28-57cf-a750-b2f1db6b1c44",
      "amount": "500",
      "asset": "ETH",
      "deposit_id": "3af60e44-be66-4c7a-ab07-e324ec1760e3",
      "participant_code": "G5JD0S",
      "platform_code": "MERCH1",
      "received_address": "0xdff7a4d40869F420fC21e04f02d8597A70c11726",
      "source_address": "0xe3940dFC61e7E097b72c66892aC01aBa0Ae6a391",
      "state": "completed",
      "state_reason": "travel_rule_info_received",
      "timestamp": 1790871429030,
      "transaction_hash": "0xb863467b6eade708f804ddf64ecc8ef25b01fb2824fa8abad046dfda14c00b8b"
}

Travel rule information verified

Travel rule information has been verified by zerohash

{
      "account_group": "YTEYJV",
      "account_label": "general",
      "account_type": "collateral",
      "asset": "ETH",
      "balance": "0",
      "movements": [
        {
          "account_id": "c21cdbd2-3d51-58cf-8728-da0cdb07fd90",
          "change": "-0.130000000000000000",
          "movement_id": "8f73cce0-d11b-458d-965a-ce0aa92d0a5f",
          "movement_timestamp": 1790873653576,
          "movement_type": "transfer",
          "parent_link_id": "a72d9ffa-ee0c-4cc3-bbe2-e6cec18f4b01",
          "transfer_request_id": "99909",
          "transfer_type": "travel_rule_deposit_info_verified"
        }
      ],
      "participant_code": "3JIAB3",
      "run_id": "146709",
      "run_type": "transfer",
      "timestamp": 1790873653630
    }

Travel rule review timeout

The 48-hour window closed without the customer providing sufficient travel rule information.


Travel rule review rejected

The information the customer supplied was rejected.


Withdrawals

There is no hold and no 48-hour window on withdrawals. A withdrawal cannot be initiated until the receiving wallet has sufficient Travel Rule information on file, so the information has to be in place first.

  1. Collect upfront, before initiating: This is the recommended approach. Before initiating a withdrawal of any kind via API, including Account Funding withdrawals, crypto withdrawals and payouts, collect Travel Rule information for the receiving wallet from the end user and send it with POST /travel-rule/submit. Check that withdrawal.status comes back as allowed before calling the withdrawal endpoint.
  2. Remediate after a failed initiation: If your Platform initiates a withdrawal and the response indicates that insufficient Travel Rule information is available for the receiving wallet, collect the information from the end user, send it with POST /travel-rule/submit, then attempt the withdrawal call again.
    Because the requirements vary by amount, a receiving wallet that was sufficient for one withdrawal can be insufficient for a larger one. Handle this response on every attempt, not only the first time a wallet is used.

POST /travel-rule/submit

Submits Travel Rule information for a wallet address, tied to one of your end customers. The same endpoint serves all three cases: whitelisting (self custody wallets) at onboarding, remediating a held deposit, and supplying receiving wallet information for a withdrawal.

FieldRequiredDescription
addressYesThe wallet address the information relates to: the deposit source address, or the receiving wallet address for a withdrawal.
participant_codeYesIdentifies the end customer the wallet is associated with.
address_owner_typeYesindividual or business. Determines which PII object applies.
wallet_typeYesself_custody or exchange.
casp_didConditionalRequired when wallet_type is exchange. The decentralised identifier of the exchange, for example did:example:kraken. Retrieve the supported list from GET /vasps.
participant_is_address_ownerNoSet this when the participant identified by participant_code owns the address. Use it for me-to-me transfers to avoid submitting individual_pii or business_pii; zerohash uses the participant's existing KYC or KYB record instead.
individual_piiConditionalIdentity of the individual who owns the address. Required when address_owner_type is individual and participant_is_address_owner is not set. Fields: first_name, last_name, address, date_of_birth, place_of_birth.
business_piiConditionalIdentity of the entity that owns the address. Required when address_owner_type is business and participant_is_address_owner is not set. Fields: legal_entity_name, legal_entity_identifier (LEI), legal_entity_address.
wallet_proof_of_ownershipConditionalSelf-custody wallets only, and only when more than €1,000 has moved to or from the wallet in the last 12 hours. Fields: document (base64-encoded screenshot showing the address), mime, file_name.
deposit_idNoThe deposit this submission relates to, where there is one. Not relevant for withdrawals or for whitelisting at onboarding.

Sample Requests: Travel Rule Information for Deposits


Example Request: Sending Wallet = Exchange, Sender = Individual

{
  "address": "0xABC123",
  "participant_code": "CUST01",
  "address_owner_type": "individual",
  "individual_pii": {
    "first_name": "Jane",
    "last_name": "Example",
    "address": "1 Example St, Dublin",
    "date_of_birth": "2000-01-01",
    "place_of_birth": "England"
  },
  "wallet_type": "exchange",
  "casp_did": "did:example:kraken",
  "deposit_id": "1234556879"
}

Example Request: Sending Wallet = Exchange, Sender = Business

{
  "address": "0xABC123",
  "participant_code": "MERCH1",
  "business_pii": { 
    "legal_entity_name": "Business XYZ",
    "legal_entity_identifier": "123456789",
    "legal_entity_address": "1 Example St, Dublin"
  },
  "wallet_type": "exchange",
  "casp_did": "did:example:kraken",
  "deposit_id": "1234556879"
}

Example Request: Sending Wallet = Self Custody, Sender = Individual, Proof of Ownership not required

 {

  "address": "0xABC123",
  "participant_code": "CUST01",
  "address_owner_type": "individual",
  "individual_pii": {
    "first_name": "Jane",
    "last_name": "Example",
    "address": "1 Example St, Dublin",
    "date_of_birth": "2000-01-01",
    "place_of_birth": "England"
  },
  "wallet_type": "self_custody",
  "deposit_id": "1234556879"
}

Example Request: Sending Wallet = Self Custody, Sender = Individual, Proof of Ownership required

  {

  "address": "0xABC123",
  "participant_code": "CUST01",
  "address_owner_type": "individual",
  "individual_pii": {
    "first_name": "Jane",
    "last_name": "Example",
    "address": "1 Example St, Dublin",
    "date_of_birth": "2000-01-01",
    "place_of_birth": "England"
  },
  "wallet_type": "self_custody",
  "wallet_proof_of_ownership": {
    "document": "aGVsbG8gd29ybGQ=",
    "mime": "png",
    "file_name": "proof_of_ownership.png"
  },
  "deposit_id": "1234556879"
}

Example Request: Sending Wallet = Self Custody, Sender = Business, Proof of Ownership not required

 {

  "address": "0xABC123",
  "participant_code": "MERCH1",
  "address_owner_type": "business",
  "business_pii": { 
    "legal_entity_name": "Business XYZ",
    "legal_entity_identifier": "123456789",
    "legal_entity_address": "1 Example St, Dublin"
  },
  "wallet_type": "self_custody",
  "deposit_id": "1234556879"
}

Example Request: Sending Wallet = Self Custody, Sender = Business, Proof of Ownership required

  {

  "address": "0xABC123",
  "participant_code": "MERCH1",
  "address_owner_type": "business",
  "business_pii": { 
    "legal_entity_name": "Business XYZ",
    "legal_entity_identifier": "123456789",
    "legal_entity_address": "1 Example St, Dublin"
  },
  "wallet_type": "self_custody",
  "wallet_proof_of_ownership": {
    "document": "aGVsbG8gd29ybGQ=",
    "mime": "png",
    "file_name": "proof_of_ownership.png"
  },
  "deposit_id": "1234556879"
}

Sample Requests: Travel Rule Information for Withdrawals


Example Request: Receiving Wallet = Exchange, Sender = Individual

{
  "address": "0xABC123",
  "participant_code": "CUST01",
  "address_owner_type": "individual",
  "wallet_type": "exchange",
  "casp_did": "did:example:kraken"
}

Example Request: Receiving Wallet = Exchange, Sender = Business

{
  "address": "0xABC123",
  "participant_code": "MERCH1",
  "wallet_type": "exchange",
  "casp_did": "did:example:kraken"
}

Example Request: Receiving Wallet = Self Custody, Sender = Individual, Proof of Ownership not required

 {

  "address": "0xABC123",
  "participant_code": "CUST01",
  "address_owner_type": "individual",
  "individual_pii": {
    "first_name": "Jane",
    "last_name": "Example"
  },
  "wallet_type": "self_custody"
}

Example Request: Receiving Wallet = Self Custody, Sender = Individual, Proof of Ownership required

  {

  "address": "0xABC123",
  "participant_code": "CUST01",
  "address_owner_type": "individual",
  "individual_pii": {
    "first_name": "Jane",
    "last_name": "Example"
  },
  "wallet_type": "self_custody",
  "wallet_proof_of_ownership": {
    "document": "aGVsbG8gd29ybGQ=",
    "mime": "png",
    "file_name": "proof_of_ownership.png"
  }
}

Example Request: Receiving Wallet = Self Custody, Sender = Business, Proof of Ownership not required

 {

  "address": "0xABC123",
  "participant_code": "MERCH1",
  "address_owner_type": "business",
  "business_pii": { 
    "legal_entity_name": "Business XYZ",
    "legal_entity_identifier": "123456789"
  },
  "wallet_type": "self_custody"
}

Example Request: Receiving Wallet = Self Custody, Sender = Business, Proof of Ownership required

  {

  "address": "0xABC123",
  "participant_code": "MERCH1",
  "address_owner_type": "business",
  "business_pii": { 
    "legal_entity_name": "Business XYZ",
    "legal_entity_identifier": "123456789"
  },
  "wallet_type": "self_custody",
  "wallet_proof_of_ownership": {
    "document": "aGVsbG8gd29ybGQ=",
    "mime": "png",
    "file_name": "proof_of_ownership.png"
  }
}

Sample Response

The response returns the resulting permission state for the address, and for self-custody wallets the verification state of any proof of ownership:

{
  "address": "0xABC123",
  "participant_code": "MERCH1",
  "address_owner_type": "business",
  "wallet_type": "exchange",
  "deposit": {
    "status": "not_allowed"
  },
  "withdrawal": {
    "status": "allowed"
  }
}

Travel Rule data for exchange wallets expires. Re-submit it after 24 hours, which in practice means submitting immediately before the transfer rather than storing it against the address indefinitely. This is the main behavioural difference between the two wallet types, and the reason exchange wallets cannot be whitelisted at onboarding.

This endpoint indicates whether a wallet address has sufficient travel rule information to allow a deposit from it or a planned withdrawal to it. The endpoint also indicates what information is missing to facilitate a deposit or withdrawal using that address.

Returns the CASPs zerohash supports, with the casp_did value to submit for each. Use it to populate the exchange selector in your collection UI rather than hardcoding identifiers, since the supported list changes as networks add participants.

  • A casp_did that is not on this list will be rejected when wallet_type is exchange.

Things to know

This is newly enacted regulation and the requirements are subject to change. On this path, each change lands in your release cycle: new fields, new thresholds and new conditional logic in your collection UI.

Platforms that would rather not own that can move to zerohash-hosted remediation without changing their deposit or withdrawal integration.

Travel Rule information will always need to be provided for self custody wallets.

Remediation is an edge case. When the sending CASP is TFR compliant, the required information reaches zerohash through Notabene or TRUST automatically and no remediation happens at all for deposits sent from CASPS.


Did this page help you?