Approve Invoice
Approves the invoice — the invoice.approved action: it stamps the
document’s approval state (approved: true plus approvedAt) and posts
NOTHING to the books. Approval is an authorization state, not money.
Money may only post against an approved invoice (paid ⇒ approved), so
this is the endpoint to call before
recording a payment.
Unapprove revokes it.
Authority. Approving over the API requires exactly one thing: the
calling key holds company.payment:approve. The company’s own approval
rules (per-approver amount and category limits, configured in the app)
scope the IN-APP approve action only — they are never applied to API
callers, who have no approver identity to match. A key without the
permission gets 403.
Attribution. The recorded approver is the acting identity behind the credential — for an API key, the user who created it. The journal record’s own author stays the External API service user, as with every write on this surface.
Precondition (422). The invoice must be neither voided
(INVOICE_VOIDED) nor deleted (INVOICE_DELETED). Re-approving an
already-approved invoice is a DEFINED no-op: still journaled (a fresh
actionId gets a fresh journal id), zero ledger rows, and the ORIGINAL
approval is kept — approvedAt does not move.
Concurrency + idempotency. Requires If-Match (the invoice’s current
headJournalId); the body carries the client-minted actionId — an
identical retry replays the original outcome.
Required permission: company.payment:approve
Authorizations
API key authentication. Send your raw API key as the Authorization header value with NO scheme prefix — Authorization: YOUR-API-KEY. Do NOT prefix it with Bearer or ApiKey, and do not use an X-API-Key header; those are not accepted.
Headers
The invoice's current headJournalId — the optimistic-concurrency watermark every single-invoice write after creation must send. Read it off any invoice read or write response and echo it verbatim (a bare uuid; an entity-tag dressing of it — "uuid" or W/"uuid" — is also accepted). Missing or malformed is a 400 (IF_MATCH_REQUIRED / IF_MATCH_INVALID); a stale value is a 409 IF_MATCH_CONFLICT whose body carries the current invoice. An idempotent actionId replay short-circuits BEFORE the watermark is evaluated.
Path Parameters
Company identifier
Invoice identifier
Body
Client-minted idempotency key — becomes the action's journal id. An identical retry replays the original outcome; reuse with a different payload is 409 ACTION_ID_REUSED
Optional display label for the source system's author (e.g. the integrator-side approver). Stamped as the journal record's display attribution; the acting principal stays the External API service user, so a label can never impersonate an in-app user. Ignored on idempotent replays
1 - 255Response
The approved invoice — refreshed read state (approved: true)
The response of every single-invoice write — refreshed read state, not an ack: the emitted journal id(s), THE invoice row (identical shape to the reads — one invoice representation everywhere; its headJournalId is the next If-Match), and the invoice's live payments. An idempotent actionId replay returns this same shape rebuilt from the original outcome, server-minted values included.
Every journal id the write emitted — the anchor action's id (the actionId you supplied) first, then any engine-minted siblings (a multi-mark payment's additional marks) and any companion the horizon rule composed (e.g. the reserve unwind of a pre-horizon eroding payment's removal, or a delete's payment sweep)
1THE invoice representation — the same shape everywhere an endpoint returns an invoice (listing rows, the detail read, and every write's refreshed-row response). headJournalId is the invoice's current journal head — the optimistic-concurrency token subsequent writes echo back as If-Match.
The invoice's live payment marks after the write, newest first
