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}/regenerateandDELETE /api-keys/{id}now answer403to ansk_test_orsk_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-paymentafter a refused card returns the same422, not200. Replaying theIdempotency-Keyof a payment refused for an expired card (1007) or an unsupported card brand (1008) used to return200with the failed payment in the body; it now returns the same422andcodethe first attempt got. If you read the failed payment from a replay, useGET /payments/{ref}instead.
Added
Restricted API keys.
POST /api/v1/developer/api-keystakespermissions, a list such as["payments:read", "billing_usage:write"]; the key then reaches only the operations those permissions cover and is refused the rest with403insufficient_scope, naming what is missing. Each operation states what it needs inx-fynex-permissions, andGET /accountreports the calling key'saccessandpermissionsunderapiKey. Keys created withoutpermissionskeep full access.API keys can be named, can expire, and rotate without an outage.
POST /api/v1/developer/api-keystakes an optionalnameandexpiresAt; a key used after itsexpiresAtis refused with401api_key_expired, and the key list reports it asexpired.POST /api-keys/{id}/regeneratetakes an optionaloverlapHours(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 — unlessexpiresAtsets 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_metricandset_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-Keyon every create route that lacked one.POST /payees,POST /payees/setup,POST /payees/{id}/payout-methods,POST /webhooksandPOST /split-rulesaccept 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 answers409idempotency_key_reused. Requests without the header behave as before.Split webhook:
SplitExecuted. A settled payment's split now records an event when it posts, soGET /eventsshows the payment, its split and its payouts in one feed. The payload carries the execution, the payment'spaymentId, the rule, each payee's share, fee and net and the wallet credited, the total fee and the remainder, in the names and values ofGET /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 itseventTypesnames it.Payout webhooks:
PayoutCompletedandPayoutFailed. A payout that reachescompletedorfailednow sends a signed event whose payload carries the fieldsGET /payouts/{id}returns, includingfailureCodeandfailureMessageon a failure. The family is opt-in: an endpoint receives it only when itseventTypesnames the events, so existing endpoints see no change. A per-requestwebhookUrlonPOST /payoutsis still validated only, not delivered to.POST /test/checkout/{sessionId}/completepays 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 toolcomplete_test_checkout. Demo accounts only (403demo_onlyon Live); repeating it answersalready_completed.POST /test/payments/{paymentId}/settlenow also answersalready_settled, not409, for a Demo payment the automatic settlement got to first.POST /test/payments/{paymentId}/settlesettles 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 (403demo_onlyon Live); repeating it answersalready_settled.GET /accountreturns the account a credential acts for. Its id, name, currency,operationalMode(DemoorLive) 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 /payoutsis 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_payeekeeps funding the payee's cashout wallet from your main wallet.GET /eventslists 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 byeventTypeand anoccurredFrom/occurredTorange; passnextCursorback ascursor, and keep it to resume later.GET /paymentslists the account's payments. Newest first, filterable bystatus(thelifecycleStatusvalues), acreatedFrom/createdTorange,currency, an inclusiveamountMin/amountMaxrange (major units, likeamount; requirescurrency) andpayeeId(the payments whose split credited that payee, once the split has run), with keyset paging (cursor/nextCursor). Each entry carries the fields ofGET /payments/{payment_id}exceptallowedActions,settlementStatusandpromotion.
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.aias on staging. A bigger Demo payment is not settled automatically: callPOST /test/payments/{paymentId}/settle. Live payments are unchanged.Every error carries a
code. Each4xxand5xxbody 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_errorand so on; see Error format in Errors. Theerrortext is unchanged. A panic answers JSONinternal_errorinstead of plain text, a refused checkout finalize sayspayment_failedfor a decline andbad_gatewayfor a retryable fault, and a route refused to a self-serve sandbox saysnot_available_on_sandbox. Promotion and reCAPTCHA refusals repeat theirreasonascode.Rate-limit refusals carry a
code, and every429carriesRetry-After. A limiter's429body is now{"error": "...", "code": "rate_limited"}, and its fail-closed503carriescodetemporarily_unavailable; theerrortext is unchanged.GET /billing-api/v1/invoices/exportrefused because too many exports are running now sendsRetry-After: 5with its429.A checkout session or payment link no terminal can take is refused at creation with
422.POST /checkoutandPOST /api/v1/payment-linksused to accept a currency the account has no terminal for (for exampleGBPon an account with onlyEURterminals); the payment then failed withfailureCode1003(routing) only after the customer opened checkout and pressed pay. Creation now answers422with{"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 anIdempotency-Keythat 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 atext/plainbody (authorization token is required,invalid authorization token,seller account is not active); it is nowapplication/json—{"error": "...", "code": "..."}— with the sameerrorwording andcodemissing_api_key,invalid_api_keyoraccount_not_active. A403forbiddenfrom a permission gate carriesinsufficient_scope, and every401carriesWWW-Authenticate: Bearer. If your client parsed these bodies as plain text, readcodeinstead; a substring match on the old wording still matcheserror.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 answers401withcodekey_environment_mismatch, and a self-serve sandbox key after expirysandbox_expired; both wereinvalid_api_key, and theerrortext still starts withinvalid authorization token. A path no route serves under/payments-api/v1used to answer a plain-text404 page not found; every unknown route now answers404{"error":"route not found","code":"not_found"}, a wrong method405withmethod_not_allowed, and an operation requested under the/payments-api/v2documentation prefix is told that operations live under/payments-api/v1.POST /initialize-paymentanswers422with acodefor a refused card. An expired card (failureCode1007) used to return502with a generic message, which read as "retry"; it now returns422with{"error": "...", "code": "card_expired"}. A card brand the seller's acquiring agreement does not accept (1008) already returned422, but withoutcode; it now carries"code": "card_brand_not_supported". The error body gains an optionalcodefield; bodies without one are unchanged. If your client retried every5xxautomatically, an expired card now stops at the first attempt — ask the customer for a different card and send a newIdempotency-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-paymentfailed with2001and the description "Card authentication failed (3-D Secure)". When the bank's verdict is a refusal (threeDResultNorR), both now fail with2001, that description,failureStage: authorizationandretry: 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 is3003on both paths.An abandoned 3-D Secure step now fails with
failureCode3003. When the customer was sent to 3-D Secure and the authentication session expired before they completed it, the payment used to fail with2002(provider error),retry: safeandfailureCategory: transient— advice to replay the sameIdempotency-Key, which returns the same expired payment. It now fails with3003(authentication not completed),retry: fix_firstandfailureCategory: requires_change.statusis stillfailedandfailureStageis stillcheckout; nothing was authorized. If you branch on2002, add3003and ask the customer to pay again. Payments that failed before this change keep2002.An acquirer's card-brand refusal at authorization now fails with
failureCode1008. 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 with2001(provider declined) or2002(provider error); it now fails with1008(card brand not supported) andfailureStage: authorization, the code a refusal before authorization already carried. No funds move. OnPOST /initialize-paymentsuch a refusal now answers422with"code": "card_brand_not_supported"where it used to answer502. 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_approvallike a Live one and wait for Fynex; it is nowapprovedat 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-methodsandPOST /payees/{payee_id}/payout-methods(and a bank-routing change throughPATCH) on a Demo account now answerapprovalStatus: approvedinstead ofpending, 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 topendingand must be approved again. Live accounts are unchanged.Payee payouts on a Demo account fund themselves.
POST /payees/{payee_id}/payoutson 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 /accountreports it asfeatures.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 andPOST /vendorsno longer answer403wallet creation is not enabledon a Demo account, which moves no real money.GET /accountreportsfeatures.walletCreation: truefor 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
statusswitches to the lifecycle enum no earlier than 2026-12-28. From that datestatusonGET /payments/{payment_id},POST /initialize-payment,POST /finalize-payment,POST /payments/{payment_id}/captureand thePaymentCompletedwebhook carries the seven lifecycle values below instead of the internal processing status. Nothing changes before then. To be unaffected, readlifecycleStatus(the new values) orlegacyStatus(the current values) now — both are already present, see Added. The exact switch date is confirmed here at least two weeks ahead.Announced:
captureModedecides manual vs automatic capture, no earlier than 2026-12-28. TodayautoSettlementdecides — or, when it is left out, your account's default — andcaptureModeis only recorded. From the switch datecaptureMode: "manual"or"auto"onPOST /initialize-paymentdecides,autoSettlementbecomes a deprecated alias, and a request whose two fields disagree (e.g.autoSettlement: truewithcaptureMode: "manual") is refused with400. A request that sends both consistently, or onlyautoSettlement, is unaffected. The exact date is confirmed here at least two weeks ahead.bcPaymentIdremoved from the REST payout response, effective 2026-09-02. ReadproviderReferenceinstead — the supported field for this purpose, available since 2026-08-28. If your integration readsbcPaymentIdfrom a REST payout response, that key is now absent from the JSON entirely (notnull); switch toproviderReference. This affects REST only — the GraphQL payout schema still exposesbcPaymentIdunchanged for now.Expired cards are now rejected before the provider is called, with a dedicated
failureCode(card_expired) andfailureCategory: requires_change— previously this returned the generic provider-error code withfailureCategory: transientandretry: 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
PaymentCompletedfires once per outcome, not once per payment. A payment reportedfailed(orcancelledby an expired checkout) that its provider later proves was captured now sends a secondPaymentCompletedwithlifecycleStatus: captured; so does a capture the provider reverses. It used to be dropped as a duplicate, leaving your record on the first outcome whileGET /payments/{payment_id}showed the new one. A repeat of an outcome already sent is still not re-sent. Each event has its owneventId.lifecycleStatus,legacyStatusandsettlementStatuson payment responses. Every response that carries a paymentstatus, and thePaymentCompletedwebhook, now also carrieslifecycleStatus— one of seven closed values:pending,under_review,authorized,captured,refunded,failed,cancelled— andlegacyStatus, the currentstatusvalue.legacyStatusis deprecated and stays for one notice period after the switch announced above. How far captured money has travelled — whatfunds_in_flight,settledanddeposit_confirmedused to say — is the newsettlementStatusonGET /payments/{payment_id}. GraphQLGenericPaymentgainslifecycleStatustoo, as thePaymentLifecycleStatusenum; itsstatusstays 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
failureMessageon REST payout responses is now populated. Every payout response —GET /payouts/{id},GET /payouts, the cancel response, and aPOST /payouts(or payee payout)201whosestatusis alreadyfailed, including an idempotent replay — carries the seller-safe catalogue text forfailureCode(for example "The bank rejected the payout."). A create that is rejected by the provider still answers422with 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 onfailureCode, displayfailureMessage.- 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/paymentscall, 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 ordinaryprovider_declinedfailure 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
captureModenow matches how it is captured. It used to default toautoeven for a manual-capture payment; it now readsmanualwhenever the payment waits forPOST /payments/{payment_id}/capture, whether it was started withPOST /initialize-paymentor a hostedPOST /checkoutsession. Only what is recorded changed — capture behaviour did not.remainderMinoris always present in thePOST /split-rules/calculateresponse,0included; it used to be omitted when zero. It is what no line allocates, and it goes to your main wallet, or to the rule'sremainderWalletId, asremainderPolicysays.orderData.payeeDistributionis 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 (absolutemode) andpayeeDistribution(the per-payment input to aweight-mode rule) are both supported, neither is legacy, and there is no migration path to follow — integrations that sendpayeeDistributionneed 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/v1as 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
returnUrlinstead of building and hosting one — areturnUrlis 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_approvaland 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-invoicesis available as documented.
Changed
- Error message bodies are now standardized across the payments API. Generic Go error text that
could previously appear in a
5xxresponse body was replaced with the platform's standard client-safe error messages. Status codes and the conditions that produce an error are unchanged — only themessagetext differs. If you matched on error message strings rather thanfailureCode, switch to the code.