> ## Documentation Index
> Fetch the complete documentation index at: https://docs.conduit.financial/llms.txt
> Use this file to discover all available pages before exploring further.

# Travel Rule scenarios

> Force Travel Rule resolution and counterparty webhook outcomes

## 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): the VASP counterparty's terminal decision -- `acknowledged`, `approved`, `rejected`, `declined`. The sandbox automatically delivers the suffix-encoded outcome 10 seconds after the Travel Rule transfer is created, unless you call the simulate endpoint first to override it.

## Wallet screening scenario catalog

| Suffix | Screening resolution | Outcome |
| - | - | - |
| `5A50AB1E` | VASP-attributed | Routes through Travel Rule; no automatic counterparty outcome fires for this suffix -- drive the counterparty webhook 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) | Held for compliance review (`status: pending`); `simulate/compliance-decision` `reject` → `transaction.failed` with `compliance_review_rejected`. |

<Warning>
  **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.
</Warning>

## Counterparty-outcome scenario catalog (VASP-attributed only)

These suffixes route through the Travel Rule flow. Unless you call the simulate endpoint first, the sandbox automatically delivers the suffix-encoded counterparty outcome 10 seconds after the Travel Rule transfer is created.

Calling `payouts/:id/simulate/counterparty-webhook` before that 10-second window elapses overrides the automatic outcome. A call that arrives after the automatic outcome has already been recorded does not change it.

