# 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://api.fynex.ai/payments-api/v2/docs#tag/quickstart)** — Take your first test payment in 5 minutes.
- **[Hosted checkout](https://api.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://api.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://api.fynex.ai/payments-api/v2/docs#tag/payouts)** — Move money out from a wallet to a registered payee.
- **[Going live](https://api.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`
- **Staging (sandbox):** `https://staging-api.fynex.ai/payments-api/v1`

Both are declared in the OpenAPI document's `servers` block — production first,
the sandbox second, each labelled — so a generated client or an agent reading
only the spec can pick the sandbox instead of defaulting to production. 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. Demo accounts exist on both.

## Versioning

`/payments-api/v1` and `/payments-api/v2` serve the **same document and the same
routes**. They are not two versions you can choose between: `v2` was the path
the current spec was first published under during the docs migration, and `v1`
is aliased to it so existing bookmarks and generated clients keep resolving.
Requests to either prefix reach identical handlers.

**Use `/payments-api/v1`.** It is the path every example here, every SDK and
every guide uses. If you are pinned to `/v2` nothing is broken — but you are not
on a newer contract, and you will not be left behind by staying on `/v1`.

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://api.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.

### 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>" }`. 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

Most endpoints accept the amount in **major units** (e.g., `19.99`). Payouts
accept **minor units** (`amountMinor`, e.g., `1999` for £19.99) — see each
endpoint for specifics. Currencies are ISO 4217 codes (`GBP`, `EUR`, `USD`).


## Error format

All handler errors return JSON with a single `error` field:

```json
{ "error": "<message>" }
```

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

Most endpoints accept the amount in **major units** (e.g., `19.99`). Payouts
accept **minor units** (`amountMinor`, e.g., `1999` for £19.99) — see each
endpoint for specifics. 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.
