# Changelog

One entry per `Swaps-Version` date. Nothing lands here before its contract test passes (API-CANON §12) — mirrors `docs/api/CHANGELOG.md` in the repository, the source of record.

## Unreleased — align public integration guidance with the enabled resource API

The public MCP page, its Markdown companion, localized installation guidance and generated
LLM manifests now describe `/v1` as enabled, subject to scopes, account capabilities and
resource availability, and point to the live documentation host. Test mode remains limited to
the operations listed in [Test mode](https://docs.swaps.app/test-mode); no complete provider
sandbox or completed rollout acceptance is claimed. Published npm route tools remain read-only;
hosted account tools preserve per-transaction human confirmation and signing requirements.
This corrects documentation only: no API, SDK, tool schema or `Swaps-Version` change.

## Unreleased (2026-10-05) — one login, one account: `POST /v1/accounts` answers `409`

`POST /v1/accounts` no longer opens a second account under the same login. A login that already owns a live account gets `409 conflict`, code [`account_already_exists`](/errors#account_already_exists), and nothing is created. Every signed-in login is given its account on its first request without `Swaps-Account`, and only the owner of the live account a request acts on gets past the earlier refusals, so today a request that gets past them always gets this `409` and never a new account. The wallet, verification, payment links, bank details and transactions belong to the login rather than to an account, so a second account would promise a separation that does not exist. The reference marks `accounts.create` as not open today. Every earlier refusal still comes first and is unchanged: `403` for a business key or a `member`, `503` for a test-mode session, `400` for a malformed body or an unrecognised `country`. `GET /v1/accounts` and the `Swaps-Account` header are unchanged. Separate accounts under one login, each with its own verification, are planned for a later release. No `Swaps-Version` change.

## Unreleased — explicit acceptance of legal revisions

Adds a disabled-by-default registry and server receipt for a published legal revision, with
separate publication, acceptance, effective and per-action requirement dates. No legal release,
mailing or enforcement date is activated by this change. Selected new live operations can return
`409 terms_acceptance_required` (`capability_unavailable`, `details.revision`/`details.action`)
or `503 legal_acceptance_unavailable` (`temporarily_unavailable`, `Retry-After`). People accept
through an authenticated Swaps screen, with owner authority required for account terms; personal
wallet intents require their own receipt. Keys and agents cannot mint receipts. Existing
completed idempotent responses, reads, cancellations and recovery retain their existing paths.
No `Swaps-Version` change. See [Errors](https://docs.swaps.app/errors#terms-acceptance).

## Unreleased — save a destination the recipient submitted as their default (payroll)

`PayrollRecipient` gains `pending_destination`: a destination the recipient saved through their payout link that is not their default yet (masked; `null` when nothing waits). New operation `POST /v1/payroll_recipients/{id}/adopt_pending_destination` (`payroll.write`, `Idempotency-Key`) saves it as the default, bound to the `submitted_at` you showed (`expected_submitted_at`), and returns the recipient plus `updated_run_items` / `skipped_run_items` for their waiting rows in unapproved runs. New error codes: `pending_destination_changed`, `no_pending_destination` (`409`), and `destination_rail_keyless` (`422`) — the last one also replaces the generic `invalid_request` code payroll approve, execute and the recipient's own destination write answered for the same refusal. On `409 pending_destination_changed` the recipient's default is unchanged; in a rare race, rows the call already filled with the destination you confirmed keep it. Additive; no `Swaps-Version` change.

## Unreleased — fund a payout from the Swaps wallet balance

`POST /v1/payouts/{id}/funding_instructions` takes an optional body: `source_chain` (chosen at funding, fixed afterwards) and `funding_source: swaps_wallet`, which prepares one unsigned send from the payer's own wallet to exactly the deposit address — the payer signs it with their passkey. New read: `GET /v1/payouts/{id}/wallet_funding_quote`. `GET /v1/capabilities?product=payouts` lists `funding_sources`. Additive; an absent body behaves exactly as before. Off until enabled per account.

## Unreleased — naming freeze

- Operation ids follow one rule, `<resource>.<verb>`, and something under a parent is part of the resource name. For example, `payouts.list_events` is now `payouts.events.list`, and `screenings.create_report` is now `screenings.reports.create`. Paths are unchanged. See [Conventions](/conventions#naming).
- The screening scopes are now `screenings.read` / `screenings.write`. A key created with the old `screening.*` names keeps working until the next `Swaps-Version` date.
- The `me` alias on `/customers/{id}` is deprecated. Use your `cus_…` id instead.
- Payouts and capabilities now return UK Faster Payments as `faster_payments` instead of `fps`.

## Unreleased — hosted MCP and the OpenAPI contract withdraw the advertised OAuth authorization server

Swaps operates no public OAuth authorization server. `/.well-known/oauth-authorization-server`
and `/.well-known/oauth-protected-resource` answer `404`; the OpenAPI contract's `agentOAuth`
scheme and every MCP discovery document that named OAuth 2.1 + PKCE are corrected to advertise a
business key (`sk_live_…`/`sk_test_…`) as the sole agent/MCP credential. See
[Authentication](/authentication). No credential class that worked before this entry stops
working — this corrects what was advertised, not what was enforced.

## Unreleased — truthful quote projection

- `rate` (the winner, every `offers[]` entry, every `providers[].offer` echo, and `route_facts.best_price.rate`) is now derived server-side from each offer's own `final_in` ÷ `final_out` decimal values, computed **before** either is rounded into the published `Money` fields — always `from_asset` per `to_asset`, identically for every provider on both `buy` and `sell` — never a provider's own field, whose basis previously differed by provider and by side. Because the derivation runs pre-rounding, `rate` × the published `final_out` amount can differ from the published `final_in` amount in the last digit(s) when an asset's published decimals are coarser than the server's internal precision — treat that as approximate, not an exact identity. Treat `rate` as informational only, never for ranking: compare by `final_out` for a `from_amount` request, or by `final_in` for a `to_amount` request — but only among offers whose `final_out` matches the amount you requested; not every provider honors an exact-output target, so a differing `final_out` means that offer bought a different amount and its `final_in` is not comparable this way. A non-winning offer whose facts cannot produce a usable rate is omitted rather than failing the whole response.
- Transak quote execution requires an explicit crypto network. When no usable offers remain for this reason, `409 capability_unavailable` preserves `QUOTE_NETWORK_REQUIRED` and names the missing directional network field.
- Winner and per-provider offers add optional `payment_method`, preserving the actual method with its `offer_id`. Unknown methods remain absent; inconsistent methods for the same winning offer are rejected. This additive field does not change `Swaps-Version` or grant execution eligibility.
- `POST /v1/quotes` exposes the winning `offer_id` and safe terminal provider outcomes, preserving success across payment methods. Route facts are projected from existing evidence.
- Readiness and status come from validated provider expiry; malformed upstream facts return an explicit unavailable response. Public cache hits retain their original fetch time as indicative prices.
- Authentication remains required on `/v1/quotes`. Test callers are refused before provider dispatch until sandbox routing is implemented. Order creation remains proposed.
- `GET /v1/capabilities?product=buy_sell` now publishes fresh-snapshot defaults (`primary_method`, optional `resolved_limits`) and a bounded coverage grid carrying `fiat` and `direction`; it never creates a quote or starts provider work. A stale or cold route snapshot returns `503 temporarily_unavailable`.

## 2026-09-05 — K1: the `/v1` gateway skeleton

- New edge function `supabase/functions/api-v1` behind a global kill switch (`feature_flags.api_v1`, **off** by default) — this release changes no live behaviour until an operator enables it.
- First two live operations: `GET /v1/account`, `PATCH /v1/account`, plus the new `GET /v1/accounts` and `POST /v1/accounts` (one login, many accounts). `account.get` / `account.update` flip from `proposed` to `available`; `accounts.list` / `accounts.create` are new and `available` from the start.
- New tables `accounts`, `account_members` (backfilled 1:1 from every existing account) and `api_idempotency_keys` (the `(key, account, route)` idempotency store, 24-hour TTL sweep).
- Three route-resolution gates land: fail-closed global and per-resource kill switches, business-key and bearer auth classes with fail-closed scopes, and per-class rate limiting on the distributed limiter.
- `Account.id` now carries the `acct_` prefix (see [Conventions](/conventions)).

## 2026-09-04 — Contract v1 (gate [A1])

> **Corrected 2026-09-16**
>
> The "OAuth 2.1 + PKCE with CIMD" auth class this entry announced was never built and is now withdrawn (decision C4-D32) — see [Authentication & keys](/authentication) and the entry above. Two live credential classes remain: business key and dashboard session, plus the public capability token.

- Resource-shaped `/v1` on `api.swaps.app`: payment links, clients, products, payments, subscriptions, payouts, payroll, wallet, quotes, orders, capabilities, eligibility, customers, account, activity, events, webhook endpoints, address book, screenings, traces, credits, API keys.
- ~~Three auth classes (business key, OAuth 2.1 + PKCE with CIMD, public capability token); the dashboard is a first-party OAuth client.~~ Superseded above — see the 2026-09-16 correction.
- `Money` object, `Idempotency-Key`, cursor pagination, one error envelope, `Swaps-Version` date header, `livemode` on every object, test mode by key prefix.
- The status of every operation is stated in the reference (`available` · `dark-flag` · `proposed`); non-available operations are documented with their reason and are not callable.
- Business-key scopes fail closed: a key with `scopes` null or empty now denies every action instead of granting all of them.
- Usage-log KPI columns and distributed rate limiting land on the usage-tracking table, feeding the [platform KPIs](https://github.com/swapsapp/swaps/blob/main/docs/api/KPI.md).

Next: [Get started](/get-started) to build against the current contract · [Providers & coverage](/providers-coverage) for what's live right now, rail by rail.
