# Capabilities — Swaps API

What this account can execute right now, rail by rail, and public eligibility by country.

**Jump to an operation:**

- [GET /capabilities](#what-this-account-can-execute-right-now-rail-by-rail-available)
- [GET /capabilities/buy_sell](#buy-or-sell-coverage-by-country-without-signing-in-dark-flag)
- [GET /capabilities/buy_sell/countries](#how-many-ways-to-pay-or-get-paid-each-country-has-without-signing-in-dark-flag)
- [GET /eligibility](#which-products-this-country-and-customer-type-can-likely-use-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="what-this-account-can-execute-right-now-rail-by-rail-available"></a>
## `GET /capabilities` — What this account can execute right now, rail by rail — available

**Operation ID:** `capabilities.get`

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

One resource, five `product` projections (`payment_links`, `payouts`, `payroll`, `buy_sell`, `wallet_bank` — D-23, D-34), behind the same authentication boundary. Payment and banking projections resolve the calling account's verification and eligibility facts. The `buy_sell` projection instead combines provider lifecycle and runtime switches with route coverage; it does not establish the caller's provider KYC eligibility. `buy_sell` also takes `country`, `method` and `provider` to narrow the coverage grid. Its defaults and coverage cells come from one fresh route snapshot; this endpoint does not quote, reserve a price, or promise a first quote. Call it before quoting or offering anything — it reports availability only and authorizes nothing; the server re-checks at create and at the money boundary (RESOURCE-MODEL §0.10). `verification_checked_at` names when the account's Bridge verification facts behind `corridors` were checked; with `verification=snapshot` they come from Swaps' stored check while it is no more than 24 hours old (no Bridge call), and a live read that cannot reach Bridge answers `503`, never the older status. **BL-60 (C4-D37)**: for `product=payroll`, `funding_currencies[]` additionally publishes, per fiat currency, whether Bridge can issue a FUNDING account for THIS account today — COP stays a selectable run currency (C4-D37) even though it answers `funding_account_available: false` and, since 13.10, has no payout corridor either. **Payroll corridors (13.10)**: `corridors[]` carries ONE `bank_bridge` corridor, USD — bank payouts exist for USD runs only (`bank_payout_fx_unsupported` for every other run currency, PAYROLL-CANON §1); a run in EUR, GBP, BRL or MXN is paid to USDC wallets where the source-currency allowlist (`PAYROLL_BRIDGE_CRYPTO_WALLET_SOURCE_CURRENCIES`, runtime config — PAYROLL-CANON §4) admits the run currency, which this projection does not list per run currency. **Fix round 1 (#1/#3)**: a Bridge funding rail existing is not sufficient — an unverified account reads `funding_account_available: false, reason: 'verification_required'` for every fundable currency while the account's `bank_bridge` corridor (`corridors[]` above; its KYC + `base` status is the gate every payroll funding currency shares) has not resolved to `available` yet; `corridors[]` and `funding_currencies[]` answer different questions (paying a recipient by bank vs. funding a run in a currency) and must never be read as the same fact. **PAYROLL-CRYPTO-1**: each `funding_currencies[]` entry also carries `crypto_funding` — whether a run in that currency can be funded with USDC sent to the employer's Bridge payroll wallet (and, when it can, which asset on which chain), gated by the same switch the funding service reads and never `available` while the currency itself cannot be funded. **K6 correction**: this operation requires a credential — an anonymous, account-free public claims projection (no entitlement, no route id, no flag, no preview row) is a DIFFERENT operation (a public corridor/rail grid with no caller identity behind it at all) and is not this one; the router has no optional-auth route class to serve it from yet. If a public projection is wanted, it should be specified and built as its own path, not folded into this operation's `security`. **BL-40**: for `payment_links`/`payroll`/`wallet_bank`, an account that already has a Bridge customer but whose live status read failed answers `503 temporarily_unavailable` for the whole projection — never a fabricated `not_started` on every corridor. **NB-5**: a `buy_sell` answer with `degraded: true` means the route snapshot could not be obtained (unreachable or no fresh lease) — report that the check failed and offer a retry, never report that no route exists. **A4-FIX-6** — Test mode: unavailable. These projections use live verification, eligibility or route-snapshot sources, with no supported test-mode projection. The router refuses a test-mode caller (`503 temporarily_unavailable`, side-effect free, no `Retry-After`) and the handler refuses it a second time before any read. It is never forwarded to the dev project. **CP-T2**: for `product=payment_links`, the `crypto_tempo` corridor carries `crypto_only` — whether this account can activate a crypto-only link (settled to its own Swaps Wallet) without any Bridge customer, and the first gate that blocks it. A read failure behind it answers `503 temporarily_unavailable` for the whole projection. **#3932**: the `crypto_relay` corridor reads `available` only while the `crypto_tempo` corridor does — Relay delivers on the Tempo leg and the payer page never offers it otherwise; its `max_amount` (the per-invoice cap) is published even while it reads `not_enabled`. **WALLET-VA-OVERRIDE-1**: for `product=wallet_bank`, the `wallet_bank_virtual_account` corridor names why it is not `available` in one order, the order `POST /wallet/virtual_accounts` refuses by — first what no step of the person opens, each `not_enabled` with `action: none`: `owner_only`, `rail_not_enabled`, `verification_closed` (with `detail_code: verification_attempts_exhausted` when the account has used every verification attempt), `payin_fiat_refused`, `endorsement_not_requestable`, `route_unavailable`, `collection_account_uses_currency`; then the person's own steps: a verification step with no `blocked_reason`, `payin_fiat_pending` (`gathering_no_path`), and last `wallet_not_found`. `CapabilityCorridor.blocked_reason` defines each.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `product` | query | yes | `payment_links` \| `payouts` \| `payroll` \| `buy_sell` \| `wallet_bank` | — |
| `country` | query | no | `string` | `product=buy_sell` only — narrows the coverage grid to one country. This filter never resolves residency or personal KYC. |
| `method` | query | no | `string` | `product=buy_sell` only — narrows the coverage grid to one payment method. |
| `provider` | query | no | `string` | `product=buy_sell` only — narrows the coverage grid to one provider. |
| `VerificationRead` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The capability snapshot for this product — public or authenticated projection depending on the credential presented. | `application/json` [Capabilities](/reference/~schemas.md#capabilities) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="buy-or-sell-coverage-by-country-without-signing-in-dark-flag"></a>
## `GET /capabilities/buy_sell` — Buy or sell coverage by country, without signing in — dark-flag

**Operation ID:** `capabilities.buy_sell.get`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** public token · **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.

The same account-independent projection `GET /v1/capabilities?product=buy_sell` returns — provider status, the country's defaults (fiat, methods, families, limits) and the coverage grid (fiat → method → provider → state), all from one fresh route snapshot — served to an anonymous caller (D-11). PUBLIC and UNAUTHENTICATED: no key or session is read. It is a separate path only because one operation cannot sit in both of the router's route tables; without `side` (or with `side=buy`) the body is the same `Capabilities` object for the same `country`, `method` and `provider`. **Sell (`side=sell`, this path only):** the body carries `side: sell`, `sell_defaults` and `sell_coverage` instead of `defaults` and `coverage` — never both. `sell_coverage` has one cell per payout method and fiat the country can be paid in by Bridge's residency rule (Bridge is the only provider that sells today), its state from the same route truth, and per cell the assets that may be sent — each with its network, its state and, when selectable, Bridge's own minimum and the single-order maximum in that asset (`min`, `max`). Bre-B (COP) is `blocked` (`route_disabled`): Bridge prices it, a sell order cannot execute it. USDT and USDB are `blocked` (`policy_blocked`) for a country in the EEA. `sell_defaults` names the first-screen currency (the country's own, else its fallback, else USD, EUR or GBP — `fiat_reason`), USDC on Base, the first selectable payout method and that method's USDC limits; `provider_fees` is empty (no pass-through fee on a sell). `country` is the country the person picked, defaulting to `US` when omitted, exactly as on the authenticated projection — it is not residence and it does not establish anyone's provider verification. It reports availability and authorizes nothing: an estimate, a quote and an order each re-check. A `degraded: true` answer means the route snapshot could not be read — report that the check failed and offer a retry, never that no route exists. `Cache-Control: public, max-age=0, must-revalidate` (`private, no-store` on a degraded answer). A non-degraded answer may be served from the Swaps edge cache for up to 60 seconds, never past its snapshot's expiry: it can be up to a minute old, repeat another caller's `x-request-id` and still list a provider switched off within that minute. **Protection:** a persistent per-client-IP limit (`429`), and a client IP on the Swaps block list is refused `403 ip_not_allowed`. **Switch:** `dark-flag` — the `api_v1.capabilities_public` kill switch answers `503 temporarily_unavailable` while it is off, which it is until the founder turns it on at launch. **No MCP tool:** an agent reads coverage through the authenticated `list_capabilities`; the public review surface stays as it is (API-CANON §13).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `country` | query | no | `string` | The country the person picked, ISO 3166-1 alpha-2 from the supported catalog; `US` when omitted. Never residence, never personal verification. |
| `method` | query | no | `string` | Narrows the coverage grid to one payment method. |
| `provider` | query | no | `string` | Narrows the coverage grid to one provider. |
| `side` | query | no | `buy` \| `sell` | `buy` (the default) or `sell`. `sell` answers `side`, `sell_defaults` and `sell_coverage` in place of `defaults` and `coverage`. Any other value, or the parameter given twice, is `400` (`param: side`). |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The coverage snapshot — for a buy, the same body the authenticated `product=buy_sell` projection returns; for `side=sell`, the sell projection. | `application/json` [Capabilities](/reference/~schemas.md#capabilities) |
| `400` | — | `BadRequest` |
| `403` | — | `Forbidden` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="how-many-ways-to-pay-or-get-paid-each-country-has-without-signing-in-dark-flag"></a>
## `GET /capabilities/buy_sell/countries` — How many ways to pay or get paid each country has, without signing in — dark-flag

**Operation ID:** `capabilities.buy_sell.countries.list`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** public token · **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.

One row per country of the supported catalog: the paying currencies that have at least one way to pay and, per currency, how many distinct payment methods are selectable for buying (`ways`) — the same count `GET /v1/capabilities/buy_sell?country=XX` gives for that country, from the same stored route snapshot (D-93). Use it to label a country list; use `GET /v1/capabilities/buy_sell` for the country the person picks, which also carries methods, providers and limits. A row is `status: ok` only when it was built from a fresh snapshot; otherwise it is `unknown` and carries no currencies and no count — never read `unknown` as "not available". A country with `status: ok` and no currencies has no way to pay today. Computed from the stored snapshots alone: no route-truth read, no quote, no provider call. **Sell (`side=sell`):** the same rows from the stored sell snapshots — the currencies a person can be paid out in and, per currency, how many distinct payout methods are selectable with at least one asset that may be sent (`sell_coverage` of `GET /v1/capabilities/buy_sell?country=XX&side=sell`). It reports availability and authorizes nothing: an estimate, a quote and an order each re-check. PUBLIC and UNAUTHENTICATED: no key or session is read. `Cache-Control: private` for at most 60 seconds and never past the earliest snapshot's expiry (no shared cache: every answer carries its own `x-request-id`). `no-store` when no row is `ok`. **Protection:** the same persistent per-client-IP limit as `GET /v1/capabilities/buy_sell` (`429`), and a client IP on the Swaps block list is refused `403 ip_not_allowed`. **Switch:** `dark-flag` — the same `api_v1.capabilities_public` kill switch answers `503 temporarily_unavailable` while it is off. **No MCP tool:** an agent reads coverage through the authenticated `list_capabilities`; the public review surface stays as it is (API-CANON §13).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `side` | query | no | `buy` \| `sell` | `buy` (the default) or `sell` — which side's ways are counted. Any other value, or the parameter given twice, is `400` (`param: side`). |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | Every catalog country, sorted by country code, each `ok` with its counts or `unknown`. | `application/json` [BuySellCountrySummaryList](/reference/~schemas.md#buysellcountrysummarylist) |
| `400` | — | `BadRequest` |
| `403` | — | `Forbidden` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="which-products-this-country-and-customer-type-can-likely-use-dark-flag"></a>
## `GET /eligibility` — Which products this country and customer type can likely use — dark-flag

**Operation ID:** `eligibility.get`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** public token · **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.

The compliance-navigator engine's own four statuses, verbatim — never collapse `likely_available` and `conditional` into one `available` (RESOURCE-MODEL §2.4). PUBLIC and UNAUTHENTICATED (L3-2): resolved off the router's public-token table with no caller identity at all (RESOURCE-MODEL §0.9), the same class `payment_sessions.get` uses. The public projection takes only `country` + `customer_type`. This PR drops `monthly_volume_band`/`purpose`/`regulated_activities` from this operation ENTIRELY, not just to "authenticated callers only": D-16 already forbids them on any anonymous path, this operation has no other path, and `capabilities.get`'s own description already names the structural reason an authenticated variant can't be folded into this one's `security` — "the router has no optional-auth route class to serve it from yet... it should be specified and built as its own path." A richer, authenticated `eligibility` projection accepting those three fields is a real future operation once that route class exists; it is not this one. `payroll` is one of the five published products (the compliance-navigator engine's `exchange`/`wallet`/`payment_links`/ `payouts`/`payroll` — NOT `/v1/capabilities`'s unrelated `payment_links|payouts|payroll|buy_sell|wallet_bank` product enum, a different question for a different, authenticated caller). `limits` is populated only for `payouts`, and only for a country whose currency AND whose payout rail this catalog actually covers (a real per-currency corridor minimum, published as `Money` — fix round 1, P1: a currency match alone is not a country match, e.g. Zimbabwe uses USD but has no US-domestic ACH/wire rail; fix round 2, P2 corrected the EUR corridor's country scope again — Monaco/San Marino/Vatican City are EUR-currency SEPA members the EEA-only gate wrongly excluded — every other product omits `limits` rather than publish an approximate or fabricated figure, and `limits` itself never carries a `max`: no producer publishes one today (fix round 2, P3). `limits.confidence` is `exact` only for a hard-rule match (EEA membership, or `country === 'US'`); Monaco/San Marino/Vatican City's match is this operation's OWN widening onto a rail with no country dimension in its payout SSOT, so it reads `approximate` there, never the same `exact` an EEA country gets for the identical `eur_sepa` corridor. `requires_bridge_verification` (fix round 1, P1 as `requires_verification`; renamed fix round 2, P2 — the bare name over-claimed for `exchange`, whose `false` means only "no Bridge customer needed," not "no KYC anywhere") marks the three products gated on a Bridge customer/endorsement the caller may not have yet (`payment_links`/`payouts`/`payroll`) — those three also lead `reasons[]` with `bridge_customer_missing` and, for an EEA country, add `eea_verification_required` to both `reasons[]` and `blockers[]`; `exchange` also publishes `eea_verification_required` in `reasons[]` for an EEA country as of fix round 2, P3 (matching the canon engine's own code for that branch), even though it needs no Bridge customer and so never gets a `blockers[]` entry for it; never conflate a Bridge-gated `likely_available` with `wallet`'s true, unconditional one. `notes[]` carries the fact `reasons[]`' shared code cannot: `exchange` always publishes `dex_no_kyc` + `fiat_provider_kyc`, and adds `eea_assets` for an EEA country — the MiCA asset restriction its `eea_verification_required` reason code alone would otherwise conflate with the other three products' real Bridge-verification gate. `disclaimers[]` (fix round 1, P2) mirrors the engine's own hedging (`not_advice` always; `approximate`/`eea`/`sanctioned`/`unknown_country` per branch, spelling corrected fix round 2, P3) — this is the one operation whose `x-swaps-status: dark-flag` exists because public capability claims need the founder's legal sign-off, so the claims never ship without the hedges that go with them everywhere else this engine speaks. `x-swaps-test-mode: unavailable` (A1-3, 2026-09-22): every `available`/`dark-flag` operation in this document was corrected to `unavailable` because K11's cross-project test-mode dispatch does not exist yet (`apps/docs/pages/test-mode.mdx`) — this operation is a strong first K11 candidate to restore, since it takes no token/key of any kind (`security: []`) and so has no `sk_test_`/live distinction to make, unlike its two public-token siblings (`payment_sessions.get`/`payroll_recipient_sessions.get`, fix round 1, P3 note) whose resolved session DOES belong to a `livemode`-bearing resource, but the claim stays `unavailable` here too until K11 names it in its evidence list with its own test, exactly like every other operation below. CORS: the 200 response carries a wildcard `Access-Control-Allow-Origin: *` for a header-free cross-origin GET (fix round 1, P2) — this is narrower than "any browser page can call this" (corrected fix round 2, P2): a caller that sends a custom request header (e.g. `Swaps-Version`) triggers a CORS preflight this router does not yet answer with a public wildcard, and every refusal this operation can return (400s, and the `503` while its flag row is unseeded) still carries an origin-specific ACAO. Fixing both needs a router-level change outside this operation's own file; tracked as a follow-up, not attempted here.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `country` | query | yes | `string` | ISO 3166-1 alpha-2 country code, matched against this exact pattern server-side — no leading/trailing whitespace is trimmed (fix round 1, P3: a previous handler bug trimmed before validating, accepting an input this published pattern refuses). |
| `customer_type` | query | yes | `individual` \| `business` | — |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The engine's answer for this country and customer type. | `application/json` [Eligibility](/reference/~schemas.md#eligibility) |
| `400` | — | `BadRequest` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |
