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.

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 invoice and reserve 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 Transaction ID, 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. Bill review, configuration import, approval reset, the read-state rebuild and Delete All Financial Data take none either.

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.

Two invoice identifiers: transactionId and invoiceNumber

Every invoice row carries two identifiers. transactionId is the invoice’s Transaction ID, INV-XXXXXX-XXX: the server generates it when the invoice is created, it is unique within the company, and it never changes or gets reused. Use it, or the invoice id, to name an invoice. invoiceNumber is the Invoice Number printed on the invoice by its sender, or null. It is optional free text with no uniqueness: trimmed, a blank value means none, and more than 255 characters returns 400. Create Invoice, Import Event Financials and every invoice plan (Update Policy Invoices, a policy transaction’s or bind by value’s invoicePlan, and a plan saved on a quote) accept it, and Update Invoice may change it before and after posting. An invoice a plan creates defaults to its own Transaction ID there when the plan states no invoiceNumber; a stated one, such as another system’s number for an imported policy invoice, is kept instead. Error bodies that name invoices, such as financialBlockers.sampleTransactionIds on a blocked delete, name them by Transaction ID.

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, …— every write’s 422 schema lists them, and a draft edit or re-link can also return DRAFT_ANNEX_CONFLICT). The policy invoice batch’s refusals use their own finv2-… codes, plus LIVE_PAYMENTS for a void of an invoice with payments (Invoice plans). 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; the policy invoice batch reads an unknown policy as one that states no billing (422 finv2-policy-invoice-missing-aggregate). 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. The policy invoice batch returns {journalIds, invoices, scheduledDates}: the policy’s whole invoice set after the batch. There is one invoice representation: reads and write responses share the same invoice shape, except that the batch’s invoice rows also carry approvedBy.

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 (number), Transaction ID (transactionId), 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 Transaction ID, and the request may carry the sender’s invoiceNumber; links (an event, 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 financial-definition corrections are void-and-recreate. The invoiceNumber remains editable after posting. Drafts still replace their document wholesale until finalize, with one exception: invoiceNumber. Leaving it out of the body keeps it on any invoice, draft or posted, and null (or blank) clears it. Links excluded either way (they change via the endpoints below).
  • Finalize Invoice (POST …/finalize) — post a draft: its charges reach the ledger and any draftPayments become real payments.
  • 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 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). A voided invoice keeps its original totalAmountCents and balanceDueCents on the read, so leave voided invoices out when adding up what is owed.
  • Approve Invoice (POST …/approve) / Unapprove Invoice (POST …/unapprove) — flip the document’s approval state; zero ledger rows. Payments recorded on a live invoice that requires approval (approvalRequired: true) need it, so approve before recording one — or let Record Payment approve for you (below). Parsed payment marks carried by a draft are the exception: Finalize Invoice materializes them without changing the draft’s approval state. Both approval actions need invoice.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.
Policy invoices. A policy-linked invoice allows an Invoice Number correction with all other document fields unchanged. Other document changes require the policy invoice batch. Create (with links.policy), Update beyond the number, Finalize, Void, Restore, Delete and Re-link (from a policy or onto one) refuse it with 422 POLICY_INVOICE_BATCH_ONLY, whose message names that policy’s batch and invoice read paths. Record Payment, Remove Payment, Approve, Unapprove, Change Invoice Payee (while it has no payments), bill review and Merge Payee still work on it. A voided policy invoice cannot be restored: the batch creates its replacement.
Which invoices require approval. Every invoice carries approvalRequired, recorded when it is created: true for money going out (a positive totalAmountCents) on an invoice type whose “Money-out invoices require approval” setting is on, false for money coming in and for types whose setting is off. A zero total follows its type’s expected direction. Each type’s setting is moneyOutRequiresApproval on List Transaction Categories, List Policy Invoice Types and the configuration export. Changing it through the API or a configuration import reaches new invoices only. Changing it in the app’s Financials settings offers to apply the change to the type’s open money-out invoices (draft, owed or partially paid); each one it reaches then reads the new approvalRequired. Paid, voided and deleted invoices never change. An invoice with approvalRequired: false takes payments without an approval, and nothing approves it; an approval it carries is kept but no longer matters.
Approval and payments. Recording a payment on an UNAPPROVED invoice that requires approval approves it automatically when the calling key holds invoice.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.
Draft payment annexes. Finalizing a draft applies its draftPayments as real payment marks but does not approve the invoice. An unapproved draft can therefore finalize as partially_paid or paid while still returning approved: false and approvedAt: null.

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 after-the-bind door for a policy’s invoices: one atomic plan that voids and creates any of them. A plan that voids every active invoice, or gives a policy with none its first, moves the policy between the billing and invoicing layers and names the move in pricingStackLayerChange (The invoice batch). See Invoicing a policy and Invoice plans.

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.
  • List Company Reserve Updates (GET /financials/reserve-updates) — every event’s reserve-update feed in one cursor-paginated read, each row with its event and running reserve: the rows of the app’s company-wide Reserve Updates export, for copying reserve history into a data warehouse.

