GET/api/v1/billing/invoice-runs
GET /api/billing/invoice-runs — most recent runs first.
Responses
Security: bearer
POST/api/v1/billing/invoice-runs
POST /api/billing/invoice-runs — evaluate every billable contract over
the window (optionally one client's) and persist one invoice per
contract with at least one candidate line. Idempotency-keyed (IF1):
the same key with the same body replays the stored run.
Request body
CreateInvoiceRunRequest
Responses
| Status | Description |
|---|
200 | |
409 | mixed-currency run or idempotency key reused |
422 | validation failure |
Security: bearer
GET/api/v1/billing/invoice-runs/{id}
GET /api/billing/invoice-runs/{id} — one run's roll-up.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | run not found |
Security: bearer
GET/api/v1/billing/preview
GET /api/billing/preview — evaluate EVERY billable contract in the
org over `[from, to]` (cancelled contracts excluded; effective windows
must intersect). READ-ONLY: the org-wide dry-run of a Wave-9 invoice
run.
Parameters
| Name | In | Type | Required | Description |
|---|
from | query | — | No | Window start (inclusive), ISO date. |
to | query | — | No | Window end (inclusive), ISO date. |
Responses
| Status | Description |
|---|
200 | |
422 | missing or inverted window |
Security: bearer
GET/api/v1/billing/verification
GET /api/billing/verification — the workspace for one contract over
one window. READ-ONLY: no entry changes, no side effects.
Parameters
| Name | In | Type | Required | Description |
|---|
contract_id | query | string | Yes | The contract under review. Required: the workspace is
per-contract because pricing, rate card, and blocks are. |
from | query | string | Yes | Inclusive period start (YYYY-MM-DD). |
to | query | string | Yes | Inclusive period end (YYYY-MM-DD). |
Responses
| Status | Description |
|---|
200 | |
404 | contract not visible in scope (rls_sparse) |
422 | inverted or oversized window |
Security: bearer
POST/api/v1/billing/verification/entries/{id}/action
POST /api/billing/verification/entries/{id}/action — every in-context
entry edit, one envelope. Gated on the instant-first mutation
contract: same key + same payload replays, different payload 409s.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | Time entry id |
Request body
EntryActionRequest
Responses
| Status | Description |
|---|
200 | |
403 | own_entry: an approver cannot decide their own entry |
404 | entry not visible in scope (rls_sparse) |
409 | entry already invoiced; verification is pre-run only |
422 | missing reason, unknown action, or invalid value |
Security: bearer
POST/api/v1/billing/verification/signoff
POST /api/billing/verification/signoff — the close ritual (report
V8). The checklist booleans are RE-VERIFIED against the live
workspace, never taken on trust: signing off "every entry
dispositioned" while a draft entry sits in the window is a 422, not
a stored lie. The overlap warning rides the response.
Request body
SignoffRequest
Responses
| Status | Description |
|---|
200 | |
404 | contract not visible in scope (rls_sparse) |
422 | checklist claim does not match the live window |
Security: bearer
GET/api/v1/contracts
GET /api/contracts — list with optional client/status/effective-date
filters, ordered by contract number.
Parameters
| Name | In | Type | Required | Description |
|---|
client_id | query | — | No | |
status | query | — | No | Exact status: `draft|active|ended|cancelled`. |
effective_on | query | — | No | Only contracts effective on this date. |
Responses
Security: bearer
POST/api/v1/contracts
POST /api/contracts — instant-first (IF1/IF2): idempotency-keyed and
returns the read-your-write `mutation` metadata.
Request body
CreateContractRequest
Responses
| Status | Description |
|---|
200 | |
409 | duplicate contract number, idempotency key reused, or version conflict |
422 | validation failure |
Security: bearer
GET/api/v1/contracts/{id}
GET /api/contracts/{id} — the contract aggregate (agreement + rate
card + fee schedule).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
PATCH/api/v1/contracts/{id}
PATCH /api/contracts/{id} — instant-first (IF1/IF2) partial update.
Absent fields keep their values; `rates`/`fees`, when present,
replace the whole card/schedule. The version gate rejects stale
writes with a typed 409.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
PatchContractRequest
Responses
| Status | Description |
|---|
200 | |
404 | contract not visible in scope |
409 | version conflict, duplicate number, or idempotency key reused |
422 | validation failure |
Security: bearer
DELETE/api/v1/contracts/{id}
DELETE /api/contracts/{id} — removes the contract aggregate. A typed
404 (`rls_sparse`) means the target was never visible in scope.
The Wave 9 invoicing lane's billing-lock guard refuses the delete
once invoices reference the contract (typed 409).
The response is an acknowledgement, not the aggregate: deleting is a
commercial action, not a rate read (RB2), so a commercial-delete holder
without `financial:read` can still remove a contract and never receives
its money. Every rate-bearing surface in this module keeps its
`financial:read` gate.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | contract not visible in scope |
409 | contract has invoices |
Security: bearer
GET/api/v1/contracts/{id}/billing-preview
GET /api/contracts/{id}/billing-preview — evaluate this contract's
billing rules over `[from, to]` and return the draft invoice-line
candidates. READ-ONLY: nothing persists, no billed markers exist.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
from | query | — | No | Window start (inclusive), ISO date. |
to | query | — | No | Window end (inclusive), ISO date. |
Responses
| Status | Description |
|---|
200 | |
404 | contract not found |
422 | missing or inverted window |
Security: bearer
POST/api/v1/contracts/{id}/change-orders
POST /api/contracts/{id}/change-orders — create a DRAFT order. The
lifecycle forward-path (approve → billed at run time) keeps billing
honest: only APPROVED orders ever price, and the run itself records
which invoice consumed them.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
CreateChangeOrderRequest
Responses
| Status | Description |
|---|
200 | |
404 | contract not visible in scope |
409 | duplicate change order number or idempotency key reused |
422 | validation failure |
Security: bearer
POST/api/v1/contracts/{id}/change-orders/{order_id}/transition
POST /api/contracts/{id}/change-orders/{order_id}/transition — move a
draft order through its lifecycle. `approve` stamps the approver +
time (the audit trail's who/when); `reject`/`cancel` are terminal.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
order_id | path | string | Yes | |
Request body
TransitionChangeOrderRequest
Responses
| Status | Description |
|---|
200 | |
404 | order not visible in scope |
409 | transition not allowed from the current status |
422 | validation failure |
Security: bearer
POST/api/v1/contracts/{id}/sla-credits
POST /api/contracts/{id}/sla-credits — record a pending service
credit. NEGATIVE at billing: the next run whose window covers
`applies_on` prices it as the invoice's `sla_credit` line.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
CreateSlaCreditRequest
Responses
| Status | Description |
|---|
200 | |
404 | contract not visible in scope |
422 | validation failure |
Security: bearer
POST/api/v1/contracts/{id}/sla-credits/{credit_id}/cancel
POST /api/contracts/{id}/sla-credits/{credit_id}/cancel — withdraw a
pending credit before any run consumes it. Applied credits are
immutable (additive-ledger rule: corrections issue NEW credits).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
credit_id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | credit not visible in scope |
409 | credit already applied |
Security: bearer
GET/api/v1/credit-notes
GET /api/credit-notes — newest first, client/status filters.
Parameters
| Name | In | Type | Required | Description |
|---|
client_id | query | — | No | |
status | query | — | No | Exact status: `issued` | `void`. |
Responses
Security: bearer
POST/api/v1/credit-notes
POST /api/credit-notes — issue a client credit through the
instant-first write path.
Request body
IssueCreditNoteRequest
Responses
| Status | Description |
|---|
200 | |
404 | client or invoice not visible in scope |
409 | idempotency key reused |
422 | validation failure |
Security: bearer
GET/api/v1/credit-notes/{id}
GET /api/credit-notes/{id} — the credit plus its application ledger.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | not visible in scope |
Security: bearer
POST/api/v1/credit-notes/{id}/apply
POST /api/credit-notes/{id}/apply — allocate part of the remaining
credit onto an issued invoice. The allocation inserts a real payment
receipt (provider `credit_note`) through the same apply path as
ordinary payments, then moves the credit's counter in the SAME
transaction (both compare-and-set: over-allocation is impossible).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
ApplyCreditNoteRequest
Responses
| Status | Description |
|---|
200 | |
404 | credit note or invoice not visible in scope |
409 | not enough remaining credit, invoice not payable, version conflict, or idempotency key reused |
422 | validation failure |
Security: bearer
POST/api/v1/credit-notes/{id}/refund
POST /api/credit-notes/{id}/refund — pay part of the remaining credit
back to the client. v1 approves the refund locally (none-mode
provider, exactly like charges); the amount may never exceed the
remaining credit.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
RefundCreditNoteRequest
Responses
| Status | Description |
|---|
200 | |
404 | credit note not visible in scope |
409 | not enough remaining credit, version conflict, or idempotency key reused |
422 | validation failure |
Security: bearer
POST/api/v1/credit-notes/{id}/void
POST /api/credit-notes/{id}/void — withdraw a note that has moved
nothing. Terminal.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
VoidCreditNoteRequest
Responses
| Status | Description |
|---|
200 | |
404 | credit note not visible in scope |
409 | already void, has allocations, version conflict, or idempotency key reused |
Security: bearer
GET/api/v1/invoices
GET /api/invoices — list with client/contract/status/dunning filters.
Parameters
| Name | In | Type | Required | Description |
|---|
client_id | query | — | No | |
contract_id | query | — | No | |
status | query | — | No | Exact status: `draft|issued|paid|void`. |
dunning_state | query | — | No | Exact dunning state:
`none|reminder_sent|overdue|final_notice|collections`. |
Responses
Security: bearer
GET/api/v1/invoices/{id}
GET /api/invoices/{id} — the invoice aggregate (document, lines,
payment receipts).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | invoice not found |
Security: bearer
GET/api/v1/invoices/{id}/document
GET /api/invoices/{id}/document — the print-ready HTML invoice
document (print-to-PDF from any browser; v1 ships no PDF binary
output — see the private build tracker).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | invoice not found |
Security: bearer
POST/api/v1/invoices/{id}/dunning
POST /api/invoices/{id}/dunning — move an ISSUED invoice's dunning
state forward (none → reminder_sent → overdue → final_notice →
collections; skipping rungs is allowed, backwards is refused).
States are data: the write emits `invoice.dunning_changed` and lands
in the write trail. Instant-first gated (IF1/IF2).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
DunningTransitionRequest
Responses
| Status | Description |
|---|
200 | |
404 | invoice not visible in scope |
409 | invoice not issuable, version conflict, or idempotency key reused |
422 | backwards or same-state transition |
Security: bearer
POST/api/v1/invoices/{id}/dunning/statement
POST /api/invoices/{id}/dunning/statement — generate an account
statement (the client's OPEN invoices with outstanding balances) and
queue it to the client's contacts through the outbound ledger
(`MailTransport`; none-mode stays queued like every other leg). The
statement rides the dunning path but is a document delivery, not a
dunning-state change. Instant-first gated (IF1).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
SendStatementRequest
Responses
| Status | Description |
|---|
200 | |
404 | invoice not visible in scope |
409 | invoice not issuable or idempotency key reused |
422 | no contact emails on the client to deliver to |
Security: bearer
GET/api/v1/invoices/{id}/payments
GET /api/invoices/{id}/payments — the payment receipts.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | invoice not found |
Security: bearer
POST/api/v1/invoices/{id}/payments
POST /api/invoices/{id}/payments — record a payment through the
`PaymentProvider` seam. The charge runs INSIDE the mutation
transaction so receipt, payment row, and invoice status commit (or
roll back) atomically. Instant-first gated (IF1/IF2).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
RecordPaymentRequest
Responses
| Status | Description |
|---|
200 | |
404 | invoice not visible in scope |
409 | invoice not payable, overpayment, version conflict, or idempotency key reused |
422 | validation failure |
Security: bearer
POST/api/v1/invoices/{id}/void
POST /api/invoices/{id}/void — withdraw an ISSUED invoice. Terminal:
consumption markers stay, so re-running the window can never
double-bill. Instant-first gated (IF1/IF2).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
VoidInvoiceRequest
Responses
| Status | Description |
|---|
200 | |
404 | invoice not visible in scope |
409 | invoice not voidable, version conflict, or idempotency key reused |
Security: bearer
POST/api/v1/webhooks/stripe
stripe_webhook_receive
Request body
array<integer>
Responses
| Status | Description |
|---|
200 | |
400 | |
403 | |
409 | the invoice refused the settlement: it left its payable state, its total moved since the event was priced, or the amount exceeds the balance |