# Simulate a Travel Rule Inbound Payment

`POST /v1/payments/simulate-travel-rule-inbound` has a Tesser-operated simulator VASP originate a real Notabene Travel Rule transfer into one of your managed wallets, so you can test your integration's handling of an inbound Travel Rule exchange without standing up a second VASP.

:::warning{title="This simulates the compliance exchange, not the money"}
This endpoint does not move money. No on-chain transfer is broadcast, no balance changes, and the payment's pending step stays `status: created` — it does not progress to `confirmed` or `completed`. If you need a credited balance to test against, use [`POST /v1/payments/simulate-inbound`](#simulate-travel-rule-inbound-vs-simulate-inbound) instead. Calling this endpoint and expecting your balance to change is the most common way to misread it.
:::

Everything about the compliance decision itself is genuine: a real Notabene transfer is created, real PII is exchanged, and your organization's own name-matching and authorization logic decides the outcome — the same code path a real counterparty-initiated inbound runs through. Only the counterparty and the settlement that would have followed it are simulated.

## Prerequisites

- **Sandbox or staging only.** The endpoint is unavailable in production, gated the same way as `simulate-inbound`. Calling it in production returns `payments-3053`.
- **The `notabene-travel-rule-inbound` feature flag must be enabled for your organization.** Without it, the webhooks that carry the compliance decision are received and dropped, so the call would otherwise hang until it times out — the endpoint checks the flag up front instead and fails fast with `payments-3083`. Contact Tesser support to have the flag enabled for your organization.
- **Your organization must have its Travel Rule VASP identity configured with Tesser.** This is set up during onboarding; if it is missing, the call fails with `payments-3085`.
- **`to_account` must be a managed `stablecoin_ethereum` wallet in your workspace.** Creating a managed wallet registers it with Notabene automatically — no separate registration step. An account that does not exist, isn't managed, or isn't a `stablecoin_ethereum` wallet fails the call (`payments-1002` / `payments-3055`).

You never see or supply any Notabene credentials — the simulator VASP's identity and your organization's own Travel Rule identity are both handled server-side.

See [Authentication](/overviews/authentication) for how to obtain the bearer token (`$CLIENT_TOKEN` below).

## Request

| Field | Required | Description |
| --- | --- | --- |
| `to_account` | Yes | The managed wallet account ID the simulated counterparty claims to be paying. |
| `claimed_beneficiary_name` | No | The name the simulated counterparty claims for the beneficiary. Omit it to have the simulator claim the destination account owner's own name — the transfer is authorized. Supply a name that doesn't match, and Tesser's own name-matching rejects the transfer, which is how you rehearse your refusal path. |
| `network` | No | One of `BASE_SEPOLIA`, `ETHEREUM_SEPOLIA`, `POLYGON_AMOY`. Defaults to `BASE_SEPOLIA`. |

