# Workflows

End-to-end recipes for the things integrators actually build. Each one names
the endpoints, the order, and the failure modes worth handling.

## Collect payment on an invoice

Invoices are issued by the billing engine (subscription renewals, usage
period close) or by your team in the dashboard. This API turns an issued
invoice into something a customer can pay.

```bash
curl -s -X POST $FYNEX_API_BASE/invoices/4180/send \
  -H "Authorization: Bearer $FYNEX_API_KEY"
```

```json
{
  "invoice": { "id": 4180, "status": "sent", "paymentLinkId": 991, "...": "..." },
  "paymentLinkUrl": "https://pay.example-host/p/7bc1f2a9e4d5"
}
```

The URL points at the Fynex hosted payment page for your deployment (the host is configured per environment, so do not hard-code it — always use the URL this response returns). Send it to your customer, embed it, or redirect to it. When the document is
configured for email delivery, Fynex also emails the customer — calling this
endpoint is what triggers that.

**Retrying is safe, and needs no idempotency header.** The invoice's unique
payment-link association is the durable safeguard: an invoice that already has
a link returns that same link rather than minting a second, even for
simultaneous calls, and does not re-send the email.

**The payment carries the document.** Whichever rail collects an invoice — the
hosted link or off-session auto-charge — the resulting payment is stamped with
what it settled, so you never have to reconstruct it from an order reference:

- The payment link's `metadata` holds `billingInvoiceId`, `invoiceNumber`,
  `contractId`, `agreementNumber` and `sellerCustomerId`. The contract and its
  agreement number reach the payments side nowhere else.
- The payment row carries `billing_invoice_id`, and so does its checkout
  session — a direct answer to "which invoice did this payment collect", with
  no join through the link.
- The acquirer's order description reads `Invoice <number>` (Paysafe
  `description`, Solidgate `order_description`), which is what makes the
  document visible in acquirer dashboards and dispute correspondence. The
  merchant reference is deliberately unchanged: it is the duplicate-detection
  and settlement-mapping key and stays machine-shaped.
- `PaymentCompleted` carries `invoiceNumber` and `contractId` when the payment
  collected an invoice, so it can be joined to `InvoicePaid` and to the
  document itself.

Three cases answer `422`, and they are not retryable — the state has to
change first:

| Message | Meaning |
| --- | --- |
| `invoice is collected by bank transfer and cannot create a payment link` | The document instructs the buyer to wire money to a named account. A simultaneously payable card link would let a transfer and a card charge both succeed. |
| `invoice has nothing left to collect` | Zero total, or stored credit already covers it. There is nothing to ask the customer for. |
| `invoice is not in a sendable state` | Not issued yet (`draft`), or already `paid`, `voided` or `written_off`. A settled invoice never hands out a payment link. It is also the catch-all: if `status` reads `issued` and send still answers this, the document itself is faulty (a collection snapshot that is missing or unreadable) — nothing you send will fix it, so raise it with Fynex rather than retrying. |

## Know when an invoice is paid

`InvoicePaid` on the webhook pipe is the push signal (see **Webhooks**);
polling remains right for reconciliation.

Do not assume the settlement was something you or the buyer did. A seller can
enable off-session collection (`billing.invoice.auto_charge`), and then a sent
or overdue invoice is charged against the customer's saved card by a
background loop — `paidVia` still reads `payment_link`, because it settles on
the same rail. Sellers can likewise have documents raised for them by the
recurring and usage invoice passes. Both are per-seller operator switches, off
until turned on, and both mean `GET /invoices` can change with no call of
yours: reconcile against the list for a period rather than against the
documents you happen to have touched.

```bash
curl -s $FYNEX_API_BASE/invoices/4180 \
  -H "Authorization: Bearer $FYNEX_API_KEY"
```

Read `invoice.status`:

| Status | Meaning |
| --- | --- |
| `draft` | Not yet issued. Not a legal document; not collectible. |
| `issued` | Numbered and legally real; no collection instrument yet. |
| `sent` | A payment link exists; awaiting payment. |
| `paid` | Settled. `paidVia` says how: `payment_link` (hosted checkout), `bank_transfer` (a matched deposit) or `credit` (stored credit covered it in full). |
| `overdue` | Past its `dueDate` and still unpaid. |
| `voided` | Cancelled before money moved; `voidedAt` and `voidReason` explain. |
| `written_off` | Collection abandoned; `writtenOffAt` and `writeOffReason` explain. |