| Suffix | Counterparty outcome | Effect on payout |
| - | - | - |
| `AC6BC0DE` | `acknowledged` only (not terminal) | Informational — payout proceeds. |
| `ACCEEDED` | `approved` | Informational — payout proceeds (counterparty resolution does not gate broadcast). |
| `BAD6A1A4` | `rejected` | See [pre- vs post-broadcast carve-out](#pre--vs-post-broadcast) below. |
| `DEC11A1D` | `declined` | Same as `BAD6A1A4`; see [pre- vs post-broadcast carve-out](#pre--vs-post-broadcast) below. |
| `DA171465` | Counterparty first asks for more information, then resolves `approved` | The payout broadcasts without waiting on this resolution; it is never gated by counterparty resolution. |

The Travel Rule transfer is created after compliance and document review clear, just before the customer is asked to sign, so it exists throughout the signing window up to broadcast. A `rejected` / `declined` outcome received before signing begins terminates the payout immediately, before broadcast. One received while the payout is still collecting signatures is held; the sandbox checks it again once the signatures reach quorum, and if the rejection is still on record it terminates the payout there, before broadcast. Once the payout has moved past that point toward broadcast, a rejection has no effect.

### Pre- vs post-broadcast

**Pre-broadcast:** counterparty REJECTED / DECLINED cancels and terminalizes the transaction.

**Post-broadcast:** audit-only; the payout completes. A confirmed chain transfer cannot be unwound (FATF Rec. 16).

| Suffix | Pre-broadcast outcome |
| - | - |
| `BAD7E517` | Travel Rule validation rejection → payout fails before broadcast |
| `5A4ED0DD` | Travel Rule screening rejects the transfer after accepting it → payout fails pre-broadcast with `travel_rule_rejected` |
| `D15CCAD0` | Travel Rule discrepancy: customer attests the destination is their own wallet 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 as long as the payout has not yet broadcast on-chain. Travel Rule runs before signing, so the Travel Rule transfer 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:

* **Manual counterparty call:** call `payouts/:id/simulate/counterparty-webhook { outcome: "rejected", reason: "..." }` once the Travel Rule transfer exists — before the customer signs, or once the signatures reach quorum. A call made while the payout is still collecting signatures is held; the sandbox checks it again when quorum is reached. In both cases the transaction terminates with `failureCode: travel_rule_rejected`, as long as it has not already broadcast.
* **Unconditional pre-broadcast suffix:** use destination suffix `BAD7E517`. This is unconditionally pre-broadcast; no manual call is needed.

For both flows, the polled `GET /v2/transactions/:id` and the `transaction.failed` webhook carry the same `failureMessage`: the standard text for `travel_rule_rejected`, as in production. The optional `reason` does not appear in it. See the [`failureMessage` symmetry contract](/webhooks#failuremessage-symmetry-contract).

## Override the auto-pilot

To inject a counterparty webhook synchronously (before the 10s timer), call:

<CodeGroup>
  ```bash curl theme={null}
  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" }'
  ```

  ```javascript Node theme={null}
  const response = await fetch(
    `${process.env.SANDBOX_HOST}/v2/sandbox/payouts/${PAYOUT_ID}/simulate/counterparty-webhook`,
    {
      method: "POST",
      headers: {
        "x-api-key": process.env.SANDBOX_API_KEY,
        "idempotency-key": crypto.randomUUID(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ outcome: "rejected", reason: "explicit pre-empt" }),
    }
  );
  ```

  ```python Python theme={null}
  import httpx, uuid, os

  httpx.post(
  f"{os.environ['SANDBOX_HOST']}/v2/sandbox/payouts/{PAYOUT_ID}/simulate/counterparty-webhook",
  headers={
  "x-api-key": os.environ["SANDBOX_API_KEY"],
  "idempotency-key": str(uuid.uuid4()),
  },
  json={"outcome": "rejected", "reason": "explicit pre-empt"},
  )

  ```
</CodeGroup>

The synchronous call overrides the pending automatic outcome and records the counterparty outcome on the payout's Travel Rule transfer. For `rejected` / `declined`: see the [pre- vs post-broadcast carve-out](#pre--vs-post-broadcast) above — post-broadcast, the call is audit-only and the payout completes. For `acknowledged` / `approved`: always informational; the payout proceeds independently of counterparty resolution. A later call that would regress an already-recorded outcome is ignored.

<Note>
  **`reason` stays internal.** The `reason` you supply is recorded for Conduit operators. The polled `GET /v2/transactions/:id` and the `transaction.failed` webhook carry the standard `failureMessage` for `travel_rule_rejected`, as in production. Correlate the simulator call with the resulting webhook by `transactionId`.
</Note>

## Example: VASP destination + REJECTED counterparty

<CodeGroup>
  ```bash curl theme={null}
  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": {
          "chain": "ethereum",
          "address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabad6a1a4",
          "attestation": { "custody": "third_party" },
          "type": "individual",
          "firstName": "Counterparty",
          "lastName": "Recipient",
          "countryOfCitizenship": "USA"
        }
      }
    }'
  ```

  ```javascript Node theme={null}
  const response = await fetch(`${process.env.SANDBOX_HOST}/v2/payouts`, {
    method: "POST",
    headers: {
      "x-api-key": process.env.SANDBOX_API_KEY,
      "idempotency-key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      customerId: "cus_01H...",
      assetAmount: {
        code: "USDC",
        chain: "ethereum",
        amount: "10.000000",
      },
      purpose: "treasury_management",
      documents: [docId],
      destination: {
        type: "crypto",
        recipient: {
          chain: "ethereum",
          address: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabad6a1a4",
          attestation: { custody: "third_party" },
          type: "individual",
          firstName: "Counterparty",
          lastName: "Recipient",
          countryOfCitizenship: "USA",
        },
      },
    }),
  });
  const payout = await response.json();
  // payout.id → "txn_..."
  ```

  ```python Python theme={null}
  import httpx, uuid, os

  r = httpx.post(
      f"{os.environ['SANDBOX_HOST']}/v2/payouts",
      headers={
          "x-api-key": os.environ["SANDBOX_API_KEY"],
          "idempotency-key": str(uuid.uuid4()),
      },
      json={
          "customerId": "cus_01H...",
          "assetAmount": {
              "code": "USDC",
              "chain": "ethereum",
              "amount": "10.000000",
          },
          "purpose": "treasury_management",
          "documents": [doc_id],
          "destination": {
              "type": "crypto",
              "recipient": {
                  "chain": "ethereum",
                  "address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabad6a1a4",
                  "attestation": {"custody": "third_party"},
                  "type": "individual",
                  "firstName": "Counterparty",
                  "lastName": "Recipient",
                  "countryOfCitizenship": "USA",
              },
          },
      },
  )
  payout = r.json()
  # payout["id"] → "txn_..."
  ```
</CodeGroup>

`amount` is a canonical decimal string with exactly `asset.precision` digits after `.` (USDC has 6 decimals → `"10.000000"` = 10 USDC).

Lifecycle:

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; Conduit also notifies the counterparty VASP of the cancellation, on a best-effort basis.

## 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

* [Sandbox overview](/sandbox/overview)
* [Webhooks reference](/webhooks)
* [Error codes](/errors)
* [Withdrawals](/sandbox/withdrawals)
* [Withdrawal failure paths](/sandbox/withdrawals#failure-paths)
* [Non-custodial wallets](/concepts/non-custodial-wallets)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.