The request never accepts `mocked_risk_status` — see [Risk status is not simulated here](#risk-status-is-not-simulated-here).

## The authorized call

Omit `claimed_beneficiary_name` and the simulator claims the destination wallet owner's actual name, so the name match succeeds and the transfer is authorized:

```bash
curl -X POST https://api.tesser.xyz/v1/payments/simulate-travel-rule-inbound \
  -H "Authorization: Bearer $CLIENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to_account": "7a3d8f2e-6b4c-4a91-b8e5-2f9c1d7e3a0b",
    "network": "BASE_SEPOLIA"
  }'
```

Once the compliance round trip finishes and the resulting payment exists, the endpoint responds `201`:

```json
{
  "data": {
    "id": "5a9c3e7f-2b1d-4f48-a6e0-8c4b2d1f9a3e",
    "workspace_id": "b53f6690-3242-4942-9907-885779632832",
    "direction": "inbound",
    "funding_account_id": null,
    "estimated": {
      "from": { "account_id": null, "amount": "10000", "currency": "USDC", "network": "BASE_SEPOLIA" },
      "to": { "account_id": "7a3d8f2e-6b4c-4a91-b8e5-2f9c1d7e3a0b", "amount": "10000", "currency": "USDC", "network": "BASE_SEPOLIA" }
    },
    // ... other payment fields (desired, actual, created_at, ...), as on any inbound payment
    "risk_status": "unchecked",
    "risk_status_reasons": [],
    "participants": [
      // ... ORIGINATOR, ORIGINATOR_VASP and BENEFICIARY entries, exactly as a real counterparty-initiated inbound would carry them
    ],
    "provider_metadata": [
      {
        "provider_metadata_type": "TRAVEL_RULE",
        "provider_metadata_key": "NOTABENE",
        "provider_metadata_data": {
          // ... the raw Notabene transfer record
        }
      }
    ],
    "expires_at": null,
    "steps": [
      {
        "step_sequence": 1,
        "step_type": "transfer",
        "status": "created",
        "transaction_hash": null,
        "confirmed_at": null
        // ...
      }
    ],
    "travel_rule": {
      "transfer_id": "e3b9f2a1-7c4d-4e0a-9b1f-2d8c5a6e3f70",
      "is_travel_rule": true,
      "presentation_answered": true,
      "status": "SETTLED",
      "name_matching_checked": true,
      "settlement_id": "eip155:84532:tx/0x4f2a9c1e8b3d"
    }
  }
}
```

`travel_rule` sits alongside the payment fields inside `data` — it is not a separate resource. Fields:

| Field | Description |
| --- | --- |
| `transfer_id` | The Notabene transfer ID the simulator created. It is not a Tesser payment ID and cannot be passed to `GET /v1/payments/{id}`, nor filtered on — see [The 202 response](#the-202-response) for how to find the payment it becomes. |
| `is_travel_rule` | Whether Notabene flagged the simulated transfer as Travel Rule. Notabene sets this shortly after the transfer is created; `false` means either it never did within the wait budget or reading the transfer was permanently refused — either way, no compliance decision will follow. |
| `presentation_answered` | Whether the simulated counterparty's PII reached your organization's Travel Rule policy. `false` means the PII exchange itself failed — no name-matching decision was made. |
| `status` | The transfer's status at Notabene, re-read after the simulator tried to settle it. `SETTLED` is the normal authorized path — Notabene authorized the transfer and the simulator's settlement went through. `AUTHORIZED` means Notabene authorized it but either the settle call or the re-read afterward did not go through; `settlement_id` is then `null`. `REJECTED` means your organization's name matching refused the claim. `null` means the transfer's state could not be read within the wait budget. |
| `name_matching_checked` | Whether a beneficiary-name-matching check was recorded for the transfer. |
| `settlement_id` | The fabricated settlement identifier the simulator used to drive the transfer to `SETTLED`, or `null` if the simulated settlement did not go through (for example, because the transfer was rejected — a rejected transfer is never settled — or because the settle call itself failed). No transaction was ever broadcast; this value only exists to make Notabene's settlement webhook fire, which stamps it onto the payment's pending step — see [below](#the-missing-transaction-hash) for what that means for `transaction_hash`. |

:::warning{title="201 is not proof of authorization"}
A `201` response means the compliance round trip *finished* — it does not mean it finished with authorization. A rejected transfer also produces a payment here, with its step `failed` rather than pending. Always read `travel_rule.status` for the actual decision, not just the HTTP status code.
:::

### The missing transaction hash

`transaction_hash: null` in the response above is expected, and stays that way even after settlement. The simulator settles the Notabene transfer with a fabricated CAIP-220 settlement id (`travel_rule.settlement_id`) rather than a real on-chain transaction, purely to make Notabene fire its settlement webhook. That webhook arrives just after this response and stamps the fabricated hash onto the payment's pending step — so a subsequent `GET /v1/payments/{id}` shows a step that carries a `transaction_hash` while its `status` is still `created`. Nothing was ever broadcast, so nothing will ever confirm it: that is the resting state by design, not a sign of something stuck.

## The refusal call

Supply a `claimed_beneficiary_name` that does not match the destination wallet's owner, and Tesser's own name-matching logic refuses the transfer — the same decision it would make against a genuine counterparty:

```bash
curl -X POST https://api.tesser.xyz/v1/payments/simulate-travel-rule-inbound \
  -H "Authorization: Bearer $CLIENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to_account": "7a3d8f2e-6b4c-4a91-b8e5-2f9c1d7e3a0b",
    "claimed_beneficiary_name": "Someone Else Entirely",
    "network": "BASE_SEPOLIA"
  }'
```

The simulated counterparty claims that wrong name as the beneficiary. Your organization's name-matching compares it against the destination account owner's actual name, does not find a match, and rejects the transfer. The response is still `201` — the compliance round trip finished, it just finished with a rejection:

```json
{
  "data": {
    "id": "8c4b2d1f-9a3e-4f48-a6e0-5a9c3e7f2b1d",
    "workspace_id": "b53f6690-3242-4942-9907-885779632832",
    "direction": "inbound",
    "steps": [
      {
        "step_sequence": 1,
        "step_type": "transfer",
        "status": "failed",
        "transaction_hash": null,
        "confirmed_at": null
        // ...
      }
    ],
    "travel_rule": {
      "transfer_id": "9f1a2b6c-3d8e-4a70-b5f2-1c9e6d3a8b70",
      "is_travel_rule": true,
      "presentation_answered": true,
      "status": "REJECTED",
      "name_matching_checked": true,
      "settlement_id": null
    }
    // ...
  }
}
```

`settlement_id` is `null` here — a rejected transfer is never settled. The pending step's `status` is `failed`, not `created`; see [Payment Participants](/overviews/payment-participants) for how a rejected Beneficiary name-match generally surfaces on a payment's `participants[]`.

## The 202 response

The compliance round trip — Notabene flagging the transfer as Travel Rule, the PII exchange, and your organization's name-matching decision — can take longer than the request's wait budget (about 40 seconds). When it does, the endpoint responds `202` with what it knows so far, rather than blocking further:

```json
{
  "data": {
    "status": "pending",
    "travel_rule": {
      "transfer_id": "e3b9f2a1-7c4d-4e0a-9b1f-2d8c5a6e3f70",
      "is_travel_rule": true,
      "presentation_answered": false,
      "status": null,
      "name_matching_checked": false,
      "settlement_id": null
    }
  }
}
```

Nothing is settled on this path — `settlement_id` is always `null` in a `202` response, and the payment does not exist yet, so this response carries no `payment_id` to poll on. `is_travel_rule: false` means either Notabene never flagged the simulated transfer as Travel Rule within the wait budget or reading the transfer was permanently refused, so no decision — and no payment — will ever follow for that transfer; `presentation_answered: false` means the simulated counterparty's PII never reached your organization's policy.

Do **not** re-issue the same request to find the earlier payment: every call creates a brand-new Notabene transfer and a new simulated inbound, so retrying can never surface the one you are waiting on, only add another. There is also no supported way to look the earlier transfer up by ID — `GET /v1/payments`'s filters (`start_date`, `end_date`, `direction`) don't include a transfer or provider-reference filter. Once the compliance decision lands, get the resulting payment one of two ways:

- Subscribe to the `payment.created` webhook, if you receive webhooks in this environment, and read the payment ID off the event.
- List inbound payments (`GET /v1/payments?direction=inbound`) and match the `transfer_id` you got from your original request or `202` response against `provider_metadata[].provider_metadata_data["@id"]` on each result yourself. `travel_rule` is only present in this endpoint's own response — `GET /v1/payments` does not return it.

## Risk status is not simulated here

The request does not accept `mocked_risk_status`. Sending it is rejected outright, because risk screening runs on inbound *money*, and this endpoint never produces any — there is nothing for a risk verdict to attach to. To rehearse a chosen risk outcome, use [`POST /v1/payments/simulate-inbound`](#simulate-travel-rule-inbound-vs-simulate-inbound) instead.

## `simulate-travel-rule-inbound` vs `simulate-inbound`

Two separate simulators cover two separate halves of an inbound payment:

<div className="balanced-table first-col-22">

| | `simulate-travel-rule-inbound` | `simulate-inbound` |
| --- | --- | --- |
| Simulates | The Travel Rule compliance exchange | The money |
| Counterparty | A real Notabene transfer from a Tesser-operated simulator VASP | N/A — funds come from the platform faucet |
| On-chain transfer | None. Nothing is ever broadcast. | A real test-USDC transfer, broadcast on-chain |
| Balance change | None | Yes — the destination wallet is credited once the transfer confirms |
| Pending step | Stays `created` (or moves to `failed` on a rejection) | Progresses to `confirmed`, then `completed` |
| Risk outcome | Not applicable — `mocked_risk_status` is rejected | Chosen via `mocked_risk_status` |
| What you control | `claimed_beneficiary_name`, to force authorization or rejection | `mocked_risk_status`, to force a risk verdict |

</div>

If you need to test both — a payment whose Travel Rule exchange you control *and* whose balance actually moves — call both endpoints; they are independent and neither substitutes for the other.

## Errors

| Error Code | HTTP Status | Meaning |
| --- | --- | --- |
| `payments-1002` | 404 | `to_account` did not resolve to an account in this workspace. |
| `payments-3053` | 400 | Simulation is not available in production. |
| `payments-3054` | 400 | An unsupported `network` value, from an internal configuration gap rather than the request itself — a `network` outside `BASE_SEPOLIA`, `ETHEREUM_SEPOLIA`, `POLYGON_AMOY` is rejected by request validation before this error can fire. |
| `payments-3055` | 400 | `to_account` exists but is not a managed `stablecoin_ethereum` wallet with an on-chain address. |
| `payments-3083` | 400 | The `notabene-travel-rule-inbound` feature flag is not enabled for your organization. Contact Tesser support to have it enabled. |
| `payments-3084` | 503 | The simulator VASP is not configured in this environment. This is an environment issue on Tesser's side, not something a request can fix. |
| `payments-3085` | 400 | Your organization has no Travel Rule VASP identity configured with Tesser. |
| `payments-3086` | 400 | The destination account resolved to no usable beneficiary name to claim. Supply `claimed_beneficiary_name`, or give the account's entity a name. |

See the [Errors](/overviews/errors#payments) reference for the full Payments error table, and [Compliance and Risk Management](/overviews/compliance-and-risk) for how Travel Rule and wallet risk screening apply across all payment directions.