Treat this list as open: a status you do not recognize should not crash your
integration. Branch on the ones you handle and fall through for the rest.

Poll open invoices every few minutes at most; a fixed 15-minute sweep over
`GET /invoices` is usually better than polling each document. Bank transfers
settle when the deposit is matched, which is hours-to-days — do not build a
tight loop waiting for one.

A credit note (`invoiceType: "credit_note"`) is a **separate document** that
corrects an earlier one, named in `originalInvoiceNumber`. It does not change
the original: to compute what a customer owes, sum the invoice and its credit
notes rather than expecting the original to shrink. Fetch them with
`?corrects=<invoiceNumber>`.

## Reconcile an invoice against its payout

"The customer paid — where is the money?" is one call per invoice:

```bash
curl -s "$FYNEX_API_BASE/invoices/4180/settlement" \
  -H "Authorization: Bearer $FYNEX_API_KEY"
```

Read it in this order: each entry in `payments` is one attempt on the
invoice's collection link, failed ones included — the newest `settled` one is
your money. Inside it, each entry in `payouts` is a payout's claim on that
payment: `state: "finalized"` with a `completedAt` means it left; `reserved`
means it is scheduled; an empty `payouts` array means the payment settled but
has not been picked into a payout yet. `attributedMinor` is that payout's
slice — a payment can be split across payouts, and the slices sum to what
left.

An invoice settled by bank transfer or stored credit has no link trail, and
the response's `note` says which case you are looking at — do not treat an
empty `payments` list as an error. Payout detail beyond the id lives on the
Payments API; this call gives you the id to look it up with.

## Reconcile a period

