# Troubleshooting & FAQ

This page aggregates the most common integration mistakes and questions across all Fynex guides into one scannable reference. Use your browser's Find (Ctrl+F / Cmd+F) to search for an error message, HTTP status code, or symptom.

> [!NOTE]
> Still stuck after checking here? Email **support@fynex.ai** with your `externalOrderRef` (or payout `id`), the HTTP status and response body, and the approximate time of the request.

---

## 1. Authentication & tokens

### Q: I'm getting `401 Unauthorized` on every request

Check the `Authorization` header format. The correct form is:

```http
Authorization: Bearer <your_token>
```

Common mistakes: omitting the word `Bearer`, adding extra whitespace, wrapping the token in quotes, or sending a token to a host other than the one that issued it — staging and production tokens are not interchangeable. A quick smoke-test is `GET /payment-methods` — a `200 OK` confirms the token is valid and the seller account is active.

### Q: How do I tell a missing key from a wrong one?

Branch on `code` in the JSON `401` body: `missing_api_key` means the `Authorization` header was absent or had no token after `Bearer `; `invalid_api_key` means the token was not recognised — including a token sent to the other environment's host. Both bodies are JSON, like every other error.

See [Errors](https://docs.fynex.ai/payments-api/v2/docs#tag/errors) for the full error-shape reference.

### Q: I get a 401 with the message `"sellerAccountId is missing in auth context"` on `POST /checkout` even though my Bearer token is correct

This message is misleading. It almost always means the **`Idempotency-Key` header is missing or not a valid UUID**, not that authentication failed. The checkout handler reads the seller context via the same helper that validates the idempotency key — a missing or malformed key surfaces as this 401 before the Bearer token check completes. Verify your `Idempotency-Key` header first.

See [Request Headers](https://docs.fynex.ai/payments-api/v2/docs#tag/headers) and [Idempotency & retries](https://docs.fynex.ai/payments-api/v2/docs#tag/idempotency).

### Q: How do I rotate my token?

Use the GraphQL mutation `regenerateSellerAccountToken` on `/dashboard/graphql` (cookie-session authenticated). Supply your seller account numeric ID:

```graphql
mutation RotateToken($merchantId: ID!) {
  regenerateSellerAccountToken(merchantId: $merchantId) {
    id
    authorizationToken
  }
}
```

> [!CAUTION]
> Rotation **atomically replaces** the existing token. The old token is invalidated immediately — there is no two-token overlap window. Update your secret manager and restart affected services before calling this mutation in production.

See [Authentication & Tokens](https://docs.fynex.ai/payments-api/v2/docs#tag/authentication) for the full rotation steps.

### Q: Do tokens expire?

No. Tokens have no expiry by default. The only way to invalidate a token is to rotate it via `regenerateSellerAccountToken`.

---

## 2. Payment creation — `/initialize-payment` and `/checkout`

### Q: I sent `successUrl` and `cancelUrl` but they were silently ignored

Top-level `successUrl`, `cancelUrl`, and `failureUrl` are not part of either endpoint's DTO. The correct field name and **shape depend on which endpoint you're calling**:

- **`POST /checkout`** (hosted checkout) — nest URLs under `returnUrls` as an object:

  ```json
  {
    "returnUrls": {
      "success": "https://example.com/orders/123/success",
      "failure": "https://example.com/orders/123/failure"
    }
  }
  ```

- **`POST /initialize-payment`** (server-to-server) — pass `returnLinks` as an **array** of `{rel, href, method}`:

  ```json
  {
    "returnLinks": [
      { "rel": "on_completed", "href": "https://example.com/orders/123/success", "method": "GET" },
      { "rel": "on_failed",    "href": "https://example.com/orders/123/failure", "method": "GET" },
      { "rel": "default",      "href": "https://example.com/orders/123/return",  "method": "GET" }
    ]
  }
  ```

  Sending `returnLinks` as an object on `/initialize-payment` fails JSON decoding and returns `400 {"error":"invalid request body"}`. Valid `rel` values: `default`, `on_completed`, `on_failed`, `on_cancelled`.

See [Hosted Checkout](https://docs.fynex.ai/payments-api/v2/docs#tag/hosted-checkout) and [Server-to-Server](https://docs.fynex.ai/payments-api/v2/docs#tag/server-to-server).

### Q: `/initialize-payment` returns `400 {"error":"valid returnLinks are required"}`

The endpoint resolves return links in this order: (1) the `returnLinks` array on the request body, (2) the return links configured on the seller account in the Dashboard. If neither source provides at least one valid link, the request is rejected with this error. Either pass `returnLinks` explicitly in the request body, or configure defaults on the seller account.

### Q: I sent a `metadata` field on `/initialize-payment` but it doesn't appear in the response

`metadata` does not exist on `InitiatePaymentRequest` or its response DTO. Other fields that also do not exist on this DTO: `saveCard`, `customerEmail`, `splitRules` (top-level), `returnUrl` (top-level), `returnUrls` (the `/checkout` shape — `/initialize-payment` uses `returnLinks` instead), `successUrl`, `cancelUrl`. The server silently discards unknown JSON fields.

### Q: `/initialize-payment` rejects my currency code, but `/checkout` accepts it

`POST /initialize-payment` enforces a strict currency allowlist: `EUR`, `USD`, `GBP`, `DKK`, `NOK`, `SEK`. An unrecognized code returns `400`. By contrast, `POST /checkout` does not validate the currency at session-creation time — an unsupported currency may be accepted at that stage and only rejected when the payment is actually processed. Test end-to-end in staging to catch currency issues before going live.

### Q: I'm getting `502 {"error":"upstream card processor returned 409"}` (the payment's `failureCode` is `2002` with an upstream "Duplicate merchant reference" detail) — but I sent a fresh `Idempotency-Key`

The upstream card processor deduplicates independently of Fynex, on its `merchantRefNum` field — which Fynex sends as your `externalOrderRef`. Re-using the same `externalOrderRef` returns `409` from the processor, regardless of your Fynex `Idempotency-Key`. Use a unique `externalOrderRef` per attempt — typically your internal order ID plus an attempt counter, e.g. `ORDER-1042`, `ORDER-1042-r1`, `ORDER-1042-r2`.

The Fynex `Idempotency-Key` (deduplicates Fynex API calls) and the upstream `merchantRefNum` (deduplicates upstream transactions) are **separate** keys — both must be fresh on a genuinely new attempt; both must be reused identically when retrying after a network failure.

See [Idempotency & retries](https://docs.fynex.ai/payments-api/v2/docs#tag/idempotency) for the full retry semantics.

### Q: My idempotency key gets a `409` — can I reuse it?

Idempotency keys are per-endpoint and per-seller. Same key + same body = the existing operation is returned (the status code changes from `201`/`202` to `200` on replay). For an active redirect APM, Fynex rehydrates the existing provider redirect in that response.

Same key + different body on `/initialize-payment` returns `409 Conflict` with a concrete reason — for example:

```json
{ "error": "Idempotency-Key reused with a different currencyCode: original=USD, request=EUR" }
```

The check covers `amount`, `currencyCode`, `countryCode`, `externalOrderRef`, `paymentType`, and `paymentMethod`. If you need to retry with corrected parameters on any of those fields, use a fresh UUID. If you are retrying an unchanged request after a network failure, reuse the original key — that is the intended behavior.

See [Idempotency & retries / Reusing a key with a different body](https://docs.fynex.ai/payments-api/v2/docs#tag/idempotency).

See [Idempotency & retries](https://docs.fynex.ai/payments-api/v2/docs#tag/idempotency).

### Q: Do I need to call `/finalize-payment` even when `requiresAction` is `false`?

Almost always: yes. `/initialize-payment` returns `202 Accepted` for a new payment but does not confirm capture. Call `/finalize-payment` and check `lifecycleStatus` before fulfilling the order.

The single exception is the `skip3DS: true` server-to-server path. There the upstream payment handle is created already in `PAYABLE` state, and Fynex's upstream status poller auto-finalizes the payment on its next tick (~5 seconds). For those payments you do not need to call `/finalize-payment` yourself — poll `GET /payments-api/v1/payments/{payment_id}` until `lifecycleStatus` is `captured` (`authorized` for a manual-capture payment), `failed` or `cancelled`. See [Server-to-Server / Skipping 3DS](https://docs.fynex.ai/payments-api/v2/docs#tag/server-to-server).

### Q: I'm getting `502 {"error":"upstream card processor returned 400"}` (the payment's `failureCode` is `2002`) — what's missing?

The upstream card processor requires `billingDetails.country` (or `countryCode`) and `billingDetails.zip` (or `postalCode`). The Fynex DTO marks `billingDetails` as optional and forwards an empty object as empty strings, which the upstream rejects. Add at minimum:

```json
"billingDetails": {
  "country": "GB",
  "zip": "SW1A1AA"
}
```

If you also see `fieldErrors` in the upstream body for `billingDetails.state`, that field is required by the upstream processor for US/CA cards.

### Q: I get `502 {"error":"no active terminal found for seller account"}` even though my seller account is active

The HTTP response carries only the error string. On the payment row, Fynex also stamps `failure_code: 1003` and `failure_stage: routing` — visible to platform operators via the database / dashboard, but **not** returned in the HTTP body.

First distinguish an unsupported method market from missing seller routing. Fynex rejects an unsupported APM combination with `400` before creating a payment — for example, Wero supports `EUR` in `BE`, `DE`, and `FR`, so Wero with `countryCode: "IT"` returns `paymentMethod wero is not supported for currency EUR and country IT`.

Hosted checkout sessions and payment links are checked when they are created: one no terminal of the account can take is refused there with `422` and `code` `currency_not_supported` (see [Errors](https://docs.fynex.ai/payments-api/v2/docs#tag/errors)). The check accepts a session when any offered method can route it, so a `1003` on a hosted checkout can still mean the customer picked a method that cannot (a card on an account that routes this currency only through a local payment method, or a card whose issuing country has no terminal on an account that routes by card country), or that the account's terminals changed after the session was created.

If you instead receive `no active terminal found`, the combination is supported but no active terminal matches the **payment method**, **operational mode**, **country**, and **currency**. Check the seller's attached terminals (in the dashboard or via `seller_account_terminals` → `terminals`) and confirm at least one active link supports the requested instrument and matches its `currencyCode`, `countryCode`, and `operationalMode` (Demo vs. Live).

---

## 3. 3DS handling (server-to-server)

### Q: After the 3DS redirect I'm losing my checkout state — the resume page has no payment ID

The 3DS challenge is a full-page browser redirect away from your origin. All in-memory JavaScript state (React state, module-level variables) is destroyed during that navigation. Persist the `paymentId` and any UI state you need to `localStorage` before redirecting, then read it back on your return page:

```js
// Before redirect
localStorage.setItem('checkout_payment_id', paymentId);
localStorage.setItem('checkout_amount', String(amount));

// On return page (your returnUrl)
const paymentId = localStorage.getItem('checkout_payment_id');
await finalizePayment(paymentId);
localStorage.removeItem('checkout_payment_id');
localStorage.removeItem('checkout_amount');
```

The Fynex hosted dashboard uses `checkout_*` keys for this purpose. If you share an origin with the dashboard, use distinct key names to avoid collisions.

See [3DS Authentication](https://docs.fynex.ai/payments-api/v2/docs#tag/3ds).

### Q: React strict-mode is firing my finalize call twice and I'm getting a double-capture error

React 18+ strict mode invokes effects twice in development. Guard your `/finalize-payment` call with a `useRef` flag:

```jsx
const hasSubmitted = useRef(false);

useEffect(() => {
  if (hasSubmitted.current) return;
  hasSubmitted.current = true;
  finalizePayment(paymentId);
}, []);
```

See [3DS Authentication](https://docs.fynex.ai/payments-api/v2/docs#tag/3ds).

### Q: How do I know whether 3DS will be triggered before I redirect the customer?

You don't know in advance — it depends on the card issuer's risk decision. After calling `POST /initialize-payment`, inspect the response: if `requiresAction` is `true`, the customer must complete the 3DS challenge at `actionUrl`. If `requiresAction` is `false`, the payment's `lifecycleStatus` may already be `authorized` or `captured` and you can proceed directly to `/finalize-payment`.

### Q: Can I use an iframe for the 3DS challenge page?

No. Card issuers reject embedded challenges. The redirect to `actionUrl` must be a full-page browser navigation (`window.location.href = actionUrl`).

### Q: I get `failureCode: 2002` and a 502 `{"error":"upstream card processor returned 400"}` after calling `/finalize-payment`

The upstream response (visible in the platform-side payment exchange logs) carries a field-level
error on `paymentHandle`, reporting that the handle is in a state from which a payment cannot be
taken.

This means `/finalize-payment` was called **before** the customer completed the 3DS challenge at `actionUrl`. The upstream payment handle is still in `INITIATED` state and cannot be used to authorize a payment. Two common ways to hit this:

- A test script that calls `/initialize-payment` and immediately calls `/finalize-payment` without visiting `actionUrl` in a browser. The sandbox 3DS page has a "complete" button — open `actionUrl` first, click through the challenge, then call `/finalize-payment`.
- A frontend that signals the backend to finalize too early — before the issuer redirects the customer back to your `returnUrl`. Always wait for the post-redirect signal (server-rendered return page or an explicit message from your frontend).

The opposite timing has its own code. If the customer never answers the challenge and the authentication session **expires** first, the payment fails with `failureCode: 3003` (authentication not completed, `failureStage: checkout`) — whether or not `/finalize-payment` is called afterwards. Nothing was authorized; replaying the request cannot revive the session, so ask the customer to pay again. And if the customer **did** answer the challenge but their bank refused the authentication, the payment fails with `2001` and the description "Card authentication failed (3-D Secure)".

If you want to test the no-3DS path, the 3DS challenge is controlled by the **`skip3DS` request flag, not by the card number** — send `"skip3DS": true` on `/initialize-payment` and the payment authorizes without a challenge (`requiresAction: false`), so there is no `actionUrl` to visit and a separate `/finalize-payment` step is not needed. Any sandbox PAN works. See [Test cards & sandbox](https://docs.fynex.ai/payments-api/v2/docs#tag/test-cards).

---

## 4. Captures & refunds

### Q: Capture or refund returns `409 "payment action is not allowed for the current payment status"`

Every capture or refund the payment's state does not allow returns this one message. Read the payment with `GET /payments-api/v1/payments/{payment_id}` and check:

- **Capture** needs a manual-capture payment — created with `autoSettlement: false`, or with `autoSettlement` left out on an account whose default is not automatic capture (`captureMode` alone does not decide this) — whose `lifecycleStatus` is `authorized`, with no refund pending or already succeeded.
- **Refund** needs `lifecycleStatus` `captured`, `settlementStatus` other than `in_flight`, no refund still `pending`, refunds enabled for your account, and an amount no larger than what is left to refund.

The full conditions are under *What you can do next* in [Payment Lifecycle](https://docs.fynex.ai/payments-api/v2/docs#tag/payment-lifecycle).

### Q: What is `provider_completed`? I expected `captured`

`provider_completed` is a previous status value, which `status` still carries until the switch announced in the [changelog](https://docs.fynex.ai/payments-api/v2/docs#tag/changelog). Its lifecycle status is `captured`: read `lifecycleStatus` instead. A partial capture and a partial refund also leave `lifecycleStatus` at `captured`. See [Payment Lifecycle](https://docs.fynex.ai/payments-api/v2/docs#tag/payment-lifecycle).

### Q: The capture/refund path parameter takes my numeric internal ID, right?

No. The `{id}` path parameter on `POST /payments/{id}/capture` and `POST /payments/{id}/refund` is the **string `externalOrderRef`** you passed to `/initialize-payment` — for example, `ORDER-1042`. It is not a numeric ID.

```bash
# Correct — use your externalOrderRef string
curl -X POST "$FYNEX_API/payments/ORDER-1042/capture" ...

# Wrong — do not use a numeric internal ID
curl -X POST "$FYNEX_API/payments/12345/capture" ...
```

### Q: I issued a partial refund successfully, but a second partial refund returns `409`

After the first refund call, the refund is `pending` and the payment stays `captured`. You cannot issue another refund until the first one is no longer `pending`. A successful partial refund leaves the payment `captured` and refundable; once refunds add up to the whole captured amount it becomes `refunded`; a failed or cancelled refund can be retried.

---

## 5. Payouts

### Q: My `Idempotency-Key` header is being ignored on `POST /payouts`

`POST /payouts` does not read the `Idempotency-Key` HTTP header for deduplication. Pass idempotency as a body field named `idempotencyKey` instead:

```json
{
  "walletId": 15,
  "amountMinor": 199900,
  "currencyCode": "GBP",
  "idempotencyKey": "6f9b84e1-3b83-4fb9-9f42-a8ac27d11d6b"
}
```

All other POST endpoints in this API use the `Idempotency-Key` header. Payouts are the sole exception.

See [Payouts](https://docs.fynex.ai/payments-api/v2/docs#tag/payouts) and [Idempotency & retries](https://docs.fynex.ai/payments-api/v2/docs#tag/idempotency).

### Q: I'm getting `409 "insufficient balance"` but I can see funds in the wallet

Check the wallet's `availableBalanceMinor`, not its total balance. Held, pending, or reserved funds are not available for payout. Top up the wallet or wait for in-flight transactions to clear, then retry with the **same** `idempotencyKey` value.

### Q: Payout amounts — should I send major or minor units?

Major, like every other endpoint — but as a decimal **string**: `"amount": "19.99"`. The integer `amountMinor` (`1999`) still works as a deprecated fallback, and `amount` wins when both are sent. Responses return both fields.

### Q: When is `failureMessage` on `GET /payouts/{id}` populated?

Whenever `failureCode` is. It carries the seller-safe catalogue text for that code (for example "The bank rejected the payout."), so you can display it as-is; branch on `failureCode`, not on the message text. Both fields are absent while the payout has not failed. Older integrations that treated the field as always empty can start reading it — nothing else about the response changed.

---

## 6. Verification & polling

### Q: How do I know when a payment succeeds?

Three options, in order of preference:

- **Webhooks:** Fynex POSTs a signed `PaymentCompleted` event to every endpoint registered on your account (`X-Fynex-Signature`, `X-Fynex-Timestamp`). Only a `200` counts as delivered; anything else is retried up to 3 attempts in total and then marked failed. Deduplicate on `eventId` and treat the event as the signal, the API read as the source of truth. See [Webhooks](https://docs.fynex.ai/payments-api/v2/docs#tag/webhooks).
- **Polling:** `GET /payments-api/v1/payments/{payment_id}` (the path value is your `externalOrderRef`) with the same bearer token until `lifecycleStatus` is `captured`, `failed` or `cancelled`. No dashboard session is needed.
- **SSE:** if the customer is sitting on a hosted checkout page, subscribe to the server-sent events stream at `GET /checkout/{session_id}/events` from the browser.

See [Polling & SSE](https://docs.fynex.ai/payments-api/v2/docs#tag/polling-sse) for cadence and backoff.

### Q: I'm polling for `status === "succeeded"` and never getting a match

`succeeded` is a refund status, not a payment status. A successful payment has `lifecycleStatus` `captured` — branch on that. See [Payment Lifecycle](https://docs.fynex.ai/payments-api/v2/docs#tag/payment-lifecycle).

### Q: The customer was redirected to my `returnUrls.success` page — can I fulfil the order now?

No. A user can navigate directly to your success URL without paying. The redirect is not authoritative. Always verify the payment state server-side via polling or SSE before dispatching goods or services.

### Q: How long should I poll before giving up?

For card payments: 10–15 minutes is a reasonable outer bound. Bank transfers may take longer. If the payment stays in a non-terminal state beyond your deadline, surface a `pending_review` UX state and alert your operations team — do not keep polling indefinitely. Once `lifecycleStatus` is `refunded`, `failed` or `cancelled` (or `captured` if you don't need settlement progress), write it to your database and stop polling that record.

See [Polling & SSE](https://docs.fynex.ai/payments-api/v2/docs#tag/polling-sse).

---

## 7. GraphQL / Dashboard API

### Q: GraphQL returns `401` with my Bearer token

`POST /dashboard/graphql` does not support Bearer token authentication. It requires an **HttpOnly session cookie** named `dashboard_session`. Obtain one by calling `POST /api/v1/login/dashboard` first:

```bash
curl -sc cookies.txt \
  -X POST https://api.fynex.ai/api/v1/login/dashboard \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "your_password"}'

# Then send GraphQL requests with the saved cookie
curl -b cookies.txt \
  -X POST https://api.fynex.ai/dashboard/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ payees(limit: 5) { id displayName } }"}'
```

See [GraphQL Authentication](https://docs.fynex.ai/payments-api/v2/docs#tag/graphql-auth).

### Q: Sign-up succeeded but my next GraphQL request gets `401`

`POST /api/v1/onboarding/start` creates a **draft** user and sets the `dashboard_session` cookie, but the account has no organization until the wizard finishes with `POST /api/v1/onboarding/complete`. GraphQL operations that need an organization fail until then — complete the onboarding first (see [Account setup & onboarding](https://docs.fynex.ai/payments-api/v2/docs#tag/onboarding)). For an existing account, `POST /api/v1/login/dashboard` obtains the cookie.

See [GraphQL Authentication](https://docs.fynex.ai/payments-api/v2/docs#tag/graphql-auth).

### Q: My dashboard session disappeared and all GraphQL requests started returning `401`

Dashboard sessions are held in-memory on a single server node with a 24-hour TTL. A server restart invalidates all active sessions — you must re-authenticate. Your integration should handle the `401` response from `/dashboard/graphql` by re-running the login flow automatically.

### Q: Where is the GraphQL Playground?

A playground is available at `GET /dashboard/playground`. Note that the playground page itself has no auth gate — the authentication requirement applies to actual query execution, not to loading the playground UI.

---

## 8. Apple Pay & Google Pay (legacy surface)

### Q: Apple Pay merchant validation is failing in production

The Fynex dashboard fakes merchant validation by calling `completeMerchantValidation({})` with an empty object — this is a development shortcut that will not work against real Apple Pay. For production you must implement a **server-side** endpoint that contacts Apple's merchant validation URL using your Apple Pay merchant certificate and private key, and returns the opaque merchant session to the browser. Browsers cannot make this call directly due to CORS restrictions.

See [Apple Pay](https://docs.fynex.ai/payments-api/v2/docs#tag/apple-pay).

### Q: Apple Pay only shows in Safari — is that expected?

Yes. `ApplePaySession` is available only in Safari on Apple devices (macOS + Safari or iOS/iPadOS). Chrome, Firefox, and other browsers do not support it. Always gate the Apple Pay button on `isApplePayAvailable()`:

```js
function isApplePayAvailable() {
  return typeof window !== 'undefined' &&
    'ApplePaySession' in window &&
    ApplePaySession.canMakePayments();
}
```

### Q: Google Pay tokenization is being rejected by Fynex

As of the recent Google Pay refactor, the `gateway` and `gatewayMerchantId` parameters are no longer chosen by integrators — Fynex's hosted checkout sets them server-side from per-deploy configuration so they always match what the upstream processor has enrolled. If you're seeing tokenization rejections on the hosted page, contact Fynex support with the GP merchant ID you registered in the Google Pay Business Console; if you're driving Google Pay outside the hosted page, that integration path is not supported as a public API today.

### Q: `isReadyToPay()` returns `true` but the Google Pay sheet shows no payment methods

`isReadyToPay()` checks whether the Google Pay API is available, not whether the user has saved cards. The sheet can open and show no cards. Gate showing the Google Pay button on `isReadyToPay()` but handle the empty-sheet case gracefully — do not treat it as an error.

### Q: The Google Pay flow returned `authorizationLink` — what do I do with it?

A 3DS step-up is required. Save `merchantRefNum`, `paymentHandleToken`, and `amount` to `localStorage`, redirect the customer's browser to the `authorizationLink`, and call `complete-google-pay-payment` once the issuer redirects them back to your return page. This is the same localStorage-bridge pattern used by the card 3DS flow.

See [Google Pay](https://docs.fynex.ai/payments-api/v2/docs#tag/google-pay) and [3DS Authentication](https://docs.fynex.ai/payments-api/v2/docs#tag/3ds).

---

## Cross-reference: common error messages

| HTTP status | Message | Cause | Guide |
|-------------|---------|-------|-------|
| `400` | `Idempotency-Key header is required` | Missing `Idempotency-Key` header on a POST endpoint | [Headers](https://docs.fynex.ai/payments-api/v2/docs#tag/headers) |
| `400` | `Idempotency-Key must be a valid UUID` | Header value is not a UUID v4 | [Idempotency](https://docs.fynex.ai/payments-api/v2/docs#tag/idempotency) |
| `400` | `currencyCode is required` | Missing `currencyCode` field | [Server-to-Server](https://docs.fynex.ai/payments-api/v2/docs#tag/server-to-server) |
| `400` | `paymentMethod wero is not supported for currency EUR and country IT` | Wero is limited to EUR in Belgium, Germany, and France | [Alternative payment methods](https://docs.fynex.ai/payments-api/v2/docs#tag/alternative-payment-methods-apm) |
| `400` | `invalid request body` | JSON parse failed — check `Content-Type: application/json` and body syntax | [Errors](https://docs.fynex.ai/payments-api/v2/docs#tag/errors) |
| `401` | `authorization token is required` (`code` `missing_api_key`) | Missing or malformed `Authorization` header | [Authentication](https://docs.fynex.ai/payments-api/v2/docs#tag/authentication) |
| `401` | `invalid authorization token` (`code` `invalid_api_key`) | Token not recognised, or issued by the other environment's host | [Authentication](https://docs.fynex.ai/payments-api/v2/docs#tag/authentication) |
| `401` | `sellerAccountId is missing in auth context` | Missing or invalid `Idempotency-Key` on `POST /checkout` | [Hosted Checkout](https://docs.fynex.ai/payments-api/v2/docs#tag/hosted-checkout) |
| `403` | `resource does not belong to this seller` | Token is valid but the resource belongs to a different seller account | [Authentication](https://docs.fynex.ai/payments-api/v2/docs#tag/authentication) |
| `409` | `payment action is not allowed for the current payment status` | The payment's state does not allow this capture or refund — see the capture & refund question above | [Payment Lifecycle](https://docs.fynex.ai/payments-api/v2/docs#tag/payment-lifecycle) |
| `409` | `insufficient balance` | Wallet `availableBalanceMinor` is below the requested payout amount | [Payouts](https://docs.fynex.ai/payments-api/v2/docs#tag/payouts) |
| `409` | `payout with this idempotency key already exists` | Replay of a successful payout — fetch the existing payout instead of creating a new one | [Payouts](https://docs.fynex.ai/payments-api/v2/docs#tag/payouts) |
| `502` | `upstream card processor returned 400` (payment row: `failureCode: 2002`, missing billing fields) | Missing `billingDetails.country` and/or `billingDetails.zip` on a card payment | [Server-to-Server](https://docs.fynex.ai/payments-api/v2/docs#tag/server-to-server) |
| `failureCode: 2002` | `upstream card processor returned 400` after `/finalize-payment` (payment row: `failureStage: authorization`) | `/finalize-payment` called before the customer completed the 3DS challenge at `actionUrl` | [3DS Authentication](https://docs.fynex.ai/payments-api/v2/docs#tag/3ds) |
| `failureCode: 3003` | payment row `failureStage: checkout` | The customer never completed the 3DS challenge and the authentication session expired — ask them to pay again | [Errors](https://docs.fynex.ai/payments-api/v2/docs#tag/errors) |
| `502` | (any other) | Upstream processor error — safe to retry with the same idempotency key | [Errors](https://docs.fynex.ai/payments-api/v2/docs#tag/errors) |

## See also

- **[Errors](https://docs.fynex.ai/payments-api/v2/docs#tag/errors)** — Full HTTP status code and error body reference with recovery guidance.
- **[Polling & SSE](https://docs.fynex.ai/payments-api/v2/docs#tag/polling-sse)** — How to verify payment and payout status without webhooks.
- **[Idempotency & retries](https://docs.fynex.ai/payments-api/v2/docs#tag/idempotency)** — Make all mutating calls safe to retry with idempotency keys.
- **[Authentication & Tokens](https://docs.fynex.ai/payments-api/v2/docs#tag/authentication)** — Bearer token provisioning, rotation, and common auth errors.
