# Credits

Stored value is a capability your deployment switches on. While it is off,
every endpoint below answers `501` with `billing credits are not enabled` —
and that is the honest answer, because with it off the billing engine does
not draw credit into invoices at all, so any balance shown would never be
consumed.

Credits are stored value held against a contract — customer prepayments,
promotional grants, goodwill. Balances are derived from an append-only
ledger; drawdown happens automatically when an invoice is issued against a
contract holding credit (visible on the invoice as `creditAppliedMinor`).

## Endpoints

- `GET /billing-api/v1/credits` — your balances across all contracts, one row
  per currency and credit type:

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

`creditType` is one of `promotional`, `purchased`, `manual`, `gift`,
`enterprise`, `ai_token`, `marketplace` or `proration`. `isLiability` marks
the paid-for types (`purchased`, `enterprise`, `proration`) — unearned revenue
you owe as service. Those never expire; granted credit may. `proration` is
minted by the engine rather than granted by you: when a mid-term subscription
amendment credits your customer more than the invoice it lands on can absorb,
the remainder becomes a `proration` lot — money the customer already paid for
service not rendered — and the next document draws it down.

- `GET /billing-api/v1/contracts/{contractId}/credits` — one contract's
  balances (always complete) plus a page of its ledger history. `limit` sets
  the page size (1–100, default 20); while `hasMore` is true, pass the
  returned `nextCursor` back as `cursor` to walk older entries:

```json
{
  "balances": [ ... ],
  "hasMore": true,
  "nextCursor": 1180,
  "nextBeforeId": 1180,
  "entries": [
    {
      "id": 1201,
      "kind": "topup",
      "creditType": "purchased",
      "signedDeltaMinor": 250000,
      "currency": "EUR",
      "invoiceId": null,
      "reason": "annual prepayment",
      "occurredAt": "2026-08-01T09:30:00Z"
    }
  ]
}
```

Entries are newest-first. `kind` is `topup`, `deduction`, `expiry` or
`reversal` — the last one returns a deduction to the customer when the invoice
it funded is cancelled. `signedDeltaMinor` is positive for `topup` and
`reversal`, negative for `deduction` and `expiry`, and `invoiceId` links a
deduction (and the reversal that undoes it) to the document it funded.

- `POST /billing-api/v1/contracts/{contractId}/credits/top-up` — grant a
  credit lot. This is the only way credit enters the ledger; `deduction`,
  `expiry` and `reversal` rows are the engine's and have no public route.

```bash
curl -s -X POST "$FYNEX_API_BASE/contracts/42/credits/top-up" \
  -H "Authorization: Bearer $FYNEX_API_KEY" \
  -H "Idempotency-Key: 3f0b6a1e-9c1d-4f10-9f2b-6f2a1c1e6a1e" \
  -H "Content-Type: application/json" \
  -d '{"creditType":"purchased","amountMinor":250000,"currency":"EUR","reason":"annual prepayment"}'
```

`Idempotency-Key` is required, and here the body **is** compared: the same key
with a different contract, `amountMinor` or `currency` answers `422` instead of
returning the first grant, because a top-up that never happened must not look
like one that did. A matching replay answers `200` with the original entry; the
first call answers `201`.

`currency` must equal the contract's own — credit is never converted. An
`expiresAt` is allowed only on granted credit: `purchased` and `enterprise`
were paid for, and money someone paid must not evaporate on a calendar date.
`sellerCustomerId` is optional attribution and must name one of your customers
(`POST /billing-api/v1/customers`); the contract remains the balance anchor.

Correcting a grant is not an edit — the ledger is append-only. Contact support
for a correcting entry.
