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

SuffixScreening resolutionOutcome
5A50AB1EVASP-attributedRoutes through Travel Rule; Travel Rule row persisted in SENT state; no auto-pilot counterparty webhook — drive the counterparty leg yourself via payouts/:id/simulate/counterparty-webhook.
5E1F0577Self-hosted walletNo Travel Rule transfer; proceeds to broadcast
12517C00Elevated risk (wallet screening)Routes self-hosted; the score sits below the rejection threshold so the payout proceeds. The elevated-risk classification is recorded for audit.
5A4070EDSanctions match (wallet screening)Payout terminates as failed; transaction.failed fires with failureCode: AML_REJECTED (sanctions classification recorded for audit)
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.
SuffixAuto-pilot counterparty outcomeFinal transfer stateEffect on payout
AC6BC0DEacknowledged only (no terminal)ACKInformational — payout proceeds.
ACCEEDEDapprovedACCEPTEDInformational — payout proceeds (counterparty resolution does not gate broadcast).
BAD6A1A4rejectedREJECTEDSee pre- vs post-broadcast carve-out below.
DEC11A1DdeclinedDECLINEDSame as BAD6A1A4; see pre- vs post-broadcast carve-out below.
DA171465WAITING_FOR_INFORMATION at /tx/create, then auto-pilot approvedRow starts at WAITING_FOR_INFORMATION (the payout broadcasts in parallel), then advances to ACCEPTED via the auto-pilot webhook. The broadcast leg never blocks on counterparty resolution.
For non-custodial payouts that park at await-user-cosign waiting for the customer’s signature, the auto-pilot rejected / declined arriving during that wait 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 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 (the cosign gate parks the request), call payouts/:id/simulate/counterparty-webhook { outcome: "rejected", reason } BEFORE payouts/:id/simulate/cosign { outcome: "approved" }. Or use the suffix BAD7E517 (TR_TX_VALIDATE_REJECTED), which is unconditionally pre-broadcast.
SuffixPre-broadcast outcome
BAD7E517Travel Rule validation rejection → payout fails before broadcast
5A4ED0DDTravel Rule row is persisted in a non-sendable state after /tx/create → payout fails pre-broadcast with TRAVEL_RULE_REJECTED
503CA110Travel Rule provider returns a retryable unavailable error; retries exhaust and the payout parks in status: processing
D15CCAD0Travel Rule discrepancy: customer attests self-custody but wallet screening identifies a VASP — payout fails with failureCode: TRAVEL_RULE_REJECTED

How to guarantee a pre-broadcast terminal rejection

A counterparty rejection arrives pre-broadcast only when the cosign gate is still open. Two reliable approaches:
  • Non-custodial wallet, manual counterparty call: use a non-custodial wallet so the payout parks at the cosign gate. Call payouts/:id/simulate/counterparty-webhook { outcome: "rejected", reason: "..." } BEFORE calling payouts/:id/simulate/cosign. 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:
curl -X POST https://api.sandbox.conduit.financial/v2/sandbox/payouts/$PAYOUT_ID/simulate/counterparty-webhook \
  -H "x-api-key: $SANDBOX_API_KEY" \
  -H "idempotency-key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "outcome": "rejected", "reason": "explicit pre-empt" }'
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

curl -X POST https://api.sandbox.conduit.financial/v2/payouts \
  -H "x-api-key: $SANDBOX_API_KEY" \
  -H "idempotency-key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "cus_01H...",
    "assetAmount": {
      "code": "USDC",
      "chain": "ethereum",
      "amount": "10.000000"
    },
    "purpose": "TREASURY_MANAGEMENT",
    "documents": ["$DOC_ID"],
    "destination": {
      "type": "crypto",
      "recipient": {
        "rail": "CRYPTO",
        "chain": "ETHEREUM",
        "address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabad6a1a4",
        "attestation": { "custody": "third_party" },
        "type": "INDIVIDUAL",
        "firstName": "Counterparty",
        "lastName": "Recipient",
        "countryOfCitizenship": "USA"
      }
    }
  }'
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 — parks at await-user-cosign):
  1. Wallet screening resolves VASP-attributed.
  2. Synthetic Travel Rule transfer row written; auto-pilot job queued.
  3. The payout parks at the cosign step waiting for the customer signature.
  4. 10s after create, 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 ledger reservation is released; the upstream VASP transfer is best-effort cancelled.

Errors

StatusReason
404simulate/counterparty-webhook called on a payout with no Travel Rule row (self-hosted wallet resolution or the payout hasn’t reached the Travel Rule step yet)

See also