Configuration discovery

  • Export Financials Configuration (POST /financials/config/export) — returns the complete, deterministically ordered Financials Config Document v2 using natural keys only. Policy line items are flat under policyFinancials.lineItemTypes, each with allowedOn: [{ invoiceType, label? }]. Omitted labels are unconfigured; null explicitly selects the canonical item name. Existing duplicate item names retain separate identities. Import refuses ambiguous identity matches and never merges existing items. Version 1 is no longer accepted.
  • Import Financials Configuration (POST /financials/config/import) — additively materializes missing vocabulary and reporting frames. Existing natural keys are never mutated, except that an invoice type takes a moneyOutRequiresApproval the document states (for new invoices only); ?dryRun=true validates and reports the exact plan without financials-config writes. ?expectedContentHash= with the export’s X-Content-Hash header refuses the import with 409 if the configuration changed since that export.
  • 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, or added with Import Financials Configuration.
  • Set Transaction Category Money-Out Approval (PUT /financials/config/categories/{categoryId}/money-out-approval) — sets whether a category’s money-out invoices require approval. The change reaches new invoices only.

Policy financials configuration

All policy vocabulary routes require financials.configure, including the two lists. List Policy Invoice Types and List Policy Line Item Types return IDs, deprecation marks and usage metadata; includeDeprecated=true includes all identities. Policy line items are independent company definitions and may be allowed on several invoice types, with a different display label on each. Create with POST /financials/config/policy-invoice-types or POST /financials/config/policy-line-item-types. Rename with PATCH /{id}, delete with DELETE /{id}, and deprecate/reactivate with POST /{id}/deprecate or POST /{id}/reactivate under the matching collection. Writes return the full refreshed Financials settings, including deprecated rows. The invoice-type create requires kind: "operating", expectedDirection, and lineItemTypes (which may be empty). Independent line-item creation takes only name and direction. Use PUT /policy-invoice-types/{id}/expected-direction, /payee-defaults, /presets, or /money-out-approval beneath /financials/config to replace those settings. A money-out-approval change reaches new invoices only. Set associations with POST /financials/config/policy-line-item-types/associations. These operations follow the same rules as the Financials settings page. A non-deleted invoice citation blocks rename and delete (vocabularyLock: "invoice"); deprecation keeps existing citations intact. Historical postings, deleted invoices, and policy pricing labels alone do not lock billing vocabulary. An original-parent invoice type with hasLegacyLineItems: true requires explicitly deleting its unused legacy line items first. Read both lists before a multi-step replacement and check each result: separate HTTP deletes are separate transactions. Import remains additive and never removes existing vocabulary.

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 (company.reset)

  • 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. Requires company.reset, and only on a Sandbox Company: both bulk wipes require Sandbox mode for initialized companies. Live and Retired companies return 409 CompanyModeConflict. Companies awaiting lifecycle initialization retain the legacy DeleteGuardConflict policy.

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 event link later for the price of one action.

Deliberate exclusions

The API deliberately does not offer a chart-of-accounts endpoint, time-travel query parameters, raw journal/ledger dumps, or a per-invoice action history. The configuration document import is additive portability, not CRUD. 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

Invoice writes are permitted by the invoice’s stage: draft; finalized (status owed or no_charges); paid (status partially_paid or paid); and void (status voided). Each stage has its own create, edit and delete permission, written invoice.create-<stage>, invoice.edit-<stage> and invoice.delete-<stage> below. Voiding has invoice.void-finalized and invoice.void-paid. A write that could be done as several separate requests is allowed in one request when the key holds the permissions for those requests. For example, deleting a paid invoice is allowed with invoice.delete-paid, or with payment.delete and invoice.delete-finalized (remove its payments, then delete it). A refused write returns 403 naming the permissions that would allow it. An approved invoice that requires approval (approved and approvalRequired both true) is changed only under the authority to approve it: Update Invoice, Re-link Invoice, Change Invoice Payee, Void Invoice, Restore Invoice, Delete Invoice, and a policy invoice batch that voids it, also require invoice.approve. Recording and removing payments do not.