Skip to content
In this section

Fynex API Changelog

What changed in Fynex public APIs, by month — breaking changes, new capabilities, and fixes.

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 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. 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.

  • 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 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.