# Wallet — Swaps API

The holder's non-custodial Tempo wallet — balances, deposits, sends, conversions, bank details. Nothing here can sign.

**Jump to an operation:**

- [GET /wallet/wallets](#list-the-holders-wallets-available)
- [POST /wallet/wallets](#create-or-sync-the-holders-wallet-available)
- [GET /wallet/wallets/{id}](#get-one-wallet-available)
- [GET /wallet/balances](#get-the-holders-wallet-balances-available)
- [GET /wallet/transactions](#list-recent-wallet-transfers-available)
- [GET /wallet/transactions/{hash}](#get-one-wallet-transaction-available)
- [GET /wallet/deposit_routes](#list-candidate-deposit-routes-available)
- [POST /wallet/deposit_quotes](#preview-a-deposits-haircut-dark-flag)
- [GET /wallet/deposit_intents](#list-the-holders-deposit-intents-dark-flag)
- [POST /wallet/deposit_intents](#create-a-cross-network-deposit-address-dark-flag)
- [GET /wallet/deposit_intents/{id}](#get-one-deposit-intent-dark-flag)
- [GET /wallet/send_routes](#list-candidate-send-routes-available)
- [GET /wallet/send_intents](#list-the-holders-cross-network-send-intents-dark-flag)
- [POST /wallet/send_intents](#prepare-a-cross-network-send-dark-flag)
- [GET /wallet/send_intents/{id}](#get-one-send-intent-dark-flag)
- [POST /wallet/send_intents/{id}/source_tx](#record-a-send-intents-source-transaction-available)
- [POST /wallet/offramp_quotes](#quote-a-bank-withdrawal-dark-flag)
- [GET /wallet/external_accounts](#list-the-holders-external-bank-accounts-dark-flag)
- [POST /wallet/external_accounts](#add-a-bank-account-the-holder-owns-dark-flag)
- [GET /wallet/offramp_intents](#list-the-holders-bank-withdrawals-dark-flag)
- [POST /wallet/offramp_intents](#withdraw-from-the-wallet-to-a-bank-account-dark-flag)
- [GET /wallet/offramp_intents/{id}](#get-one-bank-withdrawal-dark-flag)
- [POST /wallet/offramp_intents/{id}/cancel](#cancel-a-bank-withdrawal-dark-flag)
- [GET /wallet/virtual_accounts](#list-the-holders-virtual-accounts-dark-flag)
- [POST /wallet/virtual_accounts](#create-a-virtual-account-dark-flag)
- [GET /wallet/virtual_accounts/{id}](#get-one-virtual-account-dark-flag)
- [POST /wallet/virtual_accounts/{id}/deactivate](#deactivate-a-virtual-account-dark-flag)
- [POST /wallet/virtual_accounts/{id}/reactivate](#reactivate-a-virtual-account-dark-flag)
- [GET /wallet/virtual_accounts/{id}/history](#list-a-virtual-accounts-deposit-history-dark-flag)
- [POST /wallet/conversions](#build-an-unsigned-same-chain-conversion-available)
- [GET /wallet/conversions/{id}](#track-a-conversion-available)
- [POST /wallet/conversions/{id}/source_tx](#record-a-conversions-source-transaction-available)
- [GET /wallet/conversion_pairs](#list-convertible-token-pairs-dark-flag)

> Generated from the bundled OpenAPI contract. The HTML page and this Markdown twin share the same processed operation descriptions and status annotations.

<a id="list-the-holders-wallets-available"></a>
## `GET /wallet/wallets` — List the holder's wallets — available

**Operation ID:** `wallet.wallets.list`

> **Status:** Available · **Callers:** agent (business key), dashboard session, business key · **Scope:** `wallet.read` · **Test mode:** unavailable

List the caller's own non-custodial Tempo wallets. Business-key callers may read but never create or sync one (RESOURCE-MODEL §2.3). Only wallets on this environment's Tempo network are listed (`Wallet.network`); when the environment cannot resolve its network the answer is `503 temporarily_unavailable`, never a list across networks.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A page of the caller's wallets. | `application/json` [WalletList](/reference/~schemas.md#walletlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="create-or-sync-the-holders-wallet-available"></a>
## `POST /wallet/wallets` — Create or sync the holder's wallet — available

**Operation ID:** `wallet.wallets.create`

> **Status:** Available · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.write` · **Test mode:** unavailable

Creates the holder's Tempo wallet on first use, or re-syncs it on reconnect. A second create while one is already active answers `409 conflict` (`wallet_already_exists`), matching prod (R13-wallet §1 `wallet-gate.html`). Business-key callers cannot call this — they may only read (RESOURCE-MODEL §2.3). The wallet is always on this environment's Tempo network (`Wallet.network`): an already-active wallet found on another network is never returned, the answer is `409 conflict` (`wallet_already_exists`).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [WalletCreateRequest](/reference/~schemas.md#walletcreaterequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `201` | The wallet, created or synced. | `application/json` [Wallet](/reference/~schemas.md#wallet) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="get-one-wallet-available"></a>
## `GET /wallet/wallets/{id}` — Get one wallet — available

**Operation ID:** `wallet.wallets.get`

> **Status:** Available · **Callers:** agent (business key), dashboard session, business key · **Scope:** `wallet.read` · **Test mode:** unavailable

Read one of the caller's own wallets. A wallet on another Tempo network than this environment's reads as `404 not_found`; an environment that cannot resolve its network answers `503 temporarily_unavailable`.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `id` | path | yes | `string` | — |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The wallet. | `application/json` [Wallet](/reference/~schemas.md#wallet) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="get-the-holders-wallet-balances-available"></a>
## `GET /wallet/balances` — Get the holder's wallet balances — available

**Operation ID:** `wallet.balances.get`

> **Status:** Available · **Callers:** agent (business key), dashboard session, business key · **Scope:** `wallet.read` · **Test mode:** unavailable

Every stablecoin held on the caller's own wallet, plus a server-computed `usd_total`. The server resolves the caller's own wallet address server-side; a client-supplied address is never accepted, here or on any wallet read (K4 prerequisite — RESOURCE-MODEL §0.12, §2.3 D-9). A thrown error must never be read as a `$0.00` balance. Only a wallet on this environment's Tempo network is read, and `network` names it; an environment that cannot resolve its network answers `503 temporarily_unavailable`.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The caller's wallet balances. | `application/json` [WalletBalances](/reference/~schemas.md#walletbalances) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-recent-wallet-transfers-available"></a>
## `GET /wallet/transactions` — List recent wallet transfers — available

**Operation ID:** `wallet.transactions.list_recent`

> **Status:** Available · **Callers:** agent (business key), dashboard session, business key · **Scope:** `wallet.read` · **Test mode:** unavailable

A bounded recent window of on-chain transfers for the caller's own wallet — never full history, and never paginated (A1-6: this operation carried a `cursor` parameter that the handler silently ignored; dropped here rather than wired up, since `WalletTransactionList` is a fixed window, not a cursor-list — `transfers[]` stays the array name, not `data`, since `packages/sdk-ts` and `claude/dashboard-v2` already key off it). The server resolves the caller's own wallet address; a client-supplied address is never accepted (K4 prerequisite — RESOURCE-MODEL §0.12, §2.3). `degraded: true` with an empty `transfers[]` means the chain read failed and must never be read as "no activity"; offer a retry instead.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A bounded window of the caller's wallet transfers. | `application/json` [WalletTransactionList](/reference/~schemas.md#wallettransactionlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="get-one-wallet-transaction-available"></a>
## `GET /wallet/transactions/{hash}` — Get one wallet transaction — available

**Operation ID:** `wallet.transactions.get`

> **Status:** Available · **Callers:** agent (business key), dashboard session, business key · **Scope:** `wallet.read` · **Test mode:** unavailable

Read one on-chain transaction by hash, scoped to the caller's own wallet. A transaction the wallet sent (receipt `from`) is `out`. One it did not send is read from the receipt's decoded TIP-20 `Transfer` logs of the accepted Swaps-dollar tokens, because a token transfer's receipt `to` is the token contract: a transfer to the wallet is `in` (checked first), one from it `out`, and `amount`, `symbol` and the counterparty come from that log when it is the only Transfer touching the wallet. A real hash that touches neither is 404; a hash with no receipt is `status: not_found`.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `hash` | path | yes | `string` | — |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The transaction. | `application/json` [WalletTransaction](/reference/~schemas.md#wallettransaction) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-candidate-deposit-routes-available"></a>
## `GET /wallet/deposit_routes` — List candidate deposit routes — available

**Operation ID:** `wallet.deposit_routes.list`

> **Status:** Available · **Callers:** agent (business key), dashboard session, business key · **Scope:** `wallet.read` · **Test mode:** unavailable

The catalogue of cross-network paths onto Tempo. Auth is optional by design — this is route discovery, not an account read; a credential, if sent, is not checked for a specific account scope (RESOURCE-MODEL §2.3). K5a's `/v1` implementation currently requires a credential regardless (the router has no "optional auth" lane on the authenticated route table yet) — the response is identical with or without one, so no caller-specific data is withheld either way; a true anonymous-auth lane is a named follow-up.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The candidate deposit routes. | `application/json` [DepositRouteList](/reference/~schemas.md#depositroutelist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="preview-a-deposits-haircut-dark-flag"></a>
## `POST /wallet/deposit_quotes` — Preview a deposit's haircut — dark-flag

**Operation ID:** `wallet.deposit_quotes.create`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.write` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

L2-4 (R13-wallet §2 W-G1). Nothing is written on Swaps' side and no `deposit_intent` row is created, unlike `deposit_intents.create` — but Relay itself still allocates a fresh, funds-accepting deposit address per call (the response never returns it, and Swaps records nothing that reconciles or expires it). Resolves the SAME live Relay quote `deposit_intents.create` itself uses for the candidate `source_chain`/`source_asset` and reports only what that quote returned — `amount_out` and `haircut_pct` are never a locally invented figure, and a Relay outage answers a typed `503`, never an estimate. `Idempotency-Key` is required — like `deposit_intents.create`, a repeat POST without it would allocate another Relay address and consume another provider quote; a replay under the SAME key returns the stored price for 24h rather than a fresh one, so use a new key per quote you actually want re-priced. Use it to show a "you send X, receive ~Y" line before the holder commits to a real deposit. Refused before the provider is asked while Receive is paused on the wallet: `422 wallet_signer_at_risk` when the server cannot confirm the wallet's key or its key was reported lost (the message then says so), `503 wallet_signer_unreadable` (retryable) when it could not check.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [DepositQuoteRequest](/reference/~schemas.md#depositquoterequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The computed preview. | `application/json` [DepositQuote](/reference/~schemas.md#depositquote) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-the-holders-deposit-intents-dark-flag"></a>
## `GET /wallet/deposit_intents` — List the holder's deposit intents — dark-flag

**Operation ID:** `wallet.deposit_intents.list`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.read` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

List the caller's deposit intents, optionally filtered to the non-terminal set.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |
| `status` | query | no | [DepositIntentStatus](/reference/~schemas.md#depositintentstatus) | Filter to one status, e.g. the non-terminal set for an "in progress" view. |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A page of the caller's deposit intents. | `application/json` [DepositIntentList](/reference/~schemas.md#depositintentlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="create-a-cross-network-deposit-address-dark-flag"></a>
## `POST /wallet/deposit_intents` — Create a cross-network deposit address — dark-flag

**Operation ID:** `wallet.deposit_intents.create`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.write` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

Mints a one-time, amount-bound deposit address for a cross-network deposit onto Tempo. `Idempotency-Key` is mandatory — a repeat POST without it would mint a second address and a second provider quote (RESOURCE-MODEL §2.3 D-2). The API never signs and never holds funds; this only returns where to send and what will arrive. Refused before any provider call while Receive is paused on the wallet: `422 wallet_signer_at_risk` when the server cannot confirm the wallet's key or its key was reported lost (the message then says so; retrying does not change it until the key is confirmed), `503 wallet_signer_unreadable` (retryable, nothing created) when it could not check.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [DepositIntentCreateRequest](/reference/~schemas.md#depositintentcreaterequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `201` | The new deposit intent. | `application/json` [DepositIntent](/reference/~schemas.md#depositintent) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="get-one-deposit-intent-dark-flag"></a>
## `GET /wallet/deposit_intents/{id}` — Get one deposit intent — dark-flag

**Operation ID:** `wallet.deposit_intents.get`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.read` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

Read one deposit intent's status, hashes and timeline. Poll this until a terminal state (`settled`, `expired`, `failed`, `cancelled`, `manual_recovery_required`).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `id` | path | yes | `string` | — |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The deposit intent. | `application/json` [DepositIntent](/reference/~schemas.md#depositintent) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-candidate-send-routes-available"></a>
## `GET /wallet/send_routes` — List candidate send routes — available

**Operation ID:** `wallet.send_routes.list`

> **Status:** Available · **Callers:** agent (business key), dashboard session, business key · **Scope:** `wallet.read` · **Test mode:** unavailable

The catalogue of paths off the holder's Tempo wallet. `intent_required: false` marks a direct same-chain Tempo send, which never touches `/v1` at all — the holder's passkey signs a raw chain call directly (RESOURCE-MODEL §0.10, §2.3). Auth is optional.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The candidate send routes. | `application/json` [SendRouteList](/reference/~schemas.md#sendroutelist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-the-holders-cross-network-send-intents-dark-flag"></a>
## `GET /wallet/send_intents` — List the holder's cross-network send intents — dark-flag

**Operation ID:** `wallet.send_intents.list`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.read` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

List the caller's cross-network send intents, optionally filtered to the non-terminal set. A direct same-chain Tempo send never creates one of these (RESOURCE-MODEL §0.10).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |
| `status` | query | no | [SendIntentStatus](/reference/~schemas.md#sendintentstatus) | Filter to one status, e.g. the non-terminal set for an "in progress" view. |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A page of the caller's send intents. | `application/json` [SendIntentList](/reference/~schemas.md#sendintentlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="prepare-a-cross-network-send-dark-flag"></a>
## `POST /wallet/send_intents` — Prepare a cross-network send — dark-flag

**Operation ID:** `wallet.send_intents.create`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.write` · **Test mode:** unavailable · **Money boundary:** per-transaction human confirmation required · **Non-custodial:** response carries unsigned steps — Swaps never signs
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

Money boundary (RESOURCE-MODEL §0.10, §0.11) — this crosses the boundary and per-transaction human confirmation is required before its unsigned steps are signed. The API never signs: the response carries `send_instructions.steps[]`, an ordered list of unsigned calls the holder's own passkey must sign — nothing leaves the wallet until then. Never chain this from a quote straight into signing without a human confirming first; only a direct same-chain Tempo send bypasses this endpoint entirely. Before any provider call the recipient is refused when it is a Swaps Wallet address (`422 destination_rail_keyless`: a Swaps Wallet receives on Tempo only) or a deposit address Swaps issued (`422 destination_swaps_deposit_address`), `param: destination_address`; `503 destination_risk_unverifiable` when that could not be checked (nothing was created). The wallet codes go only to a sender sending to their own wallet: someone else's Swaps Wallet answers `422 destination_not_accepted` («This address can't take this transfer. Use another address.»), or `503 destination_check_unavailable` when the wallet or key-mark lookup itself failed — never the reason.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [SendIntentCreateRequest](/reference/~schemas.md#sendintentcreaterequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `201` | The new send intent, with unsigned steps to sign. | `application/json` [SendIntent](/reference/~schemas.md#sendintent) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="get-one-send-intent-dark-flag"></a>
## `GET /wallet/send_intents/{id}` — Get one send intent — dark-flag

**Operation ID:** `wallet.send_intents.get`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.read` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

Read one send intent's status, hashes and timeline. Poll this until a terminal state (`settled`, `expired`, `failed`, `cancelled`).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `id` | path | yes | `string` | — |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The send intent. | `application/json` [SendIntent](/reference/~schemas.md#sendintent) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="record-a-send-intents-source-transaction-available"></a>
## `POST /wallet/send_intents/{id}/source_tx` — Record a send intent's source transaction — available

**Operation ID:** `wallet.send_intents.source_tx.set`

> **Status:** Available · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.write` · **Test mode:** unavailable

Records the transaction hash the holder's wallet just broadcast for this intent's source leg, moving it to `source_submitted` only once the chain itself confirms an exact-token, exact-amount transfer from the holder's own wallet to the deposit step's own recipient — never a client assertion. Call once, immediately after signing. Re-sending the same hash is a safe no-op; a different hash for the same intent is refused — never retry with a fresh hash to force it through. A send prepared to fund a payout that is cancelled or closed without funds is never recorded against that payout: `409 send_intent_payout_closed` once the hash is recorded on the send (support reconciles it from there). If that write could not be made or verified, the call answers `503 source_tx_record_failed`: the hash is not recorded yet — retry the same call with the same `Idempotency-Key` after `Retry-After`. A send `payouts.fund` handed to one session is recorded only from that session, also after the expiry sweep marked it `expired` (it moves on to `source_submitted`; `409 source_tx_already_recorded` if a replacement send is already live for the payout); any other session gets `409 wallet_funding_claimed` and nothing is recorded.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |
| `id` | path | yes | `string` | — |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [SendIntentSourceTxRequest](/reference/~schemas.md#sendintentsourcetxrequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The send intent, updated to `source_submitted`. | `application/json` [SendIntent](/reference/~schemas.md#sendintent) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | State moved or the idempotency key was reused with a different body. `source_tx_already_recorded`: another intent already holds this hash, or this intent a different one (also toward a closed payout); never retry it. `source_tx_not_yet_confirmed`: retry once the chain confirms it. `send_intent_payout_closed`: this send left the wallet toward a payout that is now cancelled or closed without funds, and its hash is recorded on the send, not against the payout; support reconciles it from there. `wallet_funding_claimed`: this send was handed to another session by `payouts.fund`; only that session records it, nothing was recorded. | `application/json` [Error](/reference/~schemas.md#error) |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | A kill switch is thrown, test mode is refused, or a dependency is out (`temporarily_unavailable`); retry after `Retry-After`. `send_intent_no_steps`: the stored intent has no signable steps; contact support. `source_tx_record_failed`: the send left the wallet toward a payout that is now closed and its hash is not recorded yet (a database error); retry the same call with the same `Idempotency-Key` after `Retry-After` until it answers `409 send_intent_payout_closed` (or `200` when the hash is already on the send). | `application/json` [Error](/reference/~schemas.md#error) |

---

<a id="quote-a-bank-withdrawal-dark-flag"></a>
## `POST /wallet/offramp_quotes` — Quote a bank withdrawal — dark-flag

**Operation ID:** `wallet.offramp_quotes.create`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.write` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

Compute-only — nothing is persisted, no address is minted. Prices what the holder's wallet would need to send for a given bank-destination amount. Indicative only: `expected_source` is a point-in-time estimate, quantised to 2 decimals (`decimals: 2`, not USDC's native 6 — the quote engine rounds to cents), with no `expires_at` in this response — the actual rate is re-priced live at `offramp_intents.create`, which is the only figure that is ever binding.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [OfframpQuoteRequest](/reference/~schemas.md#offrampquoterequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The computed quote. | `application/json` [OfframpQuote](/reference/~schemas.md#offrampquote) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-the-holders-external-bank-accounts-dark-flag"></a>
## `GET /wallet/external_accounts` — List the holder's external bank accounts — dark-flag

**Operation ID:** `wallet.external_accounts.list`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.read` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

List the holder's own bank-account destinations, masked. Own means added here with `attest_own_account`, or saved under the holder's provider customer and not recorded by Swaps as anyone else's (an account Swaps recorded as a Pay an invoice or payroll recipient appears only once it is added here in the verified legal name); a holder name that differs from the verified legal name never hides one. Filtered to the rails an own account can be added for (`ach`/`wire`/`sepa`/`spei`/`faster_payments`/`pix`) — a Bridge customer's other external accounts (e.g. one on a rail no `/v1` create offers) are never returned here. `rail` is an open enum: tolerate a value you do not know. Only `ach`/`wire`/`sepa`/`spei` accounts can receive the wallet's own bank withdrawal (`offramp_intents`); `faster_payments` and `pix` accounts are sell payout destinations (`POST /v1/orders`). KNOWN LIMITATION: Bridge's own account record cannot distinguish `wire` from `ach` on read (both share the identical account+routing shape) — a `wire` account you added still lists back with `rail: "ach"`.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A page of the holder's external accounts. | `application/json` [ExternalAccountList](/reference/~schemas.md#externalaccountlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="add-a-bank-account-the-holder-owns-dark-flag"></a>
## `POST /wallet/external_accounts` — Add a bank account the holder owns — dark-flag

**Operation ID:** `wallet.external_accounts.create`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.write` · **Test mode:** unavailable · **Money boundary:** per-transaction human confirmation required
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

Money boundary, REST-only — never an MCP tool argument, because most MCP clients log tool arguments verbatim and raw bank details must cross the boundary exactly once (RESOURCE-MODEL §2.3). `attest_own_account` must be `true`; an agent may never add a destination on a user's behalf. The account holder's name is never sent: it is the verified legal name on the holder's verification record. The holder's own `address` (street, city, postal code, country; the state in the US) is needed on `ach` and `wire` and optional elsewhere: without a usable one the answer is `422 address_required` with `param: address` — ask the person for it and send it — and an address the provider refuses is `422 address_invalid`. `faster_payments` (GBP: sort code and account number) and `pix` (BRL: a Pix key) accounts are payout destinations for a sell order; the wallet's own bank withdrawal does not pay them (`offramp_intents.create` refuses those rails). A sell pays out only on a rail the provider approved the holder for: read `Customer.endorsement_statuses` (`faster_payments`, `pix`) before offering the rail. Bank details already saved as a Pay an invoice or payroll recipient are refused (`owner_name_mismatch`) unless the provider's holder name on that account is the verified legal name (ignoring case, spacing, punctuation, accents and word order); then this attestation makes it the holder's own. Bank details the provider already holds are reused only when exactly one saved account of the same currency carries the same account tail (and the same BIC, routing number or sort code when both sides have one); otherwise the answer is `409 conflict` and nothing is saved (`503` when the saved accounts can't be read right now; retry). The `201` publishes the rail on the provider's own record, not the `rail` you requested; the one exception is that a `wire` account whose provider record reads back as `ach` (both share the identical account+routing shape) still publishes the `wire` you requested — see `GET /wallet/external_accounts`'s description for the same read-side ambiguity.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [ExternalAccountCreateRequest](/reference/~schemas.md#externalaccountcreaterequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `201` | The new external account, masked. | `application/json` [ExternalAccount](/reference/~schemas.md#externalaccount) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-the-holders-bank-withdrawals-dark-flag"></a>
## `GET /wallet/offramp_intents` — List the holder's bank withdrawals — dark-flag

**Operation ID:** `wallet.offramp_intents.list`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.read` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

List the caller's off-ramp intents, optionally filtered to the non-terminal set. Returns at most the 100 most recent off-ramp intents; `has_more: false` means the end of that window, not necessarily the end of the holder's history. Paging past 100 needs the upstream keyset tracked as https://github.com/swapsapp/swaps/issues/3460.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |
| `status` | query | no | [OfframpIntentStatus](/reference/~schemas.md#offrampintentstatus) | Filter to one status, e.g. the non-terminal set for an "in progress" view. |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A page of the caller's off-ramp intents. | `application/json` [OfframpIntentList](/reference/~schemas.md#offrampintentlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="withdraw-from-the-wallet-to-a-bank-account-dark-flag"></a>
## `POST /wallet/offramp_intents` — Withdraw from the wallet to a bank account — dark-flag

**Operation ID:** `wallet.offramp_intents.create`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.write` · **Test mode:** unavailable · **Money boundary:** per-transaction human confirmation required
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

Money boundary (RESOURCE-MODEL §0.10, §0.11) — per-transaction human confirmation is required. The transit deposit into `deposit_instructions` is a direct same-chain Tempo call the holder's own passkey signs; it is never a `/v1` write. Refuses over the per-rail minimum or the $3,000 V1 ceiling, and refuses outright if the amount cannot be priced. `external_account_id` must be one of the holder's own accounts (`GET /wallet/external_accounts`); any other id answers `404` before any provider call.

**One open transfer per deposit address (2026-10-28).** The withdrawal is a flexible transfer of the holder's USDC on Tempo, at the deposit address on Tempo that the holder's sells of USDC on Tempo use too. While a sell of the holder's USDC on Tempo is open at Bridge, still being confirmed by Bridge, or being created, the create is refused `409 deposit_in_use` (`sideEffectFree: true`) before any provider call: `error.details.order_id` names an open sell order (send to it or cancel it first; `error.details.payout_id` would name an open payout instead); with nothing named, try again once the sell is confirmed. When that check cannot be read the answer is `503 temporarily_unavailable` with `Retry-After`, side-effect free: nothing was created. The other way round, while a withdrawal waits for its deposit, a sell of USDC on Tempo is refused (`orders.create`, `409 deposit_in_use`).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [OfframpIntentCreateRequest](/reference/~schemas.md#offrampintentcreaterequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `201` | The new off-ramp intent. | `application/json` [OfframpIntent](/reference/~schemas.md#offrampintent) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="get-one-bank-withdrawal-dark-flag"></a>
## `GET /wallet/offramp_intents/{id}` — Get one bank withdrawal — dark-flag

**Operation ID:** `wallet.offramp_intents.get`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.read` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

Read one off-ramp intent's status and deposit instructions. Poll until a terminal state (`payment_processed`, `refunded`, `refund_failed`, `canceled`, `failed`).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `id` | path | yes | `string` | — |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The off-ramp intent. | `application/json` [OfframpIntent](/reference/~schemas.md#offrampintent) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="cancel-a-bank-withdrawal-dark-flag"></a>
## `POST /wallet/offramp_intents/{id}/cancel` — Cancel a bank withdrawal — dark-flag

**Operation ID:** `wallet.offramp_intents.cancel`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.write` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

Cancels an off-ramp intent only from `awaiting_funds` (RESOURCE-MODEL §2.3) — once the transit deposit is signed and broadcast, this refuses rather than clawing money back. The exact eligibility window (whether `quoted` also qualifies) is an open question the design surfaces before the transit deposit is signed (R13-wallet §2 W-G14, **проверить**).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |
| `id` | path | yes | `string` | — |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The cancelled off-ramp intent. | `application/json` [OfframpIntent](/reference/~schemas.md#offrampintent) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-the-holders-virtual-accounts-dark-flag"></a>
## `GET /wallet/virtual_accounts` — List the holder's virtual accounts — dark-flag

**Operation ID:** `wallet.virtual_accounts.list`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.read` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

List the holder's bank-deposit accounts, per currency. Without `purpose` this includes Payment links collection accounts and `legacy_wallet` accounts (a wallet-purpose row settling outside this holder's Tempo wallet) that settle outside the wallet; a wallet screen asks for `purpose=wallet_funding`.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |
| `purpose` | query | no | `wallet_funding` \| `collection` \| `legacy_wallet` | Only accounts with this `purpose`. An unknown value is a `400 invalid_request`. |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A page of the holder's virtual accounts. | `application/json` [VirtualAccountList](/reference/~schemas.md#virtualaccountlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="create-a-virtual-account-dark-flag"></a>
## `POST /wallet/virtual_accounts` — Create a virtual account — dark-flag

**Operation ID:** `wallet.virtual_accounts.create`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.write` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

An explicit confirm — a virtual account is a billable provider object and is never auto-created (RESOURCE-MODEL §2.3 invariants). `409 conflict` when a Payment links collection account already receives this currency into this wallet and no activated wallet-funding account does (the provider would hand the collection account back instead of issuing a wallet-funding one): refused before any provider call, and again if the provider hands one back anyway. When an activated wallet-funding account is already on that currency and wallet, the create returns it (`409 conflict` only if the provider hands back the collection account instead). `GET /v1/capabilities?product=wallet_bank` publishes that corridor as `not_enabled` with `blocked_reason: collection_account_uses_currency`.
Refused before any provider write unless the `wallet_bank_virtual_account` corridor for `currency` reads `available` on `GET /v1/capabilities?product=wallet_bank` (W-R23). The create refuses in the order the capability publishes. First the caller (`owner_only`): only the account's owner creates its wallet bank accounts — `403 role_denied` (`permission_error`) for a `member`, at the operation's role gate, and `403 scope_denied` (`permission_error`) for an `admin` who is not the owner, before anything is read. Then it reads the holder, then its wallet: `409 capability_unavailable` with `details.reason: rail_not_enabled` while receive by bank is not launched for this account; `409 capability_unavailable` with `details.blocked_reason` `verification_closed` (the provider has given its final answer on the holder: a final rejection, a paused or offboarded customer; or the holder has used every verification attempt, so support is the next step), `payin_fiat_refused` (the provider reports the holder's fiat pay-ins as `inactive` or `rejected`) or `endorsement_not_requestable` (the holder's provider customer does not carry this currency's endorsement — USD `base`, EUR `sepa`, MXN `spei` — and no `/v1` operation requests one), whatever the holder's verification state, each with the corridor's own `detail` sentence as the message and, at the verification attempt limit, `details.detail_code: verification_attempts_exhausted`; the provider's own record is read again at the create, and an individual holder whose fiat pay-ins it reports `inactive` or `rejected` there is refused the same way (`details.blocked_reason: payin_fiat_refused`) before any account is issued; `409 network_not_supported` when the holder's wallet is not on Tempo mainnet (the provider settles USDC on Tempo mainnet, which a test-network wallet never sees) — except on a Swaps staging project whose provider runs in its sandbox, where a wallet on that project's own test network is accepted and nothing settles for real (a staging-only stub credits test tokens); `409 conflict` for a currency a Payment links collection account already receives into this wallet (above); `409 virtual_account_holder_not_verified` or `virtual_account_requirements_outstanding` while the holder still has a verification step to take, and `409 capability_unavailable` with `details.blocked_reason: payin_fiat_pending` when an individual holder's fiat pay-in activation could not be read — each with `details.currency`, `details.corridor_status` and, when the corridor has one, `details.blocked_reason`; last, `409 wallet_not_found` (`conflict`) when the holder has no active wallet yet — create one first (`POST /v1/wallet/wallets`). The capability publishes the same reason for that corridor. `503 temporarily_unavailable` when the launch flag, the holder's verification status or the accounts already on the wallet cannot be read, and `503 customer_status_unavailable` when the provider could not confirm the holder's verification status live (only a cached status was available) — all side-effect-free: retry after `Retry-After` with the same `Idempotency-Key`. `409 virtual_account_provider_refused` (`capability_unavailable`) when the provider itself refuses the create: `details.provider_status` is its HTTP status and `details.provider_code`, when it sent one, the name of its error code (never a value); no account is created. Do not retry it: no change to the request opens it. `422 destination_wallet_key_reported_lost` (`invalid_request`) when the account would pay into a Swaps Wallet whose key was reported lost (Swaps support recorded it on the holder's case): refused before any provider call, no account is created, and the same request is refused again until support clears the record after a newer signature by the same key — do not retry it. The same rule every inbound door applies refuses the other wallet facts under their own codes (`invalid_request`), before any provider call, with no account created: `422 destination_rail_keyless` (the account would pay into a Swaps Wallet off Tempo), `422 destination_wallet_disabled`, `422 destination_wallet_signer_at_risk` and `422 destination_wallet_signer_unconfirmed` (its key cannot be confirmed) — none clears by retrying. `503 destination_risk_unverifiable` (`temporarily_unavailable`) when that check could not run: nothing was created, retry after `Retry-After` with the same `Idempotency-Key`. Those reasons are told only to the wallet's own holder: for any other Swaps Wallet the answer is `422 destination_not_accepted`, and every caller hears `503 destination_check_unavailable` when the wallet or key-mark lookup itself failed. `503 temporarily_unavailable` without that guarantee when the provider did not complete the create: the account may exist, so list the accounts (`GET /v1/wallet/virtual_accounts`) before retrying, and retry with a new `Idempotency-Key` — the same key answers `409 idempotency_failed`.
Provider sandbox only (a Swaps staging project): `503 temporarily_unavailable` with `Retry-After` when the account was issued but could not be recorded as a sandbox account (one retry inside the request) — without that record its test deposits would not be credited. The account exists (listed with `provider_environment: null`); retry after `Retry-After` with the same `Idempotency-Key`: the retry reaches the same account and records it. Never answered for a live account.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [VirtualAccountCreateRequest](/reference/~schemas.md#virtualaccountcreaterequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `201` | The new virtual account. | `application/json` [VirtualAccount](/reference/~schemas.md#virtualaccount) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="get-one-virtual-account-dark-flag"></a>
## `GET /wallet/virtual_accounts/{id}` — Get one virtual account — dark-flag

**Operation ID:** `wallet.virtual_accounts.get`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.read` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

Read one of the holder's virtual accounts, including the "in your name" rail guard fields.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `id` | path | yes | `string` | — |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The virtual account. | `application/json` [VirtualAccount](/reference/~schemas.md#virtualaccount) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="deactivate-a-virtual-account-dark-flag"></a>
## `POST /wallet/virtual_accounts/{id}/deactivate` — Deactivate a virtual account — dark-flag

**Operation ID:** `wallet.virtual_accounts.deactivate`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.write` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

Deactivates a virtual account. Reversible via `reactivate`. `409 conflict` for a `collection` account: Payment links owns it. `409 conflict` (`legacy_wallet_read_only`) for a `legacy_wallet` account.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |
| `id` | path | yes | `string` | — |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The deactivated virtual account. | `application/json` [VirtualAccount](/reference/~schemas.md#virtualaccount) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="reactivate-a-virtual-account-dark-flag"></a>
## `POST /wallet/virtual_accounts/{id}/reactivate` — Reactivate a virtual account — dark-flag

**Operation ID:** `wallet.virtual_accounts.reactivate`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.write` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

Reactivates a previously deactivated virtual account. `409 conflict` for a `collection` account: Payment links owns it. `409 conflict` (`legacy_wallet_read_only`) for a `legacy_wallet` account. Reactivating reopens the account's bank details, so the provider customer is read live first (2026-10-12) and, before any provider write, the reactivation is refused for an `individual` holder whose fiat pay-ins the provider reports `inactive` or `rejected` (`409 capability_unavailable` with `details.blocked_reason: payin_fiat_refused`), and for a holder under the provider's deposits hold or one the provider does not approve (`409 virtual_account_holder_not_verified`) — the create's own checks. `503 temporarily_unavailable` when that read fails: nothing was reactivated. `422 destination_wallet_key_reported_lost` (`invalid_request`) when the account pays into a Swaps Wallet whose key was reported lost: refused before any provider call, nothing is reactivated, and the same request is refused again until support clears the record. The other wallet facts are refused under their own codes (`invalid_request`), with nothing reactivated: `422 destination_rail_keyless`, `422 destination_wallet_disabled`, `422 destination_wallet_signer_at_risk` and `422 destination_wallet_signer_unconfirmed`. `503 destination_risk_unverifiable` (`temporarily_unavailable`) when that check could not run: nothing was reactivated, retry after `Retry-After`. Those reasons are told only to the wallet's own holder: for any other Swaps Wallet the answer is `422 destination_not_accepted`, and every caller hears `503 destination_check_unavailable` when the wallet or key-mark lookup itself failed.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |
| `id` | path | yes | `string` | — |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The reactivated virtual account. | `application/json` [VirtualAccount](/reference/~schemas.md#virtualaccount) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-a-virtual-accounts-deposit-history-dark-flag"></a>
## `GET /wallet/virtual_accounts/{id}/history` — List a virtual account's deposit history — dark-flag

**Operation ID:** `wallet.virtual_accounts.history.list`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.read` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

"Money in" rows for one virtual account — a pull read over `virtual_account.deposit_received` (RESOURCE-MODEL §3); the real push producer is the Bridge webhook. One row per deposit: Bridge's lifecycle steps of one deposit collapse into one row whose `amount` is the settled step's (`payment_processed`) when present, else the received step's (`funds_received`), never a sum, and whose `occurred_at` is the first recorded of those steps, so a deposit that settles keeps its place and an issued `next_cursor` stays valid. A deposit not yet reported received is not listed. Newest first.
The `amount` is that one step's own value and currency, as the provider reports them. The settled step is net of the provider's fees, so a row's value can change on the same row, at the same `occurred_at` and cursor position, when its deposit settles: Bridge documents `usd` on both steps, so a deposit received as 120.00 USD reads 116.55 USD once settled. The currency is the step's own; today the provider reports the source currency on both steps. Never label the account by a row's currency, and never sum rows across a settlement.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |
| `id` | path | yes | `string` | — |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A page of deposit-received rows for this virtual account, one per deposit. | `application/json` [VirtualAccountHistoryList](/reference/~schemas.md#virtualaccounthistorylist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="build-an-unsigned-same-chain-conversion-available"></a>
## `POST /wallet/conversions` — Build an unsigned same-chain conversion — available

**Operation ID:** `wallet.conversions.create`

> **Status:** Available · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.write` · **Test mode:** unavailable · **Money boundary:** per-transaction human confirmation required · **Non-custodial:** response carries unsigned steps — Swaps never signs

Money boundary (RESOURCE-MODEL §0.10, §0.11) — per-transaction human confirmation is required. The API never signs: `steps[]` is an ordered list of unsigned calls (an `approve` before a `swap`, when the DEX needs one) the holder's own passkey signs; nothing moves until then. `swap-execution` builds a transaction, it never sends one. Testnet has one token, so a conversion is honestly unavailable there.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [ConversionCreateRequest](/reference/~schemas.md#conversioncreaterequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `201` | The built conversion, with unsigned steps to sign. | `application/json` [Conversion](/reference/~schemas.md#conversion) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="track-a-conversion-available"></a>
## `GET /wallet/conversions/{id}` — Track a conversion — available

**Operation ID:** `wallet.conversions.get`

> **Status:** Available · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.read` · **Test mode:** unavailable

Read a conversion's tracked on-chain leg by its `id`. Never claim the swap executed before the chain says so.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `id` | path | yes | `string` | The conversion's `id` (`cnv_`-prefixed — A1-6, RESOURCE-MODEL §0.2). A bare, un-prefixed `tracking_id` is also accepted for one version (`claude/dashboard-v2`'s `WalletConvertScreen`/`useWalletConversions` already call this route with the raw `tracking_id`, not yet the prefixed `id`) — see `Conversion.tracking_id`. |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The tracked conversion. | `application/json` [Conversion](/reference/~schemas.md#conversion) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="record-a-conversions-source-transaction-available"></a>
## `POST /wallet/conversions/{id}/source_tx` — Record a conversion's source transaction — available

**Operation ID:** `wallet.conversions.source_tx.set`

> **Status:** Available · **Callers:** agent (business key), dashboard session · **Scope:** `wallet.write` · **Test mode:** unavailable

Records the transaction hash the holder's wallet just broadcast for this conversion's swap step, moving it to `submitted` only once the chain itself confirms an exact-token, exact-amount transfer from the holder's own wallet to the swap step's own call target (the StablecoinDEX) — never a client assertion. Call once, immediately after signing. Re-sending the same hash is a safe no-op; a different hash for the same conversion is refused — never retry with a fresh hash to force it through.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |
| `id` | path | yes | `string` | The conversion's `id` (`cnv_`-prefixed — A1-6). A bare, un-prefixed `tracking_id` is also accepted for one version — see `wallet.conversions.get`'s own parameter description. |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [ConversionSourceTxRequest](/reference/~schemas.md#conversionsourcetxrequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The conversion, updated to `submitted`. | `application/json` [Conversion](/reference/~schemas.md#conversion) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-convertible-token-pairs-dark-flag"></a>
## `GET /wallet/conversion_pairs` — List convertible token pairs — dark-flag

**Operation ID:** `wallet.conversion_pairs.list`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** agent (business key), dashboard session, business key · **Scope:** `wallet.read` · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

L2-4 (R13-wallet §2 W-G10) — the mainnet Swaps-dollar token set's convertible pairs, each with a live liquidity flag from the same on-chain DEX probe `conversions.create` itself calls. Discovery data, not account-scoped; `security` marks auth optional (the router still requires a credential today — see `deposit_routes.list`'s own identical, already-documented gap — the output never varies by caller either way).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The convertible token pairs. | `application/json` [ConversionPairList](/reference/~schemas.md#conversionpairlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |
