# Fynex API Changelog

This page tracks API-visible changes to Fynex public APIs: Payments and
Billing. Entries identify the affected surface when it is not clear from the
endpoint name. Internal work with no effect on what you can call or what you
get back (infrastructure, internal tooling, refactors) is not listed here.
Some entries below note when a change only affects certain accounts or
environments — check those notes if a change doesn't seem to apply to you.

---

## API lifecycle and rate-limit contract

The Payments API's standard seller budget is **100 requests per hour**; a Demo
account has a separate budget of **200 requests per minute** (since 2026-10). When
the limiter is active and available, authorized responses report the live
allowance with `X-RateLimit-*` and the structured `RateLimit` /
`RateLimit-Policy` response headers; the policy field names the active window
in seconds. The response values are authoritative for your account and
environment. During a temporary limiter outage, requests are allowed without
rate-limit headers rather than treating their absence as extra capacity.

The versioning and deprecation policy is part of the public contract: a major
version remains supported for at least 12 months after its successor becomes
generally available, and a planned removal has at least 90 calendar days of
notice. The [API overview](https://api.fynex.ai/payments-api/v1/docs) defines
the OpenAPI and HTTP signals used for a deprecation; this changelog records
its migration guidance and removal date.

---

## 2026-10

### Breaking

- **An `sk_` API key can no longer list, create, regenerate or revoke API keys.**
  `GET`/`POST /api/v1/developer/api-keys`, `POST /api-keys/{id}/regenerate` and
  `DELETE /api-keys/{id}` now answer `403` to an `sk_test_` or `sk_live_` key, so a leaked key
  cannot mint a replacement that outlives its own revocation. Manage keys in the dashboard under
  Integration → API keys.

- **Replaying `POST /initialize-payment` after a refused card returns the same `422`, not `200`.**
  Replaying the `Idempotency-Key` of a payment refused for an expired card (`1007`) or an
  unsupported card brand (`1008`) used to return `200` with the failed payment in the body; it now
  returns the same `422` and `code` the first attempt got. If you read the failed payment from a
  replay, use `GET /payments/{ref}` instead.

### Added

- **Restricted API keys.** `POST /api/v1/developer/api-keys` takes `permissions`, a list such as
  `["payments:read", "billing_usage:write"]`; the key then reaches only the operations those
  permissions cover and is refused the rest with `403` `insufficient_scope`, naming what is
  missing. Each operation states what it needs in `x-fynex-permissions`, and `GET /account`
  reports the calling key's `access` and `permissions` under `apiKey`. Keys created without
  `permissions` keep full access.

- **API keys can be named, can expire, and rotate without an outage.** `POST
  /api/v1/developer/api-keys` takes an optional `name` and `expiresAt`; a key used after its
  `expiresAt` is refused with `401` `api_key_expired`, and the key list reports it as
  `expired`. `POST /api-keys/{id}/regenerate` takes an optional `overlapHours` (up to 168) that
  keeps the old key working beside its replacement; without it the old key stops at once, as
  before. The replacement keeps the old key's name, permissions and lifetime — a 30-day key
  regenerated today expires 30 days from now — unless `expiresAt` sets another. Existing keys have
  no name and never expire.

- **An MCP endpoint for AI agents is in preview** and not yet available on production.

- **Demo accounts can raise recurring invoices with no request to Fynex.** Recurring invoicing
  is enabled per account and currency, and a demo account had to ask for it, so a test-clock
  advance on a fresh sandbox renewed the subscription and issued nothing. A demo account is now
  enabled from the start; an explicit setting still decides, and live accounts are unchanged.
- **MCP tools `create_usage_metric` and `set_usage_price`.** Registering a metric and pricing it on
  a contract were REST-only, so an agent could create a subscription but not meter it.

- **`Idempotency-Key` on every create route that lacked one.** `POST /payees`,
  `POST /payees/setup`, `POST /payees/{id}/payout-methods`, `POST /webhooks` and
  `POST /split-rules` accept it, optionally. A retry with the same key and body answers with what
  the first request created instead of a second object; for a webhook that includes its signing
  secret. The same key with a different body answers `409` `idempotency_key_reused`. Requests
  without the header behave as before.

- **Split webhook: `SplitExecuted`.** A settled payment's split now records an event when it
  posts, so `GET /events` shows the payment, its split and its payouts in one feed. The payload
  carries the execution, the payment's `paymentId`, the rule, each payee's share, fee and net and
  the wallet credited, the total fee and the remainder, in the names and values of
  `GET /payments/{paymentId}/split-decisions`. A payment with no active rule, or nothing to divide,
  sends nothing. Opt-in, like the payout family: an endpoint receives it only when its
  `eventTypes` names it.

- **Payout webhooks: `PayoutCompleted` and `PayoutFailed`.** A payout that reaches `completed` or
  `failed` now sends a signed event whose payload carries the fields `GET /payouts/{id}` returns,
  including `failureCode` and `failureMessage` on a failure. The family is opt-in: an endpoint
  receives it only when its `eventTypes` names the events, so existing endpoints see no change. A
  per-request `webhookUrl` on `POST /payouts` is still validated only, not delivered to.

- **`POST /test/checkout/{sessionId}/complete` pays a Demo checkout without a browser.** It
  submits the session's own checkout page with the sandbox's frictionless Visa test card and the
  test customer data, runs 3DS and completes the payment, so an integration or agent that holds
  only a key can take a test payment end to end without card details passing through it. MCP
  tool `complete_test_checkout`. Demo accounts only (`403` `demo_only` on Live); repeating it
  answers `already_completed`. `POST /test/payments/{paymentId}/settle` now also answers
  `already_settled`, not `409`, for a Demo payment the automatic settlement got to first.

- **`POST /test/payments/{paymentId}/settle` settles a Demo payment.** The sandbox acquirer sends
  no settlement report, so a completed Demo payment never settled, split or credited a wallet
  unless someone pressed the dashboard's button. This call does the same with an API key: the
  payment settles, its split runs and the wallets are credited. Demo accounts only (`403`
  `demo_only` on Live); repeating it answers `already_settled`.

- **`GET /account` returns the account a credential acts for.** Its id, name, currency,
  `operationalMode` (`Demo` or `Live`) and the switches that change what the API accepts
  (`features.walletCreation`, `features.payeePayoutAutoFunding`, `features.refunds`). Call it to
  confirm Demo or Live before sending a test card.

- **Demo accounts: settled funds are withdrawable at once.** The T+N payout maturity hold no
  longer applies to a Demo account's settlement credits or to the legs its splits post, so a
  sandbox can take a payment, settle it, split it and pay a payee in one session. Live accounts
  keep the hold exactly as before.
- **MCP tool `create_payout`.** `POST /payouts` is now a tool too: it pays out from the wallet you
  name, which is how a payee is paid what a split credited its split wallet. `pay_payee` keeps
  funding the payee's cashout wallet from your main wallet.

- **`GET /events` lists what happened on your account.** Every event the webhook pipe records —
  payments, refunds, payment links, subscriptions, invoices, verification — in the same envelope a
  delivery carries, whether or not you have a webhook URL. Oldest first, filterable by `eventType`
  and an `occurredFrom`/`occurredTo` range; pass `nextCursor` back as `cursor`, and keep it to
  resume later.

- **`GET /payments` lists the account's payments.** Newest first, filterable by `status` (the
  `lifecycleStatus` values), a `createdFrom`/`createdTo` range, `currency`, an inclusive
  `amountMin`/`amountMax` range (major units, like `amount`; requires `currency`) and `payeeId` (the
  payments whose split credited that payee, once the split has run), with keyset paging
  (`cursor` / `nextCursor`). Each entry carries the fields of `GET /payments/{payment_id}`
  except `allowedActions`, `settlementStatus` and `promotion`.

### Changed

- **A Demo payment of up to £500 settles on its own, on production too.** A completed Demo
  payment in GBP, USD or EUR of at most 500 now settles within about a minute — its split runs and
  the wallets are credited — on `https://api.fynex.ai` as on staging. A bigger Demo payment is not
  settled automatically: call `POST /test/payments/{paymentId}/settle`. Live payments are
  unchanged.

- **Every error carries a `code`.** Each `4xx` and `5xx` body is now `{"error": "...", "code": "..."}`:
  the specific code where the API names one (as before), otherwise one derived from the status —
  `bad_request`, `not_found`, `conflict`, `unprocessable_entity`, `internal_error` and so on; see
  *Error format* in [Errors](https://docs.fynex.ai/payments-api/v2/docs#tag/errors). The `error` text is unchanged. A panic answers JSON
  `internal_error` instead of plain text, a refused checkout finalize says `payment_failed` for a
  decline and `bad_gateway` for a retryable fault, and a route refused to a self-serve sandbox
  says `not_available_on_sandbox`. Promotion and reCAPTCHA refusals repeat their `reason` as
  `code`.

- **Rate-limit refusals carry a `code`, and every `429` carries `Retry-After`.** A limiter's `429`
  body is now `{"error": "...", "code": "rate_limited"}`, and its fail-closed `503` carries `code`
  `temporarily_unavailable`; the `error` text is unchanged. `GET /billing-api/v1/invoices/export`
  refused because too many exports are running now sends `Retry-After: 5` with its `429`.

- **A checkout session or payment link no terminal can take is refused at creation with `422`.**
  `POST /checkout` and `POST /api/v1/payment-links` used to accept a currency the account has no
  terminal for (for example `GBP` on an account with only `EUR` terminals); the payment then failed
  with `failureCode` `1003` (`routing`) only after the customer opened checkout and pressed pay.
  Creation now answers `422` with `{"error": "...", "code": "currency_not_supported"}` and creates
  nothing. A session is accepted when any method the hosted checkout can offer for it would route,
  including a local payment method; replaying an `Idempotency-Key` that already created a session
  still returns it. A payment link is refused only when no terminal matches its currency, mode
  (live or test), payment type and country. See *Common 422 responses* in [Errors](https://docs.fynex.ai/payments-api/v2/docs#tag/errors).

- **Authentication failures are JSON with a `code`, like every other error.**
  A missing, unknown or inactive credential used to be answered with a `text/plain` body
  (`authorization token is required`, `invalid authorization token`, `seller account is not
  active`); it is now `application/json` — `{"error": "...", "code": "..."}` — with the same
  `error` wording and `code` `missing_api_key`, `invalid_api_key` or `account_not_active`. A `403`
  `forbidden` from a permission gate carries `insufficient_scope`, and every `401` carries
  `WWW-Authenticate: Bearer`. If your client parsed these bodies as plain text, read `code`
  instead; a substring match on the old wording still matches `error`.

- **A refused key says why, and an unknown route answers in JSON.** A real key used in the
  account's other mode (an `sk_test_` key after going Live) now answers `401` with `code`
  `key_environment_mismatch`, and a self-serve sandbox key after expiry `sandbox_expired`; both
  were `invalid_api_key`, and the `error` text still starts with `invalid authorization token`.
  A path no route serves under `/payments-api/v1` used to answer a plain-text `404 page not
  found`; every unknown route now answers `404` `{"error":"route not found","code":"not_found"}`,
  a wrong method `405` with `method_not_allowed`, and an operation requested under the
  `/payments-api/v2` documentation prefix is told that operations live under `/payments-api/v1`.

- **`POST /initialize-payment` answers `422` with a `code` for a refused card.**
  An expired card (`failureCode` `1007`) used to return `502` with a generic message, which read as
  "retry"; it now returns `422` with `{"error": "...", "code": "card_expired"}`. A card brand the
  seller's acquiring agreement does not accept (`1008`) already returned `422`, but without `code`;
  it now carries `"code": "card_brand_not_supported"`. The error body gains an optional `code`
  field; bodies without one are unchanged. If your client retried every `5xx` automatically, an
  expired card now stops at the first attempt — ask the customer for a different card and send a
  new `Idempotency-Key`. Provider declines are unchanged.

- **A bank's 3-D Secure refusal now carries the same code whichever way Fynex observes it.**
  When the customer completed 3-D Secure but their bank refused the authentication, a payment
  Fynex observed directly through the processor used to fail with `2002` (provider error,
  `retry: safe`), while the same refusal observed at `/finalize-payment` failed with `2001` and
  the description "Card authentication failed (3-D Secure)". When the bank's verdict is a refusal
  (`threeDResult` `N` or `R`), both now fail with `2001`, that description,
  `failureStage: authorization` and `retry: never` — the refusal is the bank's decision. A verdict
  of 3-D Secure *unavailable* (`U`) or an unanswered challenge on a failed session is no longer
  described as "Card authentication failed" on either path. A session that expires with the
  challenge unanswered is `3003` on both paths.

- **An abandoned 3-D Secure step now fails with `failureCode` `3003`.**
  When the customer was sent to 3-D Secure and the authentication session expired before they
  completed it, the payment used to fail with `2002` (provider error), `retry: safe` and
  `failureCategory: transient` — advice to replay the same `Idempotency-Key`, which returns the same
  expired payment. It now fails with `3003` (authentication not completed), `retry: fix_first` and
  `failureCategory: requires_change`. `status` is still `failed` and `failureStage` is still
  `checkout`; nothing was authorized. If you branch on `2002`, add `3003` and ask the customer to
  pay again. Payments that failed before this change keep `2002`.

- **An acquirer's card-brand refusal at authorization now fails with `failureCode` `1008`.**
  When the acquirer refuses a card because the seller's account is not configured for its brand,
  and does so at authorization rather than before it, the payment used to fail with `2001`
  (provider declined) or `2002` (provider error); it now fails with `1008` (card brand not
  supported) and `failureStage: authorization`, the code a refusal before authorization already
  carried. No funds move. On `POST /initialize-payment` such a refusal now answers `422` with
  `"code": "card_brand_not_supported"` where it used to answer `502`. Retrying the same card
  fails identically: ask the customer for a card of an accepted brand.

- **Payouts on a Demo account are approved automatically.**
  A Demo account's payout used to start `awaiting_approval` like a Live one and wait for Fynex;
  it is now `approved` at creation, at any amount and from any surface, so a test payout runs to
  completion unattended. A compliance hold (such as the marketplace vendor limit) still applies.
  Live accounts are unchanged.

- **Payout methods on a Demo account are approved at creation.** `POST /payout-methods` and
  `POST /payees/{payee_id}/payout-methods` (and a bank-routing change through `PATCH`) on a Demo
  account now answer `approvalStatus: approved` instead of `pending`, so a bank account you add can
  be paid out to without waiting for Fynex. When the account goes Live, every payout method nobody
  at Fynex reviewed returns to `pending` and must be approved again. Live accounts are unchanged.

- **Payee payouts on a Demo account fund themselves.** `POST /payees/{payee_id}/payouts` on a Demo
  account now tops up a short payee wallet from your main wallet (and creates the payee's cashout
  wallet if needed) without Fynex enabling auto-funding first. `GET /account` reports it as
  `features.payeePayoutAutoFunding: true`. Live accounts are unchanged.

- **A Demo account creates wallets without asking Fynex.** `POST /wallets`, `POST /payees/setup`,
  the payee payout that provisions a cashout wallet and `POST /vendors` no longer answer `403`
  `wallet creation is not enabled` on a Demo account, which moves no real money. `GET /account`
  reports `features.walletCreation: true` for every Demo account. On a Live account the switch is
  unchanged: off by default, Fynex turns it on; wallets made in Demo stay when the account goes
  Live.

---

## 2026-09

### Breaking

- **Announced: payment `status` switches to the lifecycle enum no earlier than 2026-12-28.**
  From that date `status` on `GET /payments/{payment_id}`, `POST /initialize-payment`,
  `POST /finalize-payment`, `POST /payments/{payment_id}/capture` and the `PaymentCompleted`
  webhook carries the seven lifecycle values below instead of the internal processing status.
  Nothing changes before then. To be unaffected, read `lifecycleStatus` (the new values) or
  `legacyStatus` (the current values) now — both are already present, see *Added*. The exact switch
  date is confirmed here at least two weeks ahead.

- **Announced: `captureMode` decides manual vs automatic capture, no earlier than 2026-12-28.**
  Today `autoSettlement` decides — or, when it is left out, your account's default — and
  `captureMode` is only recorded. From the switch date `captureMode: "manual"` or `"auto"` on
  `POST /initialize-payment` decides, `autoSettlement` becomes a deprecated alias, and a request
  whose two fields disagree (e.g. `autoSettlement: true` with `captureMode: "manual"`) is refused
  with `400`. A request that sends both consistently, or only `autoSettlement`, is unaffected. The
  exact date is confirmed here at least two weeks ahead.

- **`bcPaymentId` removed from the REST payout response**, effective 2026-09-02. Read
  `providerReference` instead — the supported field for this purpose, available since 2026-08-28.
  If your integration reads `bcPaymentId` from a REST payout response, that key is now absent from
  the JSON entirely (not `null`); switch to `providerReference`. This affects **REST only** — the
  GraphQL payout schema still exposes `bcPaymentId` unchanged for now.
- **Expired cards are now rejected before the provider is called**, with a dedicated
  `failureCode` (`card_expired`) and `failureCategory: requires_change` — previously this returned
  the generic provider-error code with `failureCategory: transient` and `retry: safe`, which could
  read as "try again." A new attempt with the same expired card will fail identically; the buyer
  needs a different card.

### Added

- **`PaymentCompleted` fires once per outcome, not once per payment.** A payment reported `failed`
  (or `cancelled` by an expired checkout) that its provider later proves was captured now sends a
  second `PaymentCompleted` with `lifecycleStatus: captured`; so does a capture the provider
  reverses. It used to be dropped as a duplicate, leaving your record on the first outcome while
  `GET /payments/{payment_id}` showed the new one. A repeat of an outcome already sent is still not
  re-sent. Each event has its own `eventId`.
- **`lifecycleStatus`, `legacyStatus` and `settlementStatus` on payment responses.** Every response
  that carries a payment `status`, and the `PaymentCompleted` webhook, now also carries
  `lifecycleStatus` — one of seven closed values: `pending`, `under_review`, `authorized`,
  `captured`, `refunded`, `failed`, `cancelled` — and `legacyStatus`, the current `status` value.
  `legacyStatus` is deprecated and stays for one notice period after the switch announced above.
  How far captured money has travelled — what `funds_in_flight`, `settled` and
  `deposit_confirmed` used to say — is the new `settlementStatus` on `GET /payments/{payment_id}`.
  GraphQL `GenericPayment` gains `lifecycleStatus` too, as the `PaymentLifecycleStatus` enum; its
  `status` stays the internal value and does not switch.

  | `legacyStatus` (current `status`) | `lifecycleStatus` |
  |---|---|
  | `draft`, `new`, `routed`, `provider_pending` | `pending` |
  | `compliance_review`, `provider_risk_review` | `under_review` |
  | `authorized` | `authorized` |
  | `provider_completed`, `funds_in_flight`, `settled`, `deposit_confirmed`, `refund_pending`, `refund_failed`, `refund_cancelled` | `captured` |
  | `refunded` | `refunded` |
  | `failed` | `failed` |
  | `cancelled` | `cancelled` |
- **`allowedActions` and `captureMode` on `GET /payments/{payment_id}`.** `allowedActions` lists
  what you can request on the payment next — `capture`, `partial_capture`, `refund`,
  `partial_refund` — by the same rules the capture and refund endpoints enforce: a listed action is
  accepted, an unlisted one is refused, and the array is empty when nothing is possible.
  `captureMode` (`auto` or `manual`) says how the payment is actually captured.
- **Split-decision records.** `GET /payments-api/v1/payments/{paymentId}/split-decisions` returns a
  decision record for every split evaluation on a payment — what each line contributed, which
  lines didn't fire and why, and which policy adjusted an amount. As with every other payment read
  on this API, `paymentId` in the path is your `externalOrderRef` from `/initialize-payment`, not a
  Fynex-issued id.
- **Split-rule operations in the OpenAPI document.** The ten `/payments-api/v1/split-rules`
  operations — create, list, get, get active, clone, delete, deactivate, `preview-activation`,
  `activate` and `calculate` — are now described in `/payments-api/v1/openapi.json`, under the
  `Splits` tag, with their request and response schemas and the statuses they answer. A client
  generated from the spec can run create → `preview-activation` → `activate` with no hand-written
  calls. The operations themselves answer as before, apart from the `remainderMinor` change below.
  The retired operations — `PATCH /split-rules/active`, `PUT /split-rules/active/lines`, `PUT` and
  `DELETE /split-rules/active/lines/by-payee/{payeeRef}`, and `POST /split-rules/{id}/schedule` —
  are **deliberately absent** from the document rather than published with `deprecated: true`: they
  answer only `409`, and a deprecated entry would still put them in every generated client.
- **`retry` and `failureCategory` published alongside `failureCode`.** A failed payment, refund, or
  the `PaymentCompleted` webhook now states whether re-sending the same request can succeed
  (`retry`: `safe`, `fix_first`, or `never`) and the shape of the failure (`failureCategory`:
  `hard`, `transient`, `requires_change`, `integration_error`, `cancelled`, or `unknown`).
  `integration_error` is worth watching for specifically — it means the request itself was wrong,
  not the payment method.
- **Webhook event subscriptions.** `eventTypes` can now be set when registering or updating a
  webhook to receive only the event types you choose; omitting it keeps receiving the full default
  set as before.
- **Marketplace vendor onboarding.** `POST /payments-api/v1/vendors` (plus list/get and a
  verification-link endpoint) lets a marketplace onboard the sellers it pays out to directly,
  including automated screening. A screened vendor can receive cumulative payouts up to a
  threshold before full verification is required; further payouts either flag for follow-up or are
  held for review, depending on your account's configuration, once that threshold is reached. See
  the vendors guide for the full flow. These endpoints are enabled per account — a `404` on
  `POST /payments-api/v1/vendors` means vendor onboarding isn't turned on for you yet.
- **`MarketplaceVendor*` webhook family.** Platforms using vendor onboarding above can now receive
  `MarketplaceVendorActivated`, `...VerificationRequired`, `...Blocked`, `...PayoutAllowanceChanged`
  and related events — opt-in, via `eventTypes`.
- **`Simulator` request header for deterministic failure testing.** On a Demo-mode seller account,
  sending `Simulator: force_failure=<failureCode>` on a payment initiation forces that exact
  `failureCode` onto the resulting payment record — the `initialize-payment` call itself still
  returns `502`, as every authorization-stage failure does, so read the forced code back from
  `GET /payments/{paymentId}` or the `PaymentCompleted` webhook. Silently ignored outside Demo mode
  (a Live payment is never affected); inside Demo mode, an unrecognized or unsupported code returns
  `400`. See the [test cards guide](https://docs.fynex.ai/payments-api/v2/docs#tag/test-cards) for the full header contract.

### Fixed

- **`failureMessage` on REST payout responses is now populated.** Every payout response —
  `GET /payouts/{id}`, `GET /payouts`, the cancel response, and a `POST /payouts` (or payee
  payout) `201` whose `status` is already `failed`, including an idempotent replay — carries the seller-safe catalogue text for
  `failureCode` (for example "The bank rejected the payout."). A create that is rejected by the
  provider still answers `422` with an error body, as before. The field was declared on the
  schema since launch but never set; the guides that said "do not rely on it" have been
  corrected. Branch on `failureCode`, display `failureMessage`.
- **AVS refusals (billing address mismatch) now return a dedicated failure code**
  (`failureCode: 2005`, address verification failed). Previously misclassified as a generic provider error at the
  point it actually occurs (the `/payments` call, not handle creation), which surfaced to buyers as
  an unhelpful "try again" — leading some to retry with the same wrong address and stack a second
  authorization hold. Now correctly tells the integrator to prompt for a corrected billing address.
- **Insufficient-funds and provider risk-management declines are now correctly classified as hard
  declines** (`retry: never`) instead of a generic, retryable provider error (`retry: safe`).
  Both still report the ordinary `provider_declined` failure code — this API does not yet
  distinguish the specific reason for an issuer/acquirer decline — but integrators are no longer
  told, incorrectly, that resending the same request is safe. Not backfilled: payments that failed
  before this fix keep their original failure code.

### Changed

- **The payment's recorded `captureMode` now matches how it is captured.** It used to default to
  `auto` even for a manual-capture payment; it now reads `manual` whenever the payment waits for
  `POST /payments/{payment_id}/capture`, whether it was started with `POST /initialize-payment` or
  a hosted `POST /checkout` session. Only what is recorded changed — capture behaviour did not.

- **`remainderMinor` is always present in the `POST /split-rules/calculate` response**, `0`
  included; it used to be omitted when zero. It is what no line allocates, and it goes to your main
  wallet, or to the rule's `remainderWalletId`, as `remainderPolicy` says.
- **`orderData.payeeDistribution` is not deprecated.** The Splits guide called it a legacy input and
  described a migration away from it; both statements are withdrawn. A split rule's own lines
  (`absolute` mode) and `payeeDistribution` (the per-payment input to a `weight`-mode rule) are
  both supported, neither is legacy, and there is no migration path to follow — integrations that
  send `payeeDistribution` need no change.

---

## 2026-08

### Breaking

- **Cardholder name is now validated on submission**: ASCII letters, spaces, apostrophes, dots and
  hyphens only, up to 80 characters. A name outside this set — including one with an accented
  letter such as é, ñ, or ü — is rejected at request validation with a field-level error, where
  previously it was accepted and passed through. If you collect cardholder names from users whose
  names use such characters, transliterate to this character set before sending.

### Added

- **Billing API launched** — a new, separate public REST surface at `/billing-api/v1`
  (documented alongside this reference), authenticated with the same seller key as the Payments
  API. Covers invoice creation and management (list, detail, PDF, send), subscriptions (create,
  cancel, pause/resume, plan changes), contracts, usage ingestion and snapshots, and credit
  balances. Top-up invoices are *not* duplicated here — they remain on `/payments-api/v1` as
  before.
- **Payouts now snapshot the destination account format at creation time**, recorded on the payout
  going forward, rather than only reflecting the payee's current configuration. Editing the payout
  method after a payout is created does not change what that payout already recorded.
- **GBP payouts can dispatch via Faster Payments** (`uk_local`), in addition to existing rails.
- **Fynex-hosted checkout return page.** A merchant with no return page of their own can supply
  Fynex's hosted URL as their `returnUrl` instead of building and hosting one — a `returnUrl` is
  still required on every checkout.
- **Multibanco payment instructions** exposed for server-to-server alternative-payment-method
  flows.
- **MB WAY in-app notification flow.** In addition to the existing redirect flow (which stays
  available as a fallback, since the in-app push has a four-minute window and can go unanswered),
  buyers can now approve an MB WAY payment via an app notification without leaving the merchant's
  checkout.
- **Merchant fee annex and 3DS verdict fields** now exposed on the GraphQL payment type (backoffice
  and dashboard surfaces). Not currently exposed on the public REST payment record.
- **Payout approval is now configured separately for self-payouts vs. payee payouts.** This is set
  on your account by Fynex, not by API call. A payout awaiting review reports
  `approvalStatus: awaiting_approval` and dispatches nothing until it is resolved — poll or
  subscribe as you would for any other non-terminal payout state.

### Fixed

- **Seller-initiated top-up invoice creation is now enabled in production.**
  `POST /payments-api/v1/topup-invoices` is available as documented.

### Changed

- **Error message bodies are now standardized across the payments API.** Generic Go error text that
  could previously appear in a `5xx` response body was replaced with the platform's standard
  client-safe error messages. Status codes and the conditions that produce an error are unchanged —
  only the `message` text differs. If you matched on error message strings rather than
  `failureCode`, switch to the code.
