Skip to main content

How it works

Travel Rule outcomes in sandbox are driven by two layers:
  1. Wallet screening (synchronous, at create time): resolves a destination to one of four categories — VASP-attributed, self-hosted, elevated risk, or sanctions match. Magic-suffix encoded.
  2. Counterparty webhook (asynchronous, post-create): the VASP counterparty’s terminal decision — acknowledged, approved, rejected, declined. Either auto-pilot fires after 10s based on the suffix, or you call the simulate endpoint to override.

Wallet screening scenario catalog

Address format. EVM addresses must be all-lowercase OR a correctly EIP-55 checksummed mixed-case form. The mnemonic suffixes called out in this page are uppercase for readability; the wire-format addresses you send to the API are all-lowercase.

Counterparty-outcome scenario catalog (VASP-attributed only)

These suffixes route through the Travel Rule flow and auto-fire the encoded counterparty-webhook outcome 10 seconds after the payout is created. Pre-emption via payouts/:id/simulate/counterparty-webhook cancels the pending auto-pilot job. There is a sub-second race window if the auto-pilot has already started; in that case both signals may be processed in arrival order. For non-custodial payouts, the Travel Rule transfer is created before the customer is asked to sign (during compliance/Travel Rule screening), so it exists throughout the signing window up to broadcast. An auto-pilot rejected / declined arriving any time in that pre-broadcast window terminates the payout before the on-chain broadcast happens. For custodial payouts that broadcast inline, the auto-pilot fires after broadcast.

Pre- vs post-broadcast

Pre-broadcast: counterparty REJECTED / DECLINED cancels and terminalizes the transaction (custodial and non-custodial). Post-broadcast in sandbox: the transaction terminalizes with failureCode: travel_rule_rejected; the counterparty reason surfaces on failureMessage within about 5 seconds. Post-broadcast in production: audit-only; a confirmed chain transfer cannot be unwound (FATF Rec. 16).
To guarantee a pre-broadcast terminal rejection: use a non-custodial wallet and call payouts/:id/simulate/counterparty-webhook { outcome: "rejected", reason } once the Travel Rule row exists. Travel Rule runs before signing, so the row appears at the pre-signing Travel Rule step — wait for transaction.processing (which fires right after) before calling; a call made earlier during compliance screening returns 404 (no row yet). A rejection is deterministically terminal through the whole queue + signing window (up to and including collect-signatures), which is where you drive this scenario. (There is a narrow window during the final co-stamp step where a late rejection instead parks the payout for operator review rather than terminating cleanly — not reachable from this sandbox scenario, which fires pre-signing.) Or use the suffix BAD7E517 (TR_TX_VALIDATE_REJECTED), which is unconditionally pre-broadcast.

How to guarantee a pre-broadcast terminal rejection

A counterparty rejection arrives pre-broadcast as long as the payout has not yet broadcast on-chain. On the non-custodial flow Travel Rule runs before signing, so the Travel Rule row exists throughout the signing window and right up to broadcast — a reject stays pre-broadcast until the payout broadcasts, after which it becomes audit-only. Two reliable approaches:
  • Non-custodial wallet, manual counterparty call: use a non-custodial wallet so the payout pauses for cosign after clearing the Travel Rule step. The Travel Rule row is created before the customer is asked to sign, so call payouts/:id/simulate/counterparty-webhook { outcome: "rejected", reason: "..." } anytime before the payout broadcasts — including while it is still awaiting signatures. Within that pre-broadcast window the transaction terminates with failureCode: travel_rule_rejected and the supplied reason on failureMessage.
  • Unconditional pre-broadcast suffix: use destination suffix BAD7E517 (TR_TX_VALIDATE_REJECTED). This is unconditionally pre-broadcast; no manual call is needed.
For both flows, the optional reason propagates 1:1 to the DB failure_message, the polled GET /v2/transactions/:id failureMessage, and the transaction.failed webhook failureMessage. See the failureMessage symmetry contract.

Override the auto-pilot

To inject a counterparty webhook synchronously (before the 10s timer), call:
The synchronous call cancels the pending auto-pilot job and records the counterparty outcome on the payout’s Travel Rule row. For rejected / declined: see the pre- vs post-broadcast carve-out above; in sandbox, post-broadcast still terminates the transaction with failureCode: travel_rule_rejected and the supplied reason on failureMessage. For acknowledged / approved: always informational; the payout proceeds independently of counterparty resolution. Subsequent simulate calls are accepted; an outcome that would regress the row’s state is ignored.
reason propagation. The reason you supply propagates 1:1 to the DB failure_message, the polled GET /v2/transactions/:id failureMessage, AND the transaction.failed webhook failureMessage. QA tooling can correlate the simulator call with the resulting webhook by the reason text.

Example: VASP destination + REJECTED counterparty

amount is a canonical decimal string with exactly asset.precision digits after . (USDC has 6 decimals → "10.000000" = 10 USDC). Lifecycle (custodial payout — broadcasts inline):
  1. Wallet screening resolves VASP-attributed.
  2. Synthetic Travel Rule transfer row written; auto-pilot job queued.
  3. The payout broadcasts on-chain (mocked) and receives a synthetic txHash.
  4. 10s after create, the synthetic counterparty webhook fires with outcome: rejected. Broadcast already happened — the Travel Rule row records REJECTED for audit, the payout is not unwound (tipping-off-safe).
  5. Call payouts/:id/simulate/confirm with a synthetic txHash to advance the payout to its terminal state.
Lifecycle (non-custodial payout — screened before signing):
  1. Wallet screening resolves VASP-attributed; the synthetic Travel Rule transfer row is written and the auto-pilot job is queued — all before the customer is asked to sign.
  2. The payout reports requiresUserSignature: true and waits while the customer’s authorized signers approve it. The Travel Rule row already exists, so a counterparty rejection can land during this window.
  3. The synthetic counterparty webhook fires with outcome: rejected. Broadcast has not yet happened; the payout terminates with transaction.failed carrying failureCode: travel_rule_rejected. The funds reserved for the payout are returned; the upstream counterparty transfer is best-effort cancelled.

Errors

See also