How it works
Travel Rule outcomes in sandbox are driven by two layers:
- 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.
- 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
| Suffix | Screening resolution | Outcome |
|---|
5A50AB1E | VASP-attributed | Routes 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. |
5E1F0577 | Self-hosted wallet | No Travel Rule transfer; proceeds to broadcast |
12517C00 | Elevated 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. |
5A4070ED | Sanctions 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.
| Suffix | Auto-pilot counterparty outcome | Final transfer state | Effect on payout |
|---|
AC6BC0DE | acknowledged only (no terminal) | ACK | Informational — payout proceeds. |
ACCEEDED | approved | ACCEPTED | Informational — payout proceeds (counterparty resolution does not gate broadcast). |
BAD6A1A4 | rejected | REJECTED | See pre- vs post-broadcast carve-out below. |
DEC11A1D | declined | DECLINED | Same as BAD6A1A4; see pre- vs post-broadcast carve-out below. |
DA171465 | WAITING_FOR_INFORMATION at /tx/create, then auto-pilot approved | Row 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.
| Suffix | Pre-broadcast outcome |
|---|
BAD7E517 | Travel Rule validation rejection → payout fails before broadcast |
5A4ED0DD | Travel Rule row is persisted in a non-sendable state after /tx/create → payout fails pre-broadcast with TRAVEL_RULE_REJECTED |
503CA110 | Travel Rule provider returns a retryable unavailable error; retries exhaust and the payout parks in status: processing |
D15CCAD0 | Travel 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):
- Wallet screening resolves VASP-attributed.
- Synthetic Travel Rule transfer row written; auto-pilot job queued.
- The payout broadcasts on-chain (mocked) and receives a synthetic
txHash.
- 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).
- 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):
- Wallet screening resolves VASP-attributed.
- Synthetic Travel Rule transfer row written; auto-pilot job queued.
- The payout parks at the cosign step waiting for the customer signature.
- 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
| Status | Reason |
|---|
404 | simulate/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