Skip to main content
The Financials API is the full read/write surface of the financials subsystem: invoices and their lifecycle, payments, per-event reserves, two composition endpoints (event import, payee merge), balance and reserve-update reads, configuration discovery, and a read-state consistency check/repair pair, plus the atomic policy-invoice batch. Every write is a recorded action on an append-only journal — the API is the action catalog with HTTP methods on it.
This page documents the contract of the current financials surface. Companies are enabled on it progressively; a company not yet enabled gets 404 from these endpoints (they behave as if they do not exist yet) and is served by the deprecated legacy endpoints below until its cutover. The policy-invoice batch is the exception: its separate policy-financials rollout gate returns 403 when disabled, never a flag-derived 404.

Design rules

These rules hold across the whole surface — each operation’s page restates only what is specific to it.

Idempotency: client-supplied actionId

Every write takes a client-minted actionId (a uuid) that becomes the recorded action’s journal id. Retrying the identical request replays the original outcome — server-minted values (invoice id and number, the created marks’ payment ids) included — instead of applying twice; duplicate resolution runs before every other check, so a replay succeeds even with a stale If-Match. Reusing an id with a different action or payload is 409 ACTION_ID_REUSED. A payment gesture becomes one action per per-line mark: the actionId anchors the FIRST mark’s action, sibling marks mint their own ids in the same transaction, and a replay reconstructs the full set. Compound endpoints follow the same rule per client-authored action: the event import takes one actionId per create and per reserve set, commits atomically, and short-circuits a retry when your ids are already journaled. The payee merge takes no actionIds at all — it is idempotent by convergence. The policy invoice batch also takes no actionId: its per-invoice watermarks protect the input snapshot and the whole policy scope is serialized in one database transaction.

Authorship: the optional author label

Every write except the policy-invoice batch accepts an optional author (1–255 characters) — a display label for the source system’s author, e.g. the person acting in your system. The batch body is exactly its detached explicit plan and has no author field. Where the app shows who recorded a change, a labeled row reads as your label; an unlabeled row reads as the built-in “External API” actor. Rows written through this API are always visibly marked as API-written regardless of the label — author is display attribution, never an identity: it cannot impersonate an in-app user, and the recorded acting principal is always the API surface itself. On an idempotent replay the original record — original label included — is returned and the retried author is ignored.

Concurrency: the If-Match watermark

Every invoice row carries headJournalId — the id of the last action that touched it. Every single-invoice write after creation must send it as If-Match. A stale value is 409 IF_MATCH_CONFLICT with the invoice’s current row in the body (error.currentInvoice): re-read, reconcile, retry with its headJournalId. A missing or malformed header is 400. Reserve writes and the compositions are exempt — they serialize on scope locks instead.

Stable error codes (422)

A write whose payload is well-formed but rejected by the books fails with 422 and a stable machine-readable code (LIVE_PAYMENTS, AMOUNT_EXCEEDS_BALANCE_DUE, INVALID_AMOUNT, INVOICE_VOIDED, LINE_ITEMS_IMMUTABLE, UNKNOWN_ID, …— the full 18-value enum is on every write’s 422 schema). Codes are contract; messages may be reworded. Preconditions fail closed: a rejected write leaves no journal record, no ledger change, no read-state change. Malformed payloads (unknown fields, both links.event and links.policy, both or neither payment mode) are schema-level 400s. Unknown path resources (invoice, event) are 404. A valid API key naming another company in the URL is 403.

Writes return refreshed read state

Every write’s response is the refreshed read state, not an acknowledgement: invoice writes return {journalIds, invoice, payments} (plus top-level paymentIds and createdPayments — the created mark rows — when recording a payment); reserve writes return the scope’s new expectedTotalCents. journalIds carries every journal id the write emitted — a payment’s sibling marks and companions included, e.g. the automatic reserve unwind composed when a pre-horizon eroding payment is removed. There is one invoice representation: reads and write responses share the same invoice shape.

Amounts and dates

