Errors
Status codes, error shapes, and how to recover from each kind of failure.
Error format
Every error — every 4xx and 5xx — is JSON with an error and a code:
{ "error": "amount must be greater than 0", "code": "bad_request" }
code is the contract; error is prose for a human and may be reworded in any
release. Where the API can say more than the status, code is specific — see
Authentication errors and Common 422 responses below, idempotency_key_reused,
rate_limited. Otherwise it is derived from the status:
| Status | code |
|---|---|
400 |
bad_request |
401 |
unauthorized |
403 |
forbidden |
404 |
not_found |
405 |
method_not_allowed |
409 |
conflict |
410 |
gone |
413 |
payload_too_large |
415 |
unsupported_media_type |
422 |
unprocessable_entity |
429 |
rate_limited |
500 |
internal_error |
502 |
bad_gateway |
503 |
service_unavailable |
504 |
gateway_timeout |
Any other 4xx is client_error and any other 5xx is server_error. A
response may gain a more specific code in a later release, so branch on the
codes you know and handle any other code by its HTTP status.
Authentication errors
Authentication failures use the same JSON envelope, and always carry a code:
| Status | code |
error |
Meaning |
|---|---|---|---|
401 |
missing_api_key |
authorization token is required |
The Authorization header is absent, or has no token after Bearer |
401 |
invalid_api_key |
invalid authorization token |
The token is not recognised — revoked, mistyped, or issued by the other environment's host |
401 |
key_environment_mismatch |
invalid authorization token: this key is for the account's other mode … |
A real key for the other mode — typically an sk_test_ key after the account went Live. Use the sk_live_ key on a Live account, sk_test_ on a Demo one |
401 |
sandbox_expired |
invalid authorization token: this sandbox has expired … |
A self-serve sandbox key after its sandbox expired. Create a new sandbox |
401 |
api_key_expired |
invalid authorization token: this API key has expired … |
An sk_ key used after its own expiresAt. Create or regenerate a key |
401 |
invalid_token |
invalid authorization token: invalid access token: … |
An OAuth access token of the MCP endpoint that expired (refresh it), whose connection was revoked, or that was sent to the REST API |
403 |
account_not_active |
seller account is not active |
The token is valid but the seller account is not active |
403 |
insufficient_scope |
forbidden, or this API key is restricted: … |
A delegated dashboard session whose user lacks the permission the route needs, or a restricted API key without the permission; the message names what is missing |
500 |
auth_unavailable |
varies | The authentication layer itself failed; retry with backoff |
{ "error": "invalid authorization token", "code": "invalid_api_key" }
Every 401 also carries WWW-Authenticate: Bearer.
A request no route serves answers 404 with code not_found, and a known path called with the wrong method answers 405 with code method_not_allowed. Operations live under /payments-api/v1; a request to an operation under /payments-api/v2 — the documentation prefix — says so in its error.
Machine-walkable support path
The currently supported error-to-support path is described by this stable
object. It is a documentation contract, not an assertion that every runtime
error response already contains a docsUrl field (that response change is
reserved for the PAY-16 contract).
{
"supportPath": {
"errorGuide": "/payments-api/v2/docs/errors.md",
"agentIndex": "/payments-api/v2/llms.txt",
"escalation": {
"kind": "email",
"address": "support@fynex.ai",
"required": ["requestId", "httpStatus", "responseBody", "occurredAt"]
}
}
}
An automated client should fetch errorGuide, then use agentIndex to find
the relevant skill or guide. Until a structured escalation endpoint is
published, send the listed fields to support@fynex.ai; requestId is the
value of the X-Request-Id response header. Do not put bearer tokens,
cardholder data, or secrets in responseBody. The email route is the current
supported escalation mechanism; it is not a promise that an API endpoint
exists.
Status codes
| Code | Meaning | Retry safely? |
|---|---|---|
200 / 201 / 202 |
Success | n/a |
400 |
Validation error in your request | No — fix the input |
401 |
Missing or invalid bearer token | No — fix auth |
403 |
Token valid but resource belongs to another seller | No — use the right resource |
404 |
Resource not found for this seller | No — check the ID |
409 |
Conflict — current state forbids the action | No — read the body and decide |
422 |
The card was refused and no funds moved (initialize-payment) |
No — ask for a different card, then send a new Idempotency-Key |
429 |
Per-seller rate limit exceeded | Yes — honor Retry-After, then retry |
500 |
Internal error | Yes — backoff + retry; alert on persistence |
502 |
Upstream provider failure | Yes — backoff + retry |
503 |
Service temporarily unavailable | Yes — backoff + retry |
Common 400 responses
| Message | Likely cause |
|---|---|
invalid request body |
JSON parse failed — check Content-Type and body syntax. On /initialize-payment, a common cause is sending returnLinks as an object ({"success": ..., "failure": ...} — that shape belongs to /checkout) instead of an array of {rel, href, method}. |
Idempotency-Key header is required |
POST endpoint called without the header |
Idempotency-Key must be a valid UUID |
Header value is not a UUID |
currencyCode is required |
Missing required field |
paymentMethod wero is not supported for currency EUR and country IT |
The APM does not support the requested currency/country. Wero supports EUR in BE, DE, and FR only. The request is rejected before a payment is persisted or routed. |
amount must be greater than 0 |
Zero or negative amount |
valid returnLinks are required |
On /initialize-payment, the request had no returnLinks (or invalid ones) and the seller account also has no valid return links. Pass returnLinks: [{rel, href, method}] in the request body, or configure default links on the seller account in the Dashboard. |
payment configuration is not set for seller account |
Seller has no payment methods configured |
Common 409 responses
These mean the resource's current state forbids the action. Read the message before deciding whether to retry:
| Message | What to do |
|---|---|
Idempotency-Key reused with a different <field>: original=X, request=Y |
On /initialize-payment you re-sent a known Idempotency-Key with a body that differs in one of the financial fields (amount, currencyCode, countryCode, externalOrderRef, paymentType, paymentMethod). To replay the original payment, send the original body unchanged. To create a new payment, mint a fresh Idempotency-Key. See Idempotency & retries. |
payment action is not allowed for the current payment status |
A capture or refund the payment's current state does not allow: it is not authorized and manual-capture (capture), not refundable yet or any more (refund), a refund is still pending, a refund already succeeded (capture), or the amount exceeds what is left to refund. GET /payments/{payment_id} shows which — lifecycleStatus, settlementStatus — and the conditions are listed under What you can do next in Payment Lifecycle. |
settlement not yet available for refund |
The payment is captured, but the settlement a card refund must target is not ready: the acquirer refuses refunds until its settlement batch has run (a refund on the day of capture gets this), or the batch has not reached Fynex yet. Nothing was refunded and the payment stays refundable. Retry after the next batch, usually the next day, with a fresh Idempotency-Key; the refused attempt is listed by GET /payments/{payment_id}/refunds with failureCode 2004. |
payout with this idempotency key already exists |
Replay of a successful create — fetch the existing payout instead |
insufficient balance |
Top up the wallet, then retry with a fresh idempotencyKey |
Common 422 responses
POST /initialize-payment answers 422 when the card itself is refused before
authorization; no funds moved, and re-sending the same card fails identically —
so, unlike a 502, this is not a retry signal. Branch on code:
code |
failureCode | Meaning | What to do |
|---|---|---|---|
card_expired |
1007 |
The card's expiry date has already passed. | Ask the customer for a different card, or for the updated details if their card was reissued. For a stored card, collect a new one. |
card_brand_not_supported |
1008 |
The seller's acquiring agreement does not accept this card's brand (for example American Express where only Visa and Mastercard are enabled). | Ask the customer for a card of an accepted brand. If you believe the brand should be accepted, contact Fynex support. |
{ "error": "This card type is not accepted for this payment. Please use a different card.", "code": "card_brand_not_supported" }
The payment is still created, in failed with the matching failureCode, so
GET /payments-api/v1/payments/{externalOrderRef} shows it. Replaying the same
Idempotency-Key with the same body returns the same 422 and code; to try
another card, send a new Idempotency-Key. A replay reflects the payment's
current failure, so it answers from the stored payment rather than repeating
the first response verbatim. Treat an unrecognised code as a refusal you
cannot fix automatically — new codes may be added. A generic HTTP client or SDK
that treats 4xx as non-retryable is doing the right thing here.
A checkout session or payment link no terminal can take
POST /checkout and POST /api/v1/payment-links answer 422 with code
currency_not_supported when none of the account's payment terminals can take
the payment — most often a currency the account has no terminal for (a GBP
session on an account with only EUR terminals), or, for a declared
countryCode, a country none of them serves. Nothing is created: no session,
no payment, no link.
{ "error": "currency GBP is not supported for country GB on this account", "code": "currency_not_supported" }
The check runs at creation so the customer never reaches a checkout that would
fail with 1003 (routing) after they press pay. A session is accepted when
any method the hosted checkout can offer for it would route: the card form, or
a local payment method the account has a terminal for in that currency and
country. A payment link lives longer than a session, so it is refused only when
the account has no terminal that matches the link's currency, mode (live or
test) and payment type and country; a matching terminal that is temporarily
suspended, or not effective yet, does not block the link.
The same request succeeds once the account has a terminal for that currency
(and country), for example after the currency is activated on the account —
ask Fynex support to enable it. Until then, re-sending it gives the same
422. Replaying an Idempotency-Key that already created a session still
returns that session. If Fynex cannot check the terminals at that moment (a
transient internal error), the session or link is created rather than refused.
Rate limiting (429 responses)
When the per-seller limiter is enabled and available, authenticated business
operation endpoints under /payments-api/v1/* are rate-limited using a
token-bucket keyed on the authenticated seller_account_id. The standard
budget is 100 requests per hour per seller. An account or environment with
a provisioned exception can have a different budget, so read
X-RateLimit-Limit and X-RateLimit-Remaining from the response: they are
authoritative for the environment you are calling. The budget is per-seller
(one noisy client cannot starve others).
A Demo account has its own, larger budget: 200 requests per minute per seller, counted separately from the Live budget. Test traffic — including a coding agent that lists, polls and retries in a loop — does not run into the Live limit, and switching an account to Live does not carry Demo usage over.
Every authorized response — both 200 and 429 — normally carries:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
The bucket size for this seller (normally 100) |
X-RateLimit-Remaining |
Tokens left in the bucket after this request |
X-RateLimit-Reset |
Whole seconds until the bucket has refilled to X-RateLimit-Limit |
RateLimit-Policy |
The budget itself, in IETF structured-field syntax: "seller";q=100;qu="requests";w=3600 — quota, unit, window in seconds |
RateLimit |
Where you stand against it: "seller";r=12;t=45 — remaining, and seconds until reset |
Retry-After (429 only) |
Whole seconds to wait before the next request is guaranteed to succeed |
Note
RateLimitandRateLimit-Policyare the current standards-track fields (draft-ietf-httpapi-ratelimit-headers). They are not theRateLimit-Limit/RateLimit-Remaining/RateLimit-Resettriple you may remember — that spelling is from an earlier revision of the same draft and Fynex does not send it. If your client only knows the old shape, read theX-RateLimit-*headers, which carry the same numbers.
X-RateLimit-Reset is what lets you pace a loop before you are refused:
X-RateLimit-Remaining alone tells you how many requests are left but not how
long you have to spend them over, and Retry-After arrives only once you have
already been throttled.
When the bucket is empty:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 36
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 3600
RateLimit-Policy: "seller";q=100;qu="requests";w=3600
RateLimit: "seller";r=0;t=3600
{"error":"rate limit exceeded; retry after the Retry-After header value","code":"rate_limited"}
Every 429 carries Retry-After, and a limiter's 429 carries code
rate_limited. If the limiter itself is unavailable on a route that fails closed,
the answer is 503 with Retry-After: 1 and code temporarily_unavailable.
How to react: sleep for at least Retry-After seconds, then retry the same request with the same Idempotency-Key. Idempotency-Key replay is safe; the original response is returned once the bucket has capacity.
How to avoid it: read X-RateLimit-Remaining and X-RateLimit-Reset on every response and back off proactively when the remaining count approaches zero. Drive your polling interval from those values rather than from a fixed number of seconds — an account-specific budget can differ from the standard one. Add ±20% jitter to any loop.
Missing headers: authorized responses — both 2xx and 429 — normally
carry the X-RateLimit-* headers. 401/403 responses never do, because the
budget is keyed on the authenticated seller and the limit is applied after
authentication. A deployment may disable the limiter, and a temporary limiter
outage can allow an authorized request without rate-limit headers. If the
headers are absent, hold your most conservative polling interval rather than
reading it as headroom, and contact support if it persists.
502 vs 500
502 Bad Gateway— Fynex called an upstream processor and it returned a non-OK response or timed out. The action may or may not have been processed upstream — your idempotency key protects you on retry.500 Internal Server Error— Fynex itself encountered an unexpected error. Should be rare. If you see persistent 500s, contact support with the request details.
Common 502 causes
A 502 can come from either Fynex-side routing (no terminal selected) or an upstream processor returning a non-OK response. An unsupported APM market is instead rejected with 400 before routing. Upstream-side 502s all share the same body shape:
{ "error": "upstream card processor returned <status>" }
To distinguish why the upstream rejected the request, fetch the payment afterwards (GET /payments-api/v1/payments/{externalOrderRef}) and inspect its failureCode + failureStage fields.
| Body (abridged) | failureCode | Cause | Fix |
|---|---|---|---|
no active terminal found for seller account |
1003 (routing) |
The method/currency/country combination is supported, but Fynex routing couldn't find an active seller terminal that matches the request's method, operationalMode (Demo vs Live), countryCode, and currencyCode. |
Check the seller's attached terminals in the Dashboard and confirm at least one active link supports the APM and matches the request's countryCode + currencyCode under the seller's operationalMode. See Troubleshooting. |
upstream card processor returned 400 |
2002 (authorization) — billingDetails missing |
Card payment sent without billing country and/or zip. The Fynex DTO marks billingDetails as optional but the upstream card processor requires both. |
Always populate billingDetails.country (or countryCode) and billingDetails.zip (or postalCode) on card initialize requests. See Server-to-Server. |
upstream card processor returned 400 |
2002 (authorization) — payment handle in wrong state |
/finalize-payment was called before the customer completed the 3DS challenge at actionUrl. The upstream payment handle is still in its initial state and cannot authorize a payment. |
Visit actionUrl from the initialize response, complete the challenge, then call /finalize-payment. See 3DS Authentication and Troubleshooting. |
upstream card processor returned 409 |
2002 (authorization) — duplicate merchant reference |
Re-using the same externalOrderRef on a fresh idempotency key. The upstream processor deduplicates on its own merchant reference, independent of the Fynex Idempotency-Key. |
Either re-use the original Idempotency-Key (replays the original response) or send a fresh externalOrderRef. |
upstream card processor returned <other> |
2001 / 2002 / 2003 |
Generic upstream processor failure, decline, or timeout. | Backoff and retry with the same Idempotency-Key. Persistent failures: contact Fynex support with the payment's externalOrderRef so we can correlate against upstream logs. |
Failure codes inside payment responses
Even when the HTTP call returns 200, the payment itself may have failed at the processor. Inspect the response body:
{
"paymentId": "ORDER-1042",
"status": "failed",
"lifecycleStatus": "failed",
"legacyStatus": "failed",
"failureCode": 2001,
"retry": "never",
"failureCategory": "hard",
"failureDescription": "Provider declined the transaction"
}
failureCode is the contract; failureDescription is prose for a human and may
be reworded in any release, so branch on the code and show the description.
Every code is enumerated below with its cause, whether retrying can succeed, and
what to do — and the same table is on the field itself in openapi.json, so a
generated client carries it. The retry verdict also rides on the response as
retry (safe, fix_first or never), on every payment and refund response
and on the PaymentCompleted webhook, so you can branch on it without joining
the table yourself.
The retry column is the part worth reading twice. Getting it wrong costs money in
both directions: retrying a decline the issuer already made loses the sale and can
get the card blocked, while not retrying a timeout with the original
Idempotency-Key is how a customer gets charged twice.
failureCategory rides beside it and answers the other question — not what to
do but what happened: hard, transient, requires_change,
integration_error, cancelled, or unknown for a code this catalogue does
not classify. Branch your retry loop on retry; count, chart and alert on
failureCategory. The split is what keeps a spike of integration_error (your
bug) out of the same number as a spike of hard (your conversion rate). Both
fields are absent while the payment has not failed.
failureStage narrows it further — the stage says where the payment stopped,
the code says why. Anything before authorization never reached a provider, so
no money moved.
Two card declines that leave a hold
A declined payment is not always a released card. On an AVS mismatch — the billing address or postal code did not match the issuer's record — the issuer may have placed a temporary authorization hold that the cardholder sees on their statement for up to 3–5 business days even though the payment failed and Fynex captured nothing. The same is true on a CVV mismatch: the security code was wrong, the payment failed, and the cardholder may still see a pending authorization for up to 3–5 business days before their issuer drops it.
Neither hold is something Fynex can release, and neither is a charge. Say so in your own customer-facing copy before the buyer calls their bank — and do not re-run the same card repeatedly to "clear" it, because each attempt can add another hold.
An AVS refusal has its own code: failureCode: 2005 (address verification
failed, failureCategory: requires_change) — the buyer must correct the billing
address, not retry. A CVV mismatch is still reported as an ordinary 2001, and
avsResult / cvvResult are not published on the payment, so for CVV the
advice above applies only when you already know the decline reason from your
own checkout flow.
Soft declines and 3-D Secure
3002 is the one decline that is not a refusal of the card: the issuer
soft-declined the authorization and asked for strong customer authentication
instead. The card processor reports its own soft-decline code at the
authorization gate and Fynex maps it onto 3002, so branch on 3002 rather
than on any processor-specific number. It carries retry: fix_first and failureCategory: requires_change,
not hard — re-run the payment through a 3-D Secure flow and it can succeed.
Retrying without one fails identically, and counting it as a decline throws away
a sale you can still make. See 3DS Authentication.
3003 is the customer walking away rather than the issuer asking: the customer
was sent to 3-D Secure and did not complete it before the authentication session
expired, so no authorization was ever attempted and nothing was charged. It also
carries retry: fix_first and failureCategory: requires_change. Replaying the
request with the same Idempotency-Key returns the same failed payment — the
session it depended on is gone — so ask the customer to pay again and complete
the verification step their bank shows them.
The /checkout Idempotency-Key pitfall
POST /checkout validates the Idempotency-Key header right after auth, in initFinalizeHeaderMiddleware. A missing or malformed key returns a JSON 400 — {"error":"Idempotency-Key header is required"} (header absent or empty) or {"error":"Idempotency-Key must be a valid UUID"} (value is not a UUID). A bad token still answers 401 first. If you see one of these 400s from /checkout, fix the Idempotency-Key header rather than your token.
Recovering from partial failures
If /initialize-payment returns 502 but you don't know whether the processor created or charged the payment:
- Wait 30–60 seconds.
- Retry the unchanged request with the same idempotency key. Fynex and its APM provider integration reuse the same provider-attempt idempotency key and byte-stable create body, so a timed-out APM create is reconciled instead of creating a second charge. If the charge is still awaiting buyer action, the replay returns the current redirect or Multibanco payment instructions again.
- If the retry also fails, poll the canonical state. From a server-to-server backend the simplest path is
GET /payments-api/v1/payments/{externalOrderRef}(bearer auth, same token). From cookie-session contexts use the GraphQLgenericPayment(id)query instead.
The same pattern applies to /finalize-payment, /payments/{id}/capture, /payments/{id}/refund, and /payouts.
See also
- Polling & SSE — Verify payment and payout state by polling or server-sent events.
- Idempotency & retries — Make your retries safe with idempotency keys.
Payment failure codes
Returned as failureCode on every payment and refund response, and on the
payment.completed webhook. A 200 does not mean the payment succeeded —
read status, and when it is failed read this.
Why the payment failed. 0 means it has not.
Retry says what re-sending achieves: safe — the same request with the same Idempotency-Key can succeed; fix first — retrying unchanged fails identically, something has to change; never — a decision was made or the outcome is not knowable by re-sending, and an automatic retry is wrong.
Category says what KIND of failure it is: hard — a decision was made and it stands; transient — nothing was decided; requires_change — the customer's instrument or authentication has to change; integration_error — the request or the seller's configuration is wrong, not the customer's card; cancelled — the payment was called off; unknown — unclassified. Both ride on the response, as retry and failureCategory.
| Code | Meaning | Category | Retry | What to do |
|---|---|---|---|---|
1001 |
Request validation failed. The request was rejected before it reached any provider. The response body names what was wrong. | integration_error |
fix first | Correct the request and send it with a NEW Idempotency-Key. Replaying the old key returns the same rejection. |
1002 |
Payment rejected by risk policy. Fynex's own risk policy declined the payment. Distinct from a card decline: the card was never charged. | hard |
never | Do not retry automatically — the same request produces the same decision. Offer the customer a different payment method, and contact Fynex support if you believe the decline is wrong. |
1003 |
No active terminal found for seller account. No active terminal on the seller account matches this request's payment method, currency, country and mode, so there was nothing to route to. | integration_error |
fix first | A configuration problem, not a customer one. Check the seller's terminals in the dashboard and confirm at least one active link covers the request's method, currency and country under the account's current mode. |
1004 |
Compliance screening declined the transaction. Transaction monitoring returned a decline before authorization. The card was never charged. | hard |
never | Do not retry. The decision is recorded and a retry produces the same outcome; contact Fynex support to have the case reviewed. |
1005 |
Compliance screening paused the transaction for review. Transaction monitoring did not return a decision in time, or returned one that requires review. The card was never charged. | hard |
never | Do not retry automatically. The case is followed up outside the API; contact Fynex support with the payment's externalOrderRef. |
1006 |
Payment initialization was interrupted. Checkout claimed the payment but failed before any provider request was made — for example the buyer disconnected mid-initialization. Not a card decline. | transient |
safe | Retry with the same Idempotency-Key. Nothing reached a provider, so no charge can be duplicated. |
1007 |
This card has expired. Please use a different card. The card's printed expiry date had already passed when the charge was attempted. Rejected before any provider was contacted, so no authorization exists and no funds moved. | requires_change |
fix first | Do not retry this card — an expiry date only moves further into the past, so every retry fails identically. Ask the customer for a different card, or for the updated details if their card was reissued. For a stored card, collect a new one and replace it. |
1008 |
This card type is not accepted for this payment. Please use a different card. The seller's acquiring agreement does not accept this card's brand (for example American Express where only Visa and Mastercard are enabled). The card was refused before any funds moved. | requires_change |
fix first | Do not retry this card — every retry fails identically. Ask the customer for a card of an accepted brand. |
2001 |
Provider declined the transaction. The card issuer or the acquirer declined the authorization. This is the ordinary decline. | hard |
never | Do not retry the same card automatically — an issuer that declined once declines again, and repeated attempts can get the card blocked. Show the customer failureDescription and let them choose to try again or use a different card. |
2002 |
Provider returned an error. The provider returned an error rather than a decision — a malformed exchange, a rejected field, or an upstream fault. The payment's outcome is not known from this response alone. | transient |
safe | Back off and retry with the SAME Idempotency-Key, which replays rather than re-charges. If it persists, poll the payment before sending anything new. |
2005 |
The billing address did not match the card issuer's records. The payment was not taken; any authorization hold is the card issuer's to release. The card issuer refused the authorization because the billing address did not match its records (Address Verification System). The acquirer reserves the amount on every attempt; release is the issuer's, and the delay is commonly several business days. | requires_change |
fix first | Do not re-send the same address — it fails identically and reserves the amount again, so each blind retry costs the customer another hold. Collect the billing address exactly as the customer's bank holds it, including street number and postcode, then submit a new payment. |
2006 |
Provider rejected a request field. The provider refused the request itself, naming a field it could not accept — part of the billing address, or the card number. No authorization was attempted, nothing was charged, and no hold was placed. | requires_change |
fix first | Re-sending the same values fails identically. Collect the billing address and card number again — check that the state/province is the two-letter code for the billing country, and that the card number was entered in full — then submit a new payment. |
2003 |
Provider request timed out. The provider did not answer in time. The request may or may not have been processed upstream. | transient |
safe | Wait 30–60 seconds and retry with the SAME Idempotency-Key. Never send a fresh key after a timeout — that is how a customer gets charged twice. |
2004 |
Refund is not yet available: the provider settlement is not ready yet. The capture succeeded, but the settlement the refund depends on is not ready yet: the acquirer has not batched it (a refund on the day of capture), or the batch has not reached Fynex. The payment is still refundable. | transient |
safe | Retry after the next settlement batch, usually the next day, with a fresh Idempotency-Key. This clears on its own; it is not a permanent refusal. |
3001 |
Capture failed. The authorization existed but the capture did not complete. | transient |
safe | Poll the payment first, then retry with the SAME Idempotency-Key if it is still uncaptured. An authorization also expires — a capture attempted after expiry cannot succeed however often it is retried. |
3002 |
Soft decline — the issuer requires strong customer authentication (3-D Secure) for this transaction. The issuer soft-declined the authorization and asked for strong customer authentication (3-D Secure) instead. No decision was made against the card. | requires_change |
fix first | Re-run the payment through a 3-D Secure flow; retrying without it fails identically. |
3003 |
Card authentication (3-D Secure) was not completed in time. The customer was sent to their card issuer's authentication step (3-D Secure) and did not complete it before the payment session expired — for example they closed the page or did not approve it in their banking app. No authorization was attempted and nothing was charged. | requires_change |
fix first | Re-sending this payment cannot succeed: the authentication session it depended on has ended. Ask the customer to pay again and complete the verification step their bank shows them. |
4001 |
Settlement failed. The payment authorized and captured, but settling the funds did not complete. Platform-side. | hard |
never | Nothing to retry through the API — re-sending cannot move a settlement. Contact Fynex support with the payment's externalOrderRef. |
5001 |
Deposit confirmation timed out. A bank-transfer deposit was not confirmed within the window. The transfer may still arrive. | hard |
never | Poll the payment rather than re-sending. A second request creates a second expected deposit, and the customer has already sent the money once. |
9001 |
Cancelled by merchant. You cancelled the payment. | cancelled |
never | Start a new payment with a new externalOrderRef if the customer wants to try again. |
9002 |
Cancelled by the system. Fynex cancelled the payment — most often an unfinished checkout that reached its expiry. | cancelled |
never | Start a new payment. The old one is terminal and cannot be revived. |
9999 |
Unknown failure. The failure did not map to any code above. This is a gap in our classification, not a statement about your request. | unknown |
never | Poll the payment for its canonical state before doing anything else, and report it to Fynex support with the externalOrderRef so the case can be classified. |
Payout failure codes
Why the payout failed. Absent while it has not.
In every case the held funds are returned to the seller's available balance before the payout is marked failed, so a failed payout never leaves money stranded — and no payout failure is retryable by simply re-sending the same request. Treat a failed payout as terminal, read failureMessage for what to tell the seller, and create a new payout only after the underlying cause is addressed.
The enumerated code list is not published yet; it is pending a rename that removes supplier-specific prefixes from four of the values.