# Fynex Payments API

Integration guides and the complete API reference for accepting payments and sending payouts with Fynex. Accept cards, Apple Pay, Google Pay, and bank transfers — and send payouts — with one unified API.

## What is Fynex?

Fynex is a unified payments platform that lets you accept money from customers across cards, Apple Pay, Google Pay, and bank transfers, and pay money out to registered counterparties via the banking provider. A single seller-issued bearer token authenticates every call, and the same set of endpoints powers both hosted-checkout and full server-to-server flows.

These docs walk you through the integration end-to-end. **Getting started** covers your first successful payment. **Concepts** dives into the data model. **Payment flows** describes each acceptance method. **Payouts** covers sending money out. **Operations** has the operational essentials — idempotency, polling & SSE, errors, reconciliation, and the production checklist. **Reference** lists every endpoint, parameter, and response — generated from the live OpenAPI spec.

## Where to start

- **[Quickstart](https://docs.fynex.ai/payments-api/v2/docs#tag/quickstart)** — Take your first test payment in 5 minutes.
- **[Hosted checkout](https://docs.fynex.ai/payments-api/v2/docs#tag/hosted-checkout)** — The fastest way to accept a payment with the lowest PCI scope.
- **[Server-to-server](https://docs.fynex.ai/payments-api/v2/docs#tag/server-to-server)** — Full control of the flow when you collect card data on your own server.
- **[Payouts](https://docs.fynex.ai/payments-api/v2/docs#tag/payouts)** — Move money out from a wallet to a registered payee.
- **[Going live](https://docs.fynex.ai/payments-api/v2/docs#tag/going-live)** — Production checklist: tokens, monitoring, and the launch playbook.

## Related: the Billing API

Invoices, subscriptions, contracts, metered usage and credit balances live on
their own surface, **`/billing-api/v1`**, documented at
`/billing-api/v1/docs` with the machine contract at
`/billing-api/v1/openapi.json`. It authenticates with the same seller secret
key. One convention difference to know before writing code: Billing carries
money exclusively as integer **minor units** in fields ending `Minor`, while
this API's checkout takes major units — see **Amounts and currency**.


# Welcome to the Fynex Payments API

Fynex is a unified payments platform for accepting cards, Apple Pay, Google Pay,
and bank transfers, plus moving money out via payouts. This reference describes
every endpoint a seller integration needs.

All routes live under `/payments-api/v1` and authenticate with a seller bearer
token. You can mint a token from the Fynex Dashboard.

## Base URLs

- **Production:** `https://api.fynex.ai/payments-api/v1`

The OpenAPI document's `servers` block declares the origin of the deployment
serving it. Each entry is the bare origin; the `/payments-api/v1` prefix is
already part of every path in the document.

Which host you call does not decide whether money is real: that follows your
**account's** operational mode. A Demo account is a sandbox account on any host.

## Versioning

Every operation is served under **`/payments-api/v1`** — there is no `/v2`
operation surface. `/payments-api/v2` carries only the documentation for that
same contract (`/payments-api/v2/docs`, `/openapi.json`, `/llms.txt`), because it
was the path the current spec was first published under during the docs
migration. A request to `/payments-api/v2/initialize-payment` answers `404`.

**Call `/payments-api/v1`.** Reading the reference at `/v2/docs` does not put
you on a newer contract; there is only one.

When a genuinely incompatible version ships it will be a new prefix, announced
before it lands, and this section will say what changed and by when the old one
stops. Until then there is one contract.

### How a change is signalled

Additive changes — a new endpoint, a new optional field, a new enum value —
ship without notice, so write clients that ignore fields they do not recognise
and do not fail on an unknown enum value.

Anything being withdrawn is marked `deprecated: true` in `openapi.json` before
it goes, on the field, schema or operation itself. A renamed schema keeps its
former name published as a deprecated alias pointing at the replacement, so a
client generated against the older document still resolves. Read the spec, not
just the guides: the deprecation markers are in the schema, which is where a
generated client and an agent will see them.

### Support and deprecation policy

Payments and Billing use the same shared public API lifecycle and deprecation
policy. The terms below apply consistently to both public API references.

A public major version remains supported for **at least 12 months** after its
successor becomes generally available. We give integrators **at least 90
calendar days** of notice before removing a public operation, field or enum
value from a supported major version. Each notice names the replacement (when
there is one), the migration action and the planned removal date in the public
[Changelog](https://docs.fynex.ai/payments-api/v2/docs#tag/changelog).

The OpenAPI `deprecated: true` marker is the machine-readable signal. For an
HTTP operation with a scheduled retirement, the changelog entry records the
planned `Deprecation` and `Sunset` response headers: `Deprecation` will say the
operation is no longer the preferred integration path, and `Sunset` will give
its planned last-availability date. No HTTP operation has a scheduled
retirement today, so neither header is currently emitted. A critical security,
legal or abuse response may need an earlier withdrawal; if that happens, we
publish the reason and the affected contract in the Changelog as soon as it is
safe to do so.

## Integration paths

Pick the flow that fits your product:

- **Hosted checkout** — call `POST /checkout`, redirect the buyer to the
  returned URL, receive the result via webhook. Lowest PCI scope.
- **Server-to-server** — call `POST /initialize-payment`, complete any 3DS
  challenge client-side, then call `POST /finalize-payment`. You handle card data.
- **Capture / refund** — once a payment is authorized or captured, use
  `POST /payments/{payment_id}/capture` and `/refund` for the full amount or partials.
- **Payouts** — `POST /payouts` to send money from a wallet to a registered
  payee's payout method.
- **Top-up invoices** — `POST /payments-api/v1/topup-invoices` issues an
  invoice a buyer settles by bank transfer, funding the seller's wallet once
  the deposit is matched. Enabled per deployment (`404` where it is off); ask
  Fynex before building on it.

### The open top-up invoice cap

A seller may hold at most **20 open** top-up invoices at a time — open being
`pending`, `outdated` or `marked_paid`. Issuing the twenty-first answers `422`
with `too many open top-up invoices`; settle or cancel one and the same request
succeeds.

The bound exists because every issued invoice burns one number from the shared
`(agreement number, YYMM)` invoice-number series, which is also drawn on by
billing and settlement invoices. It is a supply bound, not a rate limit:
retrying the same request without clearing an open invoice will keep answering
`422`, and there is nothing to back off for.

## Errors

Errors return JSON in the form `{ "error": "<message>", "code": "<code>" }`:
`code` is specific where the API can say more than the status, and otherwise
derived from it (`bad_request`, `not_found`, `conflict`, …). Status codes follow
the usual conventions:

| Code | Meaning |
|------|---------|
| 400  | Validation error |
| 401  | Missing or invalid bearer token |
| 404  | Resource not found for this seller |
| 409  | Conflict (e.g., invalid status transition, duplicate refund) |
| 422  | The request is well formed but not applicable in the seller's current state (e.g., the open top-up invoice cap above) |
| 500  | Internal error |
| 502  | Upstream provider failure |

## Amounts and currency

Request amounts are **major units** everywhere on this API (e.g. `19.99`), with one
exception: a split share in `orderData.payeeDistribution` takes `amountMinor`, integer minor
units (its major-units `amount` is deprecated).
Payouts take them as a decimal **string** (`"amount": "19.99"`); the integer
`amountMinor` on payouts is a deprecated fallback that `amount` overrides when
both are sent. Currencies are ISO 4217 codes (`GBP`, `EUR`, `USD`).


## Error format

Every error returns JSON with an `error` and a machine-readable `code`. Branch
on `code`, never on the `error` text, which may be reworded:

```json
{ "error": "This card has expired. Please use a different card.", "code": "card_expired" }
```

`code` is specific where the API can say more than the status (`card_expired`
above); otherwise it is derived from the status — `bad_request`,
`unauthorized`, `forbidden`, `not_found`, `conflict`, `unprocessable_entity`,
`rate_limited`, `internal_error`, `bad_gateway` and so on.

Status codes:

| Code | Meaning |
|------|---------|
| 400  | Validation error |
| 401  | Missing or invalid bearer token |
| 404  | Resource not found for this seller |
| 409  | Conflict (e.g., invalid status transition, duplicate refund). A refund refused by the seller balance check carries `code` `insufficient_balance` (the main wallet cannot cover the refund plus its fee), `balance_settling` (retry later: the payment's balance is still settling) or `refund_requires_review` (Fynex must review the payment first); no refund was created |
| 422  | Card refused with no funds moved (`code`: `card_expired`, `card_brand_not_supported`) — ask for a different card. On `POST /checkout` and payment-link creation, `code` `currency_not_supported`: no terminal of the account can take the currency (or declared country), and nothing was created |
| 500  | Internal error |
| 502  | Upstream provider failure |

## Quoting a failure to support

Every response carries `X-Request-Id` — a `req_`-prefixed identifier generated
at the boundary, on failures as well as successes, and on a 404 for a URL that
does not exist. Log it alongside your own request and quote it when reporting a
problem: it is what turns "a call failed this morning" into one line in ours.

It is generated here and never taken from your request, so it is a correlation
id and not a field you can set. If you need to carry your own, send it in a
header of your own choosing — and note that on writes, `Idempotency-Key` is the
one caller-supplied value this API does act on.


## Amounts and currency

Request amounts are **major units** everywhere on this API (e.g. `19.99`).
Payouts take them as a decimal **string** (`"amount": "19.99"`); the integer
`amountMinor` on payouts is a deprecated fallback that `amount` overrides when
both are sent. Responses add `…Minor` integer twins where the wire needs them.
Currencies are ISO 4217 codes (`GBP`, `EUR`, `USD`).

**Read the field name before the value — this is where the 100× mistake
lives.** The platform's house convention (the Billing API follows it
throughout, and new Payments fields adopt it) is:

| Value | Type | Field name | Example |
| --- | --- | --- | --- |
| Money amount | Integer, **minor units** | ends `Minor` | `4999` is £49.99 |
| Rate / percentage | Integer, **basis points** | ends `Bps` | `275` is 2.75% |
| Quantity | Decimal **string** | — | `"1250.5"` |
| Tax rate | Decimal **string**, percent | — | `"20.0"` |

Checkout's major-unit fields predate that convention and remain accepted.
The trap is real in both directions: `49.99` sent into a `…Minor` field
underpays a hundredfold, and `4999` sent into checkout's `totalAmount`
overcharges by the same factor — **neither errors**. When a field name ends
`Minor`, it is an integer count of the currency's smallest unit, everywhere,
on both APIs.