**For month-end, take the file.** One call returns every invoice of the
window as CSV (or NDJSON — the list API's objects, one per line), with money
in integer minor units and column names that say so. Both dates are required;
there is deliberately no default window:

```bash
curl -s "$FYNEX_API_BASE/invoices/export?issuedFrom=2026-08-01&issuedTo=2026-09-01" \
  -H "Authorization: Bearer $FYNEX_API_KEY" -o invoices_2026-08.csv
```

The paged list below remains right when you are feeding an application rather
than closing books. Bound it by issue date rather than walking history until
you cross the boundary — the range is half-open (`issuedFrom` inclusive,
`issuedTo` exclusive), so adjacent months never double-count a document:

```bash
# First page of August
curl -s "$FYNEX_API_BASE/invoices?issuedFrom=2026-08-01&issuedTo=2026-09-01&limit=100" \
  -H "Authorization: Bearer $FYNEX_API_KEY"

# Next page: pass the previous response's nextCursor
curl -s "$FYNEX_API_BASE/invoices?issuedFrom=2026-08-01&issuedTo=2026-09-01&limit=100&cursor=4081" \
  -H "Authorization: Bearer $FYNEX_API_KEY"
```

Filters worth combining:

- `contractId=42` — one customer relationship.
- `sellerCustomerId=7` — every document addressed to one of your customers,
  across contracts. This is how you pull a customer statement without walking
  the whole period.
- `status=sent` — one lifecycle state (`draft`, `issued`, `sent`, `paid`,
  `overdue`, `voided`, `written_off`).
- `invoiceType=credit_note` — corrections only. The full set is `standard`,
  `credit_note`, `simplified` (retail below the threshold) and `modified` (UK
  retail above it, VAT-inclusive line prices).
- `corrects=UK2607AA-2608AAB` — the credit notes issued against one invoice
  number, without scanning history for them.
- `origin=usage` — the subsystem that produced the document: `recurring`,
  `usage`, `milestone`, `project`, `one_time`, `adhoc`, `marketplace`.
  (`subscription` is a deprecated alias of `recurring`: still accepted, no
  longer produced, and it resolves to the same set as `recurring` — see below.)

`origin`, `status` and `invoiceType` reject an unrecognized value with `400`,
so a typo cannot masquerade as "no such invoices". `contractId` and `corrects`
are matched as given: a well-formed but wrong value legitimately returns an
empty page, so check those two against `GET /contracts` and the invoice number
you meant.

`origin=recurring` and `origin=subscription` return the same rows. The second
is the pre-schema name, and documents issued before the rename still carry it
— filtering either way includes both, so a monthly report cannot silently lose
the older half.

`customerName` and `customerEmail` are returned on the single-invoice read and
on send, but **not** on list rows: they live in the document's frozen payload,
and reading them for a whole page would mean parsing every document. To group
a report by customer, filter with `sellerCustomerId` rather than fetching each
invoice — that keeps a monthly reconciliation to a handful of requests instead
of one per document.

For accounting, the figures per document are `grandTotalMinor` (what was
invoiced), `creditAppliedMinor` (funded by stored credit rather than cash),
and `collectibleMinor` (the grand total less that credit — what collection
asks the customer for).

Two traps worth stating plainly:

- **`creditSettledMinor` is not a payment total.** It counts only the stored
  credit drawn down; a card- or transfer-paid invoice reports `0` there.
- **`collectibleMinor` does not drop to zero when an invoice is paid.** It
  describes the ask, not the outcome.

So "has this been paid?" is answered by `status` (and `paidVia` for how), never
by arithmetic on the amounts.

Because paging is keyset, not offset, documents issued while you are walking
the pages never shift rows into or out of a page you already fetched. Start a
fresh walk to pick them up.

## Watch usage against plan limits

For metered contracts, the snapshot answers "where is this customer in the
current period":

```bash
curl -s $FYNEX_API_BASE/contracts/42/usage \
  -H "Authorization: Bearer $FYNEX_API_KEY"
```

```json
{
  "contractId": 42,
  "metrics": [
    {
      "metricName": "api_calls",
      "used": "10250",
      "includedUnits": "10000",
      "capQuantity": "50000",
      "capMode": "hard",
      "percentOfCap": "20.5",
      "percentOfPlan": "102.5",
      "periodStart": "2026-08-01",
      "periodEnd": "2026-08-31"
    }
  ]
}
```

Useful signals:

- `percentOfPlan` above 100 means the customer is into overage — that is what
  will be rated onto the next usage invoice.
- `capQuantity` with `capMode: "hard"` is the ceiling enforcement uses. Warn
  customers well before `percentOfCap` reaches 100 rather than at it.
- Quantities are decimal strings; compare with a decimal type.

An unknown `contractId` answers `404`. A real contract with no metered usage
answers `200` with an empty `metrics` array — that is a valid state, not an
error.

Usage aggregates continuously and closes at period end, so a snapshot is a
live read, not a final invoice figure. Reconcile money against invoices.

## Reconcile stored credit

Balances across all contracts:

```bash
curl -s $FYNEX_API_BASE/credits \
  -H "Authorization: Bearer $FYNEX_API_KEY"
```

```json
{
  "balances": [
    {"currency": "EUR", "creditType": "purchased", "balanceMinor": 250000, "isLiability": true}
  ]
}
```

`isLiability: true` marks credit that was **paid for** — `purchased`,
`enterprise` and the engine-minted `proration` (unserved time returned on a
mid-term downgrade) — which is unearned revenue you still owe service against.
Everything you granted (`promotional`, `manual`, `gift`, `ai_token`,
`marketplace`) is not a liability. Finance wants these separated, so do not
sum them blindly. Paid-for credit also never expires; granted credit may.

One contract's balances plus its ledger:

```bash
curl -s "$FYNEX_API_BASE/contracts/42/credits?limit=50" \
  -H "Authorization: Bearer $FYNEX_API_KEY"
```

The ledger is append-only and newest-first. `kind` is `topup` (credit granted
or bought), `deduction` (consumed into an invoice) or `expiry` (a lapsed lot
removed); `signedDeltaMinor` is positive for topups and negative for the other
two, and `invoiceId` links a deduction to the invoice it funded. Corrections are new rows, never edits, so
replaying the ledger from the beginning always reproduces the current
balance.

Page it the same way as invoices: `limit` sets the page, and while `hasMore`
is true you pass `nextCursor` back as `cursor`. A long-lived contract's
history exceeds one page, and a ledger you cannot read to the end cannot be
reconciled.

## See what the next invoice will carry

`GET /contracts/{contractId}/invoices/upcoming` projects the contract's next
document — before it exists, and without writing anything.

```bash
curl -s "$FYNEX_API_BASE/contracts/42/invoices/upcoming" \
  -H "Authorization: Bearer $FYNEX_API_KEY"
```

The `lines` array is the document as it would be composed, each line tagged
with the engine that produced it: `subscription` (a term falling due, priced
at a scheduled plan change when one takes effect by the term's end),
`proration` (an unbilled mid-term amendment the engine would bind to that
term — a charge as an extra line, a credit as a discount on the recurring
one), `usage` (an open metered period rated by the same engine
`POST /prices/evaluate` answers with) and `seat` (a per-seat period rated from
the contract's seat schedule, to date). `nextIssueDate` is the day the
document would appear, and `collectibleEstimateMinor` is what your buyer would
be asked for after stored credit is drawn down.

**Every figure is an estimate, and `estimate: true` says so on every
response.** Metered usage keeps accruing after `asOf`; the period close
resolves a rollover carry this projection assumes is zero; an amendment can
still move before the recurring pass binds it. Amounts are also **pre-tax**:
the engine copies a document's tax treatment from a prior invoice on the same
contract when it issues, so no tax is projected here and `grandTotalMinor`
equals `subtotalMinor`. Read the `notes` array — it names every reason the
figures are estimates and every exclusion made (an unpriced metric, a
foreign-currency line, a capability switched off), which is what stops a
number in this response from being read as a commitment.

Use it to show a customer what is coming, to check a plan change landed
before the term bills, or to catch an unpriced metric while there is still
time to price it. Reconcile against the **issued** document
(`GET /invoices/{invoiceId}`), never the other way round.

## Track subscription state

```bash
curl -s "$FYNEX_API_BASE/subscriptions?contractId=42" \
  -H "Authorization: Bearer $FYNEX_API_KEY"
```

To answer "is this customer currently being billed, and for how much", read
`status`, `priceMinor` + `currency`, and `billingFrequency`. There is no
separate plan object — those three fields *are* the plan.

Watch for:

- `pendingPriceMinor` / `pendingPriceChangeAt` — a scheduled change (usually a
  downgrade) that takes effect at term end. Showing only `priceMinor` will
  misinform a customer who already requested a change.
- `cancelRequestedAt` / `cancelEffectiveAt` — cancellation with notice. The
  subscription keeps serving until the effective date, so `status` alone is
  not "are they leaving".
- `trialEnd` with `status: "trial"` — not yet paying.
- `status: "past_due"` — collection failed. **What happens next depends on a
  switch the seller controls**, so do not hard-code either answer:
  - With off-session collection enabled (`billing.invoice.auto_charge`), a
    failed charge is retried on a bounded schedule — a few attempts, spaced
    apart — and the subscription is moved to `past_due` only once that budget
    is spent. It can come back on its own when a later payment succeeds, which
    is what `BillingSubscriptionRecovered` announces. Do not treat `past_due`
    as terminal and do not start your own retry loop against it; you would be
    charging the same card twice.
  - Without it, nothing retries: `past_due` is a queue for you to work, and
    the way out is the customer paying the outstanding invoice or a new card
    being added.

  In both cases the events to watch are `BillingSubscriptionPastDue` and
  `BillingSubscriptionRecovered`, and the invoice's `outstandingMinor` is what
  says how much is actually still owed.
- `pauseEndsAt` — an auto-resume date for a paused subscription.

Lifecycle changes (cancel, pause, plan change) are dashboard operations in
this version.

## Retries and rate limits

Reads are safe to retry. So is `POST .../send`: the invoice can hold only one
payment link, so a retry returns the existing one.

- `429` — you exceeded the per-seller budget. Wait the `Retry-After` seconds;
  do not retry sooner. `X-RateLimit-Remaining`, when present, lets you slow
  down before you hit it — the limiter fails open during an outage, and then
  no rate-limit headers are sent at all.
- `5xx` — retry with exponential backoff and jitter.
- `4xx` other than `429` — do not retry; the request has to change.

A retry loop that ignores `Retry-After` turns one throttled request into a
sustained overage. If you are paging a large history, a small delay between
pages is cheaper than being throttled mid-walk.
