Skip to content
In this section

Workflows

End-to-end recipes: collecting and reconciling invoices, watching usage, tracking subscriptions.

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.

curl -s -X POST $FYNEX_API_BASE/invoices/4180/send \
  -H "Authorization: Bearer $FYNEX_API_KEY"
{
  "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>, 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. Where the deployment has off-session collection switched on (billing.invoice.auto_charge, a platform flag), 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 platform 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.

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:

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:

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:

# 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":

curl -s $FYNEX_API_BASE/contracts/42/usage \
  -H "Authorization: Bearer $FYNEX_API_KEY"
{
  "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:

curl -s $FYNEX_API_BASE/credits \
  -H "Authorization: Bearer $FYNEX_API_KEY"
{
  "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:

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), expiry (a lapsed lot removed) or reversal (a deduction handed back when its invoice was voided); signedDeltaMinor is positive for topup and reversal and negative for deduction and expiry, 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.

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

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 are public API operations, yours to make: POST /subscriptions/{subscriptionId}/cancel, /pause, /resume, /end-trial and /change-plan. Each route documents its allowed starting states — see Subscriptions. The dashboard is an alternative operator surface, not the only way to manage a subscription.

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. When the rate-limit headers are absent the surface-wide budget is not being enforced at that moment; the three write quotas (POST /contracts, /contracts/{contractId}/amendments, /invoices) instead answer 503 with Retry-After while their limiter is unreachable — see Errors.
  • 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.