All amounts are integer cents. On line items, direction never rides the sign: amountCents is the magnitude a user would type, and a negative amount is a credit within the item’s own frame — it posts opposite the line item type’s expected direction (e.g. a reversal or refund); the posting rule always comes from the type, never the sign. The invoice’s document scalars are oriented net-cash figures: totalAmountCents sums the line amounts oriented by their types’ directions (payable +, receivable −) — the net cash the document commits to move; amountPaidCents is the oriented sum of live payment marks — net cash moved so far; balanceDueCents is their difference — net cash remaining, positive = out, negative = in. A document whose oriented due is negative is an open credit (money in), and the due may move non-monotonically as opposite-direction lines settle. A payment is a per-line settlement mark, signed in its line’s frame, and settles that line’s remaining toward zero — per line, never as a document scalar; an invoice is paid when every line is settled, never when a scalar hits zero. The record-payment request names its marks in that same line frame, or asks for the whole balance due (see Record Payment). A reserve scope’s remaining reserve is likewise a signed balance — the identity Reserves + Paid = Expected Total is its only law, and the remaining may read negative (over-paid against a standing estimate). Balance reads return raw signed cents in the ledger convention with each account’s role and provenance direction (and each category’s expectedDirection) embedded; the carrying side derives from role × direction (assets/expenses-side accounts carry debit, liabilities/income-side carry credit), so clients orient displays themselves. Dates are ISO YYYY-MM-DD; timestamps are ISO 8601.

Cursor pagination

List reads are cursor-paginated: pass the previous page’s nextCursor back as cursor until it is null. Cursors are opaque — echo them verbatim.

API Endpoints

Invoice reads

  • List Invoices (GET /financials/invoices) — global and entity-scoped lists in one endpoint: filter by status, category, linked event/policy/payee, invoice number, and date ranges; cursor-paginated. Deleted invoices are hidden.
  • Get Invoice (GET /financials/invoices/{invoiceId}) — the whole current document with its live payments and headJournalId. Returns deleted invoices too (a delete’s confirmable terminal state).

Invoice writes

  • Create Invoice (POST /financials/invoices) — the server mints the id and invoice number; links (event or policy, plus payee) are optional and can attach later.
  • Update Invoice (PUT /financials/invoices/{invoiceId}) — once an invoice is POSTED its line items and category are immutable (echo the stored set verbatim; any deviation is 422 LINE_ITEMS_IMMUTABLE, at any payment count): the mutable remainder is dates, memo, and fieldData, and corrections are void-and-recreate. Drafts still replace their document wholesale until finalize. Links excluded either way (they change via the endpoints below).
  • Delete Invoice (DELETE /financials/invoices/{invoiceId}) — terminal; live payments are swept in the same action.
  • Re-link Invoice (POST …/relink) — attach/detach/move the event or policy link; zero ledger rows; requires no live payments.
  • Change Invoice Payee (POST …/payee) — set/clear/change the payee; requires no live payments.
  • Record Payment (POST …/payments) — either payBalanceDue: true (settle every open line, both directions) or explicit per-line allocations, exactly one per request; returns the created mark rows; erodeReserves steers reserve erosion on reserved-category invoices.
  • Remove Payment (DELETE …/payments/{paymentId}) — exact negation; the horizon rule’s companion reserve unwind composes automatically.
  • Void Invoice (POST …/void) / Restore Invoice (POST …/restore) — write the charges down to zero and back; both have defined zero-row no-ops (re-void, restore of a non-voided invoice).
  • Approve Invoice (POST …/approve) / Unapprove Invoice (POST …/unapprove) — flip the document’s approval state; zero ledger rows. Money may only post against an approved invoice (paid ⇒ approved), so approve before recording a payment — or let Record Payment approve for you (below). Both need company.payment:approve, which is also the whole authority: the company’s in-app approval rules never apply to API callers. Re-approving keeps the ORIGINAL approval; unapproving a paid or partially-paid invoice is 422 UNAPPROVE_PAID.
Approval and payments. Recording a payment on an UNAPPROVED invoice approves it automatically when the calling key holds company.payment:approve (in the same transaction, before the payment marks); a key without the permission gets 422 NOT_APPROVED instead. The auto-approval shows up as approved: true on the response’s invoice row and is deliberately absent from journalIds, so an idempotent actionId replay returns a byte-identical body.

Bill review

A bill review checks one invoice against the company’s own bill review rules and reports what it thinks is wrong with it. It is asynchronous: starting one queues background work and returns immediately, and the verdict arrives on the read.
  • Start Invoice Bill Review (POST …/bill-review) — 202 with the runId to poll. No request body, no actionId, no If-Match: the rules are the company’s own (snapshotted when the run is created) and the invoice version is pinned from its current head, so there is nothing to send, and a review records a verdict beside the invoice rather than journaling an action against it. Re-posting while a review of this invoice is under way creates nothing and returns outcome: in_flight with the run already running; once that run is terminal, the next call mints a fresh one. A company at its background-work capacity gets a 429 and nothing is queued.
  • Get Invoice Bill Review (GET …/bill-review) — the latest run, its verdict and its findings. Poll until status is succeeded or failed; passed is null until then, and stays null forever on a failed run, so read error there rather than reading an empty findings list as a pass. billReview is null when the invoice has never been reviewed.
Findings can be stale, and you check that yourself. invoiceHeadJournalId is the invoice version the run judged, pinned when the review was started; a review is never re-run because the document changed. If it differs from the invoice’s current headJournalId, the findings describe an earlier version — start a fresh review. Dismissals (dismissedAt) are stamps an operator applies in the app, not deletes, so dismissed findings are still returned; filter on the field if you only want outstanding ones.

Reserves

  • Set Reserves (PUT /financials/events/{eventId}/reserves/{categoryId}) — absolute “set expected total to X” per (event, reserved category) scope; previous total and delta are recorded server-side. X is unconstrained — a set below the scope’s paid-to-date is legal and leaves the remaining reserve negative.
  • Reset Reserve History (POST …/reserves/{categoryId}/history-reset) — zero the scope’s remaining expectation and mark the reserve-update feed’s horizon; idempotent-shaped, legal on deprecated categories.

Compositions

  • Import Event Financials (POST /financials/events/{eventId}/import) — wholesale refresh in one transaction: sweep the event’s current invoices, create the new slate, set expected totals. Importing replaces, it does not append.
  • Merge Payee (POST /financials/payees/{payeeId}/merge) — repoint every linked invoice at another payee, removing and re-recording payments so attribution follows; idempotent by convergence.
  • Update Policy Invoices (POST /financials/policies/{policyId}/invoices/batch) — the only document write door for policy-linked invoices. Submit the complete explicit keep/void/create plan; every void cites the invoice’s headJournalId, and kept plus created invoices must conserve the policy’s current pricing contract. The batch is atomic and never infers dates, payees, or schedules.

Balances and feeds

  • Get Balances (GET /financials/balances) — company-wide per-category rollups with per-account detail, raw signed cents.
  • Get Entity Balances (GET /financials/entities/{entityType}/{entityId}/balances) — one entity’s slice in the same shape (event, policy, or payee); also the read for a scope’s current expected total before setting reserves.
  • List Reserve Updates (GET /financials/events/{eventId}/reserve-updates) — an event’s reserve-update feed: user updates, automatic eroding-payment rows, and history-reset markers.

Configuration discovery

  • List Transaction Categories (GET /financials/config/categories) — read-only discovery of the category and line item type ids write payloads cite (includeDeprecated=true to resolve old references). Categories and line item types are configured in the app — this surface has no configuration writes.

Validation

  • Check Financials Read-State Consistency (GET /financials/validation/check) — read-only: verify a scoped window of the derived read state against a replay of the journal; returns the mismatch list.
  • Rebuild Financials Read-State (POST /financials/validation/sync) — rebuild the derived read state from the journal under an exclusive company-level lock; returns per-table rebuilt counts. Idempotent, safe at any time.

Operational reset (SUPER_ADMIN)

  • Delete All Financial Data (POST /financials/deleteAll) — hard-delete every financial record the company holds (invoices, payments, journal, ledger, balances) while keeping financial configuration (categories, line item types, approval config). The first rung of the breaking-config reset ladder: the per-type entity wipe (POST .../entities/{entityType}/deleteAll) rejects with 409 while financials hold a live claim, and this endpoint is its unblock path. SUPER_ADMIN-only. Both bulk wipes are additionally delete-guarded: a company whose delete guard is active or onboarding-active is rejected with 409 DeleteGuardConflict — the wipes require a non-active delete guard, set from the Control Plane.

Attachments ride the documents API

Invoice endpoints take no files. Upload documents through the platform Company Files API and they attach alongside the invoice — an invoice can arrive unlinked and gain its links later for the price of one action.

Deliberate exclusions

The API deliberately does not offer: configuration writes (categories and line item types are managed in the app), a chart-of-accounts endpoint, time-travel query parameters, raw journal/ledger dumps, or a per-invoice action history. The journal is the system’s internal source of truth; the read surface above is the contract. Bill review follows the same rule: the rules themselves are managed in the app, so there is no endpoint to write them, no endpoint to dismiss a finding, and no history of earlier runs — only the latest one per invoice.

Permissions