Finance & Budgets API
The Finance API exposes the same routes the product uses, with the same validation and permissions. See the Finance Setup and Budgets guides for the product behaviour.
All routes use API key authentication and run
with the key owner’s live permissions. Finance must be turned on for the
organisation; otherwise workspace routes return 409 with
code: "FINANCE_NOT_ENABLED". The settings routes stay reachable so Finance can
be turned on through the API.
Optional tracking features
Section titled “Optional tracking features”GET /api/v1/finance/status includes tracking: { time, expenses }, with resolved
boolean values. Both follow Finance activation by default. A settings manager
can PATCH /api/v1/organizations/:id with
{ "settings": { "tracking": { "time": true, "expenses": false } } }.
Each field is optional. Boolean values override Finance; null restores its
default. Unknown fields and other value types are rejected. Partial writes keep
the other feature’s choice. Normal organisation binding and settings permission apply.
New time entries, timers, schedule conversions and expense drafts return 409 when the feature is off. Task completion still succeeds and ignores supplied actual hours in that mode. Historical reads and corrections retain their permissions. Expense participation can be enabled without Finance, but billing entity and category setup are still required.
GET /api/v1/finance/expenses/options requires expense access and returns
billingEntities (id, isDefault, defaultCurrency), expenseCategories (id, name,
code, mileage, perDiem, disbursement) and taxRates (id, billingEntityId, name,
code). These claim-form choices do not require a Finance seat or expose financial
account configuration. The endpoint is also available through call_public_api.
Receivable document intake
Section titled “Receivable document intake”All routes below are under /api/v1/finance/receivable-documents. They require
enabled Finance, finance:manage_receivables and price access for the document’s
client. Invoice import additionally requires draft or issue authority; payment
confirmation requires finance:record_payments.
| Method and suffix | Behaviour |
|---|---|
GET / | List documents. Optional status, billingEntityId, clientOrganizationId, limit (1–100) and offset. Returns documents and visible total. |
POST / | Upload and automatically start extraction. Returns document and duplicate; 201 for a new upload, 200 for identical bytes already uploaded in this organisation. |
GET /:id | Return document, suggested invoice matches, duplicateInvoices and existingPayments. Optional invoiceQuery (up to 100 characters) filters invoice numbers. Up to 200 candidates are returned; narrow the search for larger lists. |
GET /:id/file | Original PDF/image bytes, with private/no-store headers. Add download=true for attachment disposition. |
PATCH /:id | Save reviewed fields, billingEntityId, nullable clientOrganizationId and expectedRevision. Optional kind corrects the document type. |
POST /:id/extract | Retry reading with expectedRevision. A new attempt clears the prior review acknowledgement. |
POST /:id/import-invoice | Create a draft, or link existingInvoiceId, using expectedRevision. |
POST /:id/confirm-payment | Explicitly record allocations or link existingPaymentId. |
POST /:id/dismiss | Retain the source but stop processing, using expectedRevision. |
POST /:id/restore | Restore a dismissed document to received, with review acknowledgement cleared. Uses expectedRevision; imported or reconciled records cannot be restored. |
Upload JSON contains kind (remittance or invoice), billingEntityId,
optional nullable clientOrganizationId, fileName, contentType and
contentBase64. Content types are application/pdf, image/png, image/jpeg
and image/webp. Decoded bytes must match the type and be no larger than 10 MB.
The document has status, revision, original file metadata, editable fields,
original extractedFields, confidence, reader note/error, reviewedAt, and
linked invoiceId or paymentId. Statuses are received, extracting,
review_ready, failed, imported, reconciled and dismissed. Poll while
extracting; failures support retry or manual review.
In this intake contract, monetary field values and match amounts are bare
integer minor-unit strings, paired with currency. Currency codes are
normalised to uppercase, and surrounding whitespace is removed from document
and payment references. Dates use YYYY-MM-DD.
Fields include clientName (a hint only), invoiceReference, paymentReference,
documentDate, dueDate, currency, subtotalAmount, taxAmount, totalAmount,
description, lineItems and invoiceReferences. Each invoice line has
description, decimal-string quantity, unitPrice and an explicitly reviewed
taxRateId (or null for no tax). Model-provided tax IDs are discarded.
References have reference and optional amount. Arrays are limited to 200.
A proposal is not an acknowledgement: save reviewed fields before any import or payment action. A stale revision returns 409. Invoice imports check the original reference for duplicates and compare calculated figures with supplied source totals. Imports create drafts with retained source references, without issuing or sending them.
Payment confirmation takes expectedRevision, paymentReceived: true, and
explicit allocations: [{ invoiceId, amount }]. The allocations must be positive,
within current outstanding balances, and sum exactly to fields.totalAmount.
Optional method is bank_transfer, card, direct_debit, cheque, cash or
other. A matching recorded payment can be linked using existingPaymentId
and allocations: []. If a similar payment exists and a new payment is intended,
provide duplicateReason. Period and payment permission failures apply normally.
Repeated terminal confirmation returns the already-linked record.
These operations are available through call_public_api, including base64
uploads and binary download responses. Pass confirm: true for writes. The
internal propose_receivable_fields tool is restricted to a server-created
extraction task; external clients use the review and confirmation routes.
See document intake.
Choose Finance usage
Section titled “Choose Finance usage”POST /api/v1/finance/usage accepts usage: "full" or
usage: "resource_planning". Both require an active membership and
finance:manage_settings plus organization:manage_settings. Enabling also
requires the Finance seat entitlement.
For new full setup, send legalName, two-letter jurisdictionCountry and
taxRegistered. Supply taxIdentifier when registered; an Australian ABN is
validated. Optional fields are tradingName, defaultCurrency,
financialYearStartMonth (1-12), addressLine1, city, stateProvince,
postalCode and billingEmail. Unknown fields return 400.
The response includes usage, enabled, defaultBillingEntityId, settings and
setupChecklist. Resource planning disables Finance and financial planning
without deleting records or removing clients. Ordinary POST /finance/enable
can enable it again. Writes and their audit record commit together.
MCP offers configure_finance_usage with the same fields and authority, and
get_finance_setup for the authorised setup checklist. call_public_api retains
the same route validation, organisation binding and failure behaviour.
Money and visibility
Section titled “Money and visibility”Most financial record amounts use an object with minor units as a string and an ISO currency code:
{ "amount": "740000", "currency": "AUD" }740000 in AUD is $7,400.00. Send money the same way on writes, except line
fields such as unitPrice, discountAmount, capAmount and estimatedCost,
which take the minor-unit string alone in the budget’s currency. Quantities and
hours are decimal strings such as "40" or "1.5".
Responses are stripped by the caller’s view capabilities. A field the caller
cannot see is absent, not null:
| Missing capability | Absent fields |
|---|---|
finance:view_prices | sold, price, rate, invoiced and money custom field values |
finance:view_cost | estimated cost, cost and committed cost |
finance:view_profit | margin |
Financial record responses generally carry lens, the resolved
{ prices, cost, profit } flags for the caller on that subject.
Errors
Section titled “Errors”Business rule refusals return 409 with a code:
| Code | Meaning |
|---|---|
FINANCE_NOT_ENABLED | Finance is not turned on |
BUDGET_NOT_ACTIVE | The action needs an active budget |
BUDGET_MANAGED_BY_OBJECT | The project’s budget figure is managed by a budget and cannot be edited directly |
BUDGET_NO_LINES, BUDGET_LINE_UNPRICED | Activation needs at least one line and a price on every billable line |
SCHEDULE_UNBALANCED | The billing schedule does not add up to the schedule base; the response names the unallocated amount |
RATE_CARD_CONFIRMATION_REQUIRED | The project’s rate card confirmation is still pending |
CHANGE_ORDER_REQUIRED | The edit changes sold or a cap, or an applicable change order is already open. Edit the version instead of live lines |
CHANGE_ORDER_INPUTS_CHANGED | Saved line inputs changed since opening, or an older proposal has ambiguous operational differences. Review, withdraw and reopen it |
CHANGE_ORDER_OPEN | Another change order is already open |
VERSION_BASE_MISMATCH | The budget moved on since the change order was opened |
APPROVAL_REQUIRED, APPROVAL_PENDING, APPROVAL_STALE, APPROVAL_NO_ELIGIBLE_APPROVER | Approval outcomes when applying a change order |
BUDGET_PENDING_APPROVALS, BUDGET_UNINVOICED, BUDGET_INVOICED | Deliver, close and cancel guards |
LINE_NOT_TRACKING, BUDGET_OVERRUN_BLOCKED | Time cannot be recorded against the line |
TAX_RATE_LOCKED | A used tax rate’s percentage cannot change |
NUMBER_COLLISION | A rendered number already exists in the sequence |
BANK_CHANGE_PENDING | A bank account change is already waiting for confirmation |
INVALID_TRANSITION | The status change is not allowed from the current status |
TIMESHEET_LOCKED | The entry’s date is behind the timesheet lock; needs time:manage_locked and lockOverrideReason |
PERIOD_CLOSED, PERIOD_SOFT_CLOSED | The date falls in a closed period, or in a soft-closed one and needs periodReason. Both carry details.period (YYYY-MM) |
TIME_ENTRY_STATE_CHANGED | The entry changed while the request was in flight; read it again and retry |
TIME_ENTRY_SUBMITTED, TIME_ENTRY_APPROVED, TIME_ENTRY_INVOICED, TIME_ENTRY_WRITTEN_OFF, TIME_ENTRY_NOT_APPROVED | The entry is in a state that refuses the action |
TIMER_RUNNING, TIMER_STOPPED | One timer per person; the timer is already running or already stopped |
SEGMENT_DECIDED | The approval segment was already decided |
WIP_ADJUSTMENT_STATE | The write-off is not in a state that allows the action |
WIP_SOURCE_CHANGED | The time in the proposal changed since it was proposed; propose it again |
WIP_REVERSAL_EXISTS | The write-off already has a reversal in progress |
WIP_ADJUSTMENT_EMPTY | The chosen time has no recognisable value left to write off |
TIME_ENTRY_UNVALUED | The chosen time has no rate or no exchange rate for its day, so it cannot be valued |
OVERHEAD_RATE_EXISTS | An overhead rate already starts on that date |
NOTHING_TO_INVOICE | The chosen sources produce no lines |
SOURCE_RESERVED, SOURCE_UNAVAILABLE | Another draft holds the work, or it was invoiced or credited since the draft was made |
INVOICE_IMMUTABLE, INVOICE_ALREADY_ISSUED, INVOICE_NOT_ISSUED | The invoice’s state refuses the change |
LINE_COMPUTED | Deposit and retention lines are calculated, not edited |
INVOICE_INVALID | Issue was refused; the response lists the blocking issues |
PERIOD_SOFT_CLOSED | The date falls in a soft-closed period, so a reason is required |
RENDER_PENDING | The document is still being prepared; the response gives its status and attempts |
CREDIT_EXCEEDS_LINE, DEPOSIT_APPLIED | The credit note is larger than the line, or the deposit must be reversed first |
INVOICE_PAID, INVOICE_CREDITED | The invoice cannot be voided in that state |
OVER_SETTLEMENT | A refund or reversal is larger than what was settled (409) |
REFUND_SOURCE_ALLOCATION | Earlier refund allocations do not reconcile to cash receipts (409); no new refund is recorded |
SEGREGATION_OF_DUTIES | The same person cannot both raise and settle when segregation is on |
A budget or catalogue record in another organisation returns 404.
Status and settings
Section titled “Status and settings”Settings routes need finance:manage_settings unless noted.
| Method | Path | Notes |
|---|---|---|
GET | /api/v1/finance/status | Any member. Returns enabled, entitled, enforcementMode, hasFinanceAccess, canManageSettings and lens; settings holders also get setupChecklist, settings, defaultBillingEntityId and suggestedJurisdiction |
POST | /api/v1/finance/enable | Turns Finance on. Optional legalName, tradingName, jurisdictionCountry, taxRegistered, taxIdentifier, defaultCurrency, financialYearStartMonth. 201 the first time, 200 after |
PATCH | /api/v1/finance/settings | budgetWarnAt (array of percentages), defaultOverrunMode (warn, block_tracking, block_approval), membersHoursOnly, segregationOfDuties, financialYearStartMonth, targetMarginBp, defaultBillingEntityId. Time policy: timeRoundingMode (none, nearest, up, down), timeRoundingMinutes (6, 15 or 30), timeApprovalDefault (required, auto), timeApprovalPolicyType (any, all), timeRequiredFields ({ note, task }), expectedHoursPerWeek, timesheetLockMode (off, weekly, monthly), timesheetLockGraceDays, timesheetLockedBefore (date or null), scheduleConversionEnabled, periodSoftCloseDay |
GET, POST, PATCH | /api/v1/finance/billing-entities[/:id] | One entity per organisation for now. An Australian ABN is checksum validated |
GET | /api/v1/finance/billing-entities/:id/number-sequences | Each sequence with preview.next and preview.example |
PATCH | /api/v1/finance/number-sequences/:id | format, reset, separateByClient, startValue (before the first number only) |
GET | /api/v1/finance/number-sequences/:id/audit | Every number issued, its subject and status |
GET, POST, PATCH | /api/v1/finance/tax-rates[/:id] and POST .../:id/archive | |
GET, POST, PATCH | /api/v1/finance/activity-codes[/:id], POST .../:id/archive, POST /api/v1/finance/activity-codes/seed-from-rate-card | Reads need any finance capability |
GET, POST, PATCH | /api/v1/finance/expense-categories[/:id] and POST .../:id/archive | Disbursement categories refuse a markup |
GET | /api/v1/finance/billing-entities/:id/bank-accounts | Masked accounts and change history |
POST | /api/v1/finance/billing-entities/:id/bank-accounts/changes | Needs procurement:manage_bank_details. Proposes a new account or a change |
POST | /api/v1/finance/bank-account-changes/:id/confirm, .../reject | A different active member with bank-details permission confirms within 24 hours |
GET | /api/v1/finance/bank-accounts/:id/reveal | Needs procurement:manage_bank_details. Returns the full details and records an event |
GET, POST, PATCH | /api/v1/finance/fields[/:id] and POST .../:id/archive | Custom field definitions: entityKind, type, required, options |
GET | /api/v1/finance/jurisdictions/:code/rules | Statutory rules with their verified flag |
GET | /api/v1/finance/billing-entities/:id/periods | Accounting periods. Retained closedChecklist values follow the caller’s current Finance lens; sales amounts are omitted without organisation-wide prices access, including after reopening |
GET, POST | /api/v1/finance/billing-entities/:id/overhead-rates | Overhead per hour added to cost. Read needs the cost lens or settings; write needs settings: hourlyRate (minor units), currency, effectiveFrom, basis. A second rate from the same date returns 409 OVERHEAD_RATE_EXISTS |
GET | /api/v1/finance/events | The organisation’s finance audit log, redacted by lens |
Approval policies for the budget change order gate use the
Approvals API with rules[] and an
optional billingEntityId.
Budgets
Section titled “Budgets”Writes need finance:manage_budgets. Reads need any finance capability in a
scope covering the budget’s project or client; the lens decides which figures
come back.
| Method | Path | Notes |
|---|---|---|
GET | /api/v1/finance/budgets | Filters: status (comma list), clientOrganizationId (internal for none), projectId, ownerUserId, kind, q, fieldKey with fieldValue (a custom field value: text, or one option of a select or multi-select), limit, offset. Each budget carries a summary. limit is capped at 200; the response carries total, limit and offset, so page with offset rather than asking for more |
GET | /api/v1/finance/projects/:projectId/budgets | The project’s budgets, unmappedTaskCount, legacyBudgetSource, legacyBudget |
GET | /api/v1/finance/master-projects/:masterProjectId/budgets | Every child project’s budget with its summary, projectsWithoutBudget, and allocationLimit from the master project’s overall budget. Each budget is redacted by the lens for its own project and client, so one row can carry prices while another does not, and a budget you cannot read is left out. allocationLimit is absent without the prices lens, and the response lens is what every row has in common |
POST | /api/v1/finance/budgets | name; optional projectId, clientOrganizationId, kind (project, retainer, internal), currency, ownerUserId, startDate, endDate, rateCardId, templateId, warnAt, overrunMode, clientVisible |
POST | /api/v1/finance/projects/:projectId/budgets/convert-legacy | Turns the project’s single figure into a budget with one fixed “Project fee” line |
GET | /api/v1/finance/budgets/:id | budget, sections, lines, totals, schedule, versions (without snapshots), fieldValues, openVersion, unmappedTaskCount |
PATCH | /api/v1/finance/budgets/:id | Header fields, fieldValues, and the time policy: timeApproval (inherit, auto, required), timeApprovalPolicyType (any, all), timeApprovers (array of { kind, id? } with kind budget_owner, project_owner, user or group), timeRoundingMode, timeRoundingMinutes |
DELETE | /api/v1/finance/budgets/:id | Discards a draft |
POST | /api/v1/finance/budgets/:id/activate, deliver, close, cancel, reopen, set-primary, reprice | reason (up to 1000 characters; non-blank for cancel and reopen). Reopen changes only closed budgets to delivered, retaining lines, amounts and version history; confirmRateCard: true on activate to confirm a pending rate card |
Sections and lines
Section titled “Sections and lines”| Method | Path | Notes |
|---|---|---|
POST, PATCH, DELETE | /api/v1/finance/budgets/:id/sections[/:sectionId] | name, description, position |
POST, PATCH, DELETE | /api/v1/finance/budgets/:id/lines[/:lineId] | See the line fields below |
PUT | /api/v1/finance/budgets/:id/lines/order | order: [{ id, position, sectionId? }] |
Line fields: sectionId, serviceTypeId (the activity code), name,
description, billingType (time_and_materials, fixed, non_billable,
percentage, expense_passthrough), unit (hour, day, item, percent),
quantity, unitPrice, rateCardRoleId, rateBasis (line or member),
discountBp, discountAmount, markupBp, capAmount, estimatedHours,
estimatedCost, tracksTime, tracksExpenses, bookable, taxRateId,
recognitionMethod, percentageBase (section or budget), percentBp,
burnBasis (rate, hours, cost), warnAt, overrunMode.
Each line in a response carries the budget’s currency, its soldAmount,
usedAmount, usedHours, costAmount, committedAmount, invoicedAmount,
pendingAmount, pendingHours and unvaluedEntries, subject to the lens.
Example: add a time and materials line.
curl -X POST "https://app.runnit.io/api/v1/finance/budgets/<budgetId>/lines" \ -H "Authorization: Bearer <api-key>" \ -H "Content-Type: application/json" \ -d '{ "sectionId": "<sectionId>", "serviceTypeId": "<activityCodeId>", "name": "Content strategy", "billingType": "time_and_materials", "unit": "hour", "quantity": "40", "unitPrice": "18500", "estimatedHours": "40" }'Change orders
Section titled “Change orders”| Method | Path | Notes |
|---|---|---|
GET, POST | /api/v1/finance/budgets/:id/versions | POST opens a change order: reason, clientReference, lines: [{ id?, remove?, ...line fields }] |
PATCH | /api/v1/finance/versions/:versionId | Edits a draft or approved version. Any change to approval-relevant content invalidates a live approval |
POST | /api/v1/finance/versions/:versionId/submit | Returns outcome: "applied" or outcome: "pending_approval" with approvalRequestId. The reason comes from the version, not this body: save it with PATCH /api/v1/finance/versions/:versionId first |
POST | /api/v1/finance/versions/:versionId/apply, .../withdraw | Apply is safe to repeat: an already applied version returns unchanged. Approval starts application automatically; use apply to retry after resolving a blocker; 409 APPROVAL_STALE means the approval no longer matches the change order, so refresh and submit it again |
After a budget.change_order approval closes, asynchronous processing sets the
current proposal to rejected for rejection, requested changes or expiry, or
withdrawn for cancellation. Its approvalRequestId remains available for
reading the exact decision and comments. The live budget and current version
stay unchanged; budget detail returns openVersion: null once processed.
Read the approval and budget again before opening a replacement. A delayed
decision cannot close a revised or resubmitted proposal. These outcomes also
apply to decisions sent through MCP call_public_api. See
approval decisions.
While a change order is open, send all line edits, including estimates,
descriptions and position, to PATCH /api/v1/finance/versions/:versionId in
lines. Direct line PATCH and line-order PUT return 409 CHANGE_ORDER_REQUIRED.
Section deletion also requires withdrawing the open proposal first. Estimates
accept null or non-negative values: cost has up to 18 integer minor-unit digits;
hours have up to 8 whole digits and 2 decimal places. Budget alternative and
template saves enforce the same limits.
Read the saved version back before confirming the proposed values. They become
live only when application succeeds. Estimate-only edits do not invalidate a
commercial approval.
Application can return 409 CHANGE_ORDER_INPUTS_CHANGED without changing either
the live budget or proposal. Review both sets of values, withdraw the version,
then open a new proposal from the current budget. Older proposals with differing
operational inputs can require this review even when the difference was
intentional. The same rules apply through MCP call_public_api;
propose_change_order still only opens a draft.
Billing schedule
Section titled “Billing schedule”| Method | Path | Notes |
|---|---|---|
GET, POST, PATCH, DELETE | /api/v1/finance/budgets/:id/schedule[/:itemId] | Items: kind (deposit, milestone, interval, final, retention), label, amount or percentBp, trigger (manual, on_date, on_milestone, on_delivery, on_approval), dueOn, projectMilestoneId. PATCH takes the same fields and clears needsReview when amount or percentBp is supplied. Every response carries the balance: scheduleBase, allocated, unallocated, balanced |
POST | /api/v1/finance/budgets/:id/schedule/:itemId/skip | Marks an item as one that will not be invoiced. Only a scheduled or ready item can be skipped (409 otherwise); repeating it is safe. A skipped item stays in the schedule and still counts in the balance |
Schedule trigger semantics: on_approval becomes ready on successful budget
activation; on_delivery on delivery. Saving lifecycle items on an already
active/delivered budget and reopening a closed budget reconcile missed eligible
items. Responses return the saved readiness; existing missed items also recover
nightly. Only scheduled items are promoted. Amounts, review flags and invoice
links remain unchanged. Changing a ready item’s trigger does not reset readiness.
Quote acceptance alone leaves a converted draft’s items scheduled. These effects
share the lifecycle/save transaction and the generated-token REST and MCP
call_public_api paths, with the same live permissions and organisation binding.
No invoice is created until a caller explicitly drafts one.
Required budget fields
Section titled “Required budget fields”Activation returns 409 BUDGET_REQUIRED_FIELDS when a live required budget field
is absent, blank or an empty selection. The error names the fields; the budget
remains a draft with no new version or activation event. Zero number and money
values are valid. Archived or optional fields do not block activation.
A PATCH supplying a blank required value returns 400. Header edits and field
values in the same PATCH save together or both remain unchanged. Activation or
field saves can return 409 FINANCE_FIELDS_BUSY while another Finance field
action is in progress. Retry after it finishes. These rules apply equally through
MCP call_public_api.
Work, lineage and activity
Section titled “Work, lineage and activity”| Method | Path | Notes |
|---|---|---|
GET | /api/v1/finance/budgets/:id/work | Approved and pending minutes per line |
GET | /api/v1/finance/budgets/:id/lineage?figure=used&lineId= | The rows behind used, cost, sold or forecast. cost needs the cost lens |
GET | /api/v1/finance/budgets/:id/activity | Finance events for the budget, each with eventType, before, after and occurredAt |
For figure=used, time rows include their recorded usedAmount in budget
currency and current status, including invoiced. Display that amount directly;
a billable-rate field is not required. A null amount means no valuation is
available. Without price access, the amount is omitted. The same projection is
available through MCP call_public_api.
Task mapping
Section titled “Task mapping”| Method | Path | Notes |
|---|---|---|
PUT | /api/v1/finance/tasks/:taskId/budget-line | budgetLineId or null to unmap. The line must fund the task’s project |
PUT | /api/v1/finance/tasks/budget-line | taskIds[] (one project) and budgetLineId |
Templates and catalogue
Section titled “Templates and catalogue”| Method | Path | Notes |
|---|---|---|
GET, POST, PATCH | /api/v1/finance/budget-templates[/:id] and POST .../:id/archive | Sections, lines with role keys, schedule pattern and thresholds |
Saved template responses include read-only currency, billingEntityId and
pricingSettings. Writes reject those fields and lines[].pricingTaxIdentity.
Line money fields are minor-unit strings: unitPrice, discountAmount,
capAmount and estimatedCost. Quantities support four decimal places.
For row edits, retain lines[].id so omitted fields keep their values. Use
sections[].id and lines[].sectionId when section names repeat. Omitted
collections stay unchanged; an empty array removes that collection. A section
name without an ID must match exactly one section. Creation with templateId
commits the complete draft or nothing. A missing active rate card role or a
conflicting saved currency or billing entity returns 409. These contracts also
apply through MCP’s call_public_api with the same permissions and organisation.
| POST | /api/v1/finance/budgets/:id/save-as-template | name |
| GET, POST, PATCH | /api/v1/finance/catalogue[/:id] and POST .../:id/archive | Reusable priced lines |
Time routes are participation routes: they need the capability shown but not
the Finance plan, so everyone can log and submit time. Acting on someone
else’s time needs time:approve. Writes dated behind the timesheet lock need
time:manage_locked and a lockOverrideReason; writes dated in a soft-closed
period need a periodReason. Members see hours and their own billable minutes,
never rates.
| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/time/week?weekStart=&userId= | time:log | The week: entries, the batch and its segments, minutes per day, lock flags, expectedMinutes, lockedBefore, closedPeriods, periodLocks (one row per billing entity and month covering the week, including months with nothing logged), lineEntities, defaultBillingEntityId and lens. Each entry is serialised through its own project’s lens |
GET | /api/v1/time/entries | time:log for your own, time:approve for others | Filters userId, projectId, budgetId, taskId, status (comma list), from, to, limit, offset |
POST | /api/v1/time/entries | time:log | taskId, or projectId with budgetLineId; date or startTime and endTime; minutes, note, billable, userId (others need time:approve). Returns 201 |
PATCH | /api/v1/time/entries/:id | Owner or time:approve | Same fields. Editing a submitted entry withdraws its approval segment |
DELETE | /api/v1/time/entries/:id | Owner or time:approve | Draft or rejected entries only |
POST | /api/v1/time/timer/start, /api/v1/time/timer/:id/stop | time:log | One running timer per person in the organisation, including concurrent starts. Stopping writes minutes onto a dated entry, so it accepts periodReason |
POST | /api/v1/time/weeks/submit | time:log | weekStart (a Monday), idempotencyKey, note, entryIds. Returns the batch and segments, each with outcome approved or pending_approval and an approvalRequestId |
GET | /api/v1/time/approvals?status=&scope=mine&userId=&budgetId=&projectId= | time:approve organisation wide, in any project or client scope, or an assignment on a time approval | Segments waiting on you by default; scope=all lists every segment and is for organisation-wide holders, everyone else is served their own assignments; budgetId and projectId narrow the list |
GET | /api/v1/time/approvals/:segmentId | Submitter, time:approve or request access | The segment, its entries, the approval request, its approvers and canDecide |
POST | /api/v1/time/approvals/:segmentId/decide | time:approve in a covering scope | decision, comment, entryOutcomes (array of { entryId, outcome, reason }), edits (array of { entryId, minutes?, budgetLineId?, note?, billable? }). Omit both arrays to endorse the outcome an earlier approver proposed; sending either replaces it, and everyone who already approved on the stage is asked again |
POST | /api/v1/time/approvals/bulk | time:approve | segmentIds, decision, comment. One transaction per segment with per-segment results |
POST | /api/v1/time/entries/unapprove | time:approve | entryIds, reason. Clears billable minutes and the approved rate and returns the entries to draft for correction; holds survive. Time behind the timesheet lock needs time:manage_locked |
Completing a task (PUT /api/v1/tasks/:taskId/status) also accepts
lockOverrideReason and returns a timeEntry result beside the task, because
task completion records time.
Uninvoiced work and periods
Section titled “Uninvoiced work and periods”These need Finance to be turned on. Queue reads need any of
finance:draft_invoices, finance:manage_budgets, finance:view_invoices,
finance:manage_recognition or finance:issue_invoices. Amounts follow the
caller’s lens.
| Method | Path | Notes |
|---|---|---|
GET | /api/v1/finance/uninvoiced | Rows client, budget and line with approved uninvoiced hours and amount, ready schedule items, the oldest unbilled date and its age bucket, and a totals strip in the reporting currency covering only the rows you may see. Filters clientOrganizationId, ownerUserId, billingType, ageBucket, includeHeld, minAmount |
GET | /api/v1/finance/uninvoiced/budgets/:budgetId | The review grid: every approved entry with its recognisable allocation and state (ready, held, written_off), the line rows and the budget’s write-offs |
POST | /api/v1/finance/uninvoiced/hold, .../release, .../transfer, .../non-billable | entryIds. Hold and release take an optional reason, transfer takes budgetLineId, reprice and reason, non-billable needs reason. All are dated, so the reason doubles as the soft-close reason. Needs finance:draft_invoices, finance:manage_budgets or finance:manage_recognition |
POST | /api/v1/finance/uninvoiced/write-off | entryIds, reason. Anyone with queue write access proposes; finance:manage_recognition posts. Returns 201 with adjustments, each with outcome proposed, posted or pending_approval |
GET | /api/v1/finance/wip-adjustments?budgetId=&status= | Write-offs and their state |
POST | /api/v1/finance/wip-adjustments/:id/post, .../withdraw, .../reverse | Post needs finance:manage_recognition and may answer pending_approval when a policy applies; withdraw and reverse take a reason |
POST | /api/v1/finance/periods/ensure | billingEntityId, year, month. Creates the period row for a month with no dated activity so it can be closed. Needs finance:close_periods. Returns 201 |
GET | /api/v1/finance/periods/:id | The period and its month-end checklist. Needs finance:close_periods, finance:manage_settings, finance:manage_recognition, finance:view_prices or finance:issue_invoices |
POST | /api/v1/finance/periods/:id/soft-close, .../close, .../reopen | Needs finance:close_periods. reason (required to reopen; reopening may answer pending_approval), note on close |
Invoices and credit notes
Section titled “Invoices and credit notes”Use kind to distinguish an invoice from a credit note. A credit note’s positive
totalAmount and amountDue describe the credit document; amountDue is not a
collectible balance. Issuing the credit adjusts its parent and can create a
separate client-credit balance when the parent has already been paid.
Invoice-targeted refunds, client-credit applications, write-offs, disputes,
reminder changes and promised-payment dates reject credit notes with
409 INVOICE_NOT_RECEIVABLE. Explicit manual payment allocations retain
409 INVOICE_NOT_OPEN for credit-note targets. Refused writes do not change
financial state. Ordinary client-credit application and refund remain supported.
Credit-note list rows return daysOverdue: 0 without changing stored amounts.
Payment status returns eligible: false and reason: "nothing_due" for credit
notes, including through get_payment_status. These rules also apply through
call_public_api, with the same permissions and confirmation requirements.
For an ordinary credit, lines[].amount is a nonnegative minor-unit integer
string in the original invoice’s price-entry mode. It includes tax for inclusive
invoices and excludes tax for exclusive invoices. For example, selecting 5000
from an inclusive line with original total 11000, net 10000 and tax 1000
credits 5000 in total: 4545 before tax and 455 in tax. An amount selection uses quantity one and clears the original discount.
Negative adjustments use a signed quantity instead.
Other non-voided credit drafts and issued credits count towards the remaining
line amount. Inclusive credits must stay within both the original before-tax
and tax-inclusive amounts. An excessive edited draft can be saved for correction,
but cannot be issued. Creation and issue refusals return 409 without posting
changes. Inclusive selections use the original line’s remaining net and each
tax component, after other credit drafts and issued credits. An exact full-line
selection retains its recorded tax. Partial selections divide those remaining
amounts while keeping the requested gross exact; the net/tax split can differ
from a fresh tax calculation. Unchanged drafts keep their allocation when a
sibling is discarded. Existing issued values stay unchanged.
For a retention-release credit, each selected line keeps the original release’s
recorded tax amounts. lines[].amount selects net minor units; a quantity selects
that share of the release. Other non-voided credit drafts reserve both net and tax.
Changing the amount recalculates its share of the remaining recorded components.
An unchanged allocation stays the same if another draft is discarded.
Issuing restores only the credited net and tax to retention. A later release can
use those restored amounts. Written-off retention stays written off. Changing a
retention credit’s source tax is refused with 409 CREDIT_SOURCE_MISMATCH;
exceeding the remaining net or tax returns 409 CREDIT_EXCEEDS_LINE without saving
the change. The same rules apply through MCP call_public_api.
Invoice detail requires scoped finance:view_invoices. A matching client grant
or organisation-wide grant covers the document. Project grants must cover every
project on a consolidated invoice; a document without project scope needs client
or organisation-wide access. Unreadable detail returns 404.
Draft header and line edits, line ordering, source additions, credit creation,
void, dispute and promised-date writes need this view permission alongside their
action permission. Missing view access returns 404 without saving the change.
Adding sources, removing lines and creating a credit also require access to the
resulting document; an unreadable result is refused without saving. The same
rules apply through MCP call_public_api. These operations do not grant price
access.
Figures appear only with finance:view_prices; a preview or a PDF is refused
outright without it. Cost never appears on an invoice.
| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/invoices | any invoice capability | view (drafts, awaiting_approval, unpaid, overdue, paid_this_month, all), kind, status, clientOrganizationId, budgetId, projectId, q, limit, offset. Returns the invoices with their days overdue and timeline, a count, totals by currency, and the lens. A scoped reader gets no totals |
POST | /api/v1/finance/invoices | draft or issue | Composes a draft. method is one of uninvoiced, schedule_item, percentage, custom_amount, remaining, consolidated, standalone or retention_release, with the sources for that method (budgetIds, entryIds, scheduleItemIds, holdingIds), plus header fields such as issueDate, dueDate, subject, clientPoReference, documentTemplateId, applyDeposit and retentionBp. Returns 201 with the full detail |
GET | /api/v1/finance/invoices/composer/candidates?budgetId= | drafting | What can go on an invoice: every approved entry with its availability and state, ready schedule items, deposits and retention holdings |
GET | /api/v1/finance/invoices/:id | finance:view_invoices | The invoice, lines, sources, reservations, events, validation, whether it can be issued, warnings about a stale draft or an expiring reservation, the entity and client profile, documents, credit notes and remaining balances |
PATCH, DELETE | /api/v1/finance/invoices/:id | drafting | Edit or discard a draft. Discarding releases its reservations and returns schedule items to ready |
POST, PATCH, DELETE | /api/v1/finance/invoices/:id/lines[/:lineId] | drafting | Manual lines. Deposit and retention lines are calculated and cannot be edited |
PUT | /api/v1/finance/invoices/:id/lines/order | drafting | order, listing every line once |
POST | /api/v1/finance/invoices/:id/sources | drafting | Add approved work or ready schedule items to an existing draft |
GET | /api/v1/finance/invoices/:id/validation | any | The blocking and non-blocking issues, and whether the invoice can be issued |
GET | /api/v1/finance/invoices/:id/preview | any, with prices | Complete styled HTML; current draft or saved issued document |
POST | /api/v1/finance/invoices/:id/request-approval | drafting | Ask for approval without issuing |
POST | /api/v1/finance/invoices/:id/issue | finance:issue_invoices | idempotencyKey, and a reason when the period is soft closed. Returns the invoice and an outcome of issued, pending_approval or already_issued |
POST | /api/v1/finance/invoices/:id/send | finance:issue_invoices | mode (email, mark_sent, schedule), recipients, cc, replyTo, scheduledFor, message. Returns a job id |
GET | /api/v1/finance/invoices/:id/pdf | any, with prices | The frozen bytes of an issued document |
GET | /api/v1/finance/invoices/:id/documents | any | Every version with its status, checksum, size, attempts and template version, plus the send jobs |
POST | /api/v1/finance/invoices/:id/documents/:documentId/retry | finance:issue_invoices | Re-queue a failed render |
GET, POST | /api/v1/finance/invoices/:id/link, .../link/rotate, .../link/revoke | finance:issue_invoices | Mint, rotate or revoke the client’s hosted link |
POST | /api/v1/finance/invoices/:id/credit-notes | finance:issue_invoices | reason, lines[{ lineId, quantity?, amount? }] (all lines when omitted), and sourceMode reissue (the work returns to the queue) or write_off |
POST | /api/v1/finance/invoices/:id/void, .../write-off, .../dispute, .../reminders, .../promise | issue, record payments or manage receivables | Void and write off take a reason; dispute pauses reminders; promise records a promised date |
POST | /api/v1/finance/invoices/bulk | finance:issue_invoices | action (issue, send, apply_sequence) over ids. One transaction per invoice, with a per-invoice result |
Approved time composition and adding sources preserve the approved net amounts.
A single-rate group uses hours only when its four-decimal quantity and rate produce
that exact net amount. Otherwise it uses unit: "item", quantity: "1.0000" and
the approved net unitPrice, with minutes in its description. Sources retain
their minutes and rates, and tax groups remain separate. REST, call_public_api
and draft_invoice share this behaviour. Explicit draft price edits still apply.
Discard and recompose an older incorrect draft before issuing; existing issued
invoices remain unchanged.
Credit-note creation includes signed adjustments when you omit lines. Explicit
quantity values must retain the original quantity’s sign and fit its magnitude.
Duplicate lineId values, wrong signs and quantities rounded to zero are refused.
amount stays a nonnegative minor-unit string; use signed quantity for a
negative-net line. A credit cannot have a negative total or exceed the remaining
invoice or signed line amounts. Other non-voided credit drafts count towards the
line limit; discard unused drafts before retrying.
Parented credit lines must retain their source binding and tax treatment, and the
credit must retain the original price-entry and rounding settings. Draft edits
that break these rules block issue. The response validation identifies
CREDIT_EXCEEDS_LINE, CREDIT_EXCEEDS_INVOICE, NEGATIVE_TOTAL or
CREDIT_SOURCE_MISMATCH. REST and MCP share these checks and settlement effects.
Credits and voids update both positive and negative on-invoice revenue amounts on the affected budget lines. Successive partial credits apply only the remaining credited share.
Payments, credits and settlement
Section titled “Payments, credits and settlement”| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/payments | any invoice capability | Filters by client, invoice, kind and date range |
POST | /api/v1/finance/payments | finance:record_payments | receivedOn, amount, currency, clientOrganizationId, billingEntityId, method, reference, and allocations (oldest open invoice first when omitted). Any unallocated remainder becomes a client credit. Returns 201 |
GET | /api/v1/finance/invoices/:id/payments | any | Allocations, payments, per-line allocations and credit applications |
POST | /api/v1/finance/payments/refunds | finance:record_payments | Refund against an invoice or a client credit |
GET, POST | /api/v1/finance/client-credits, .../:id/apply | any, record payments | List open credits and apply one to an invoice |
A manual invoice refund can return several allocations for one payment. Each
reversesId identifies its original cash receipt allocation. reversesPaymentId
on the refund is populated when there is one source payment. Credit applications
and write-offs cannot fund a cash refund. Refunds use remaining receipts in
reverse date order. The invoice lock protects concurrent refunds. REST and MCP
share these results, permissions and 409 failures.
Allocations are made against each line’s remaining balance and each tax component, never the original totals, so repeated part payments settle exactly. Every valuation is stored with the rate that applied on its own economic date, so a later rate change never restates a settled figure.
Receivables, statements and reminders
Section titled “Receivables, statements and reminders”| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/receivables | any invoice capability | asOf, basis (due_date or issue_date), and optional client and entity filters. Returns the ageing per client with buckets, holds, disputes, promised dates and available credit, rollups by entity, the invoices with their bucket, and the average days to pay |
POST | /api/v1/finance/receivables/clients/:clientId/hold | finance:manage_receivables | Put a client on hold or release them, with a reason |
GET, POST | /api/v1/finance/statements | read any, write manage receivables | Generate a statement for a client and a date range |
GET | /api/v1/finance/statements/:id, .../pdf | any, PDF needs prices | The statement and its document |
POST | /api/v1/finance/statements/:id/send | finance:manage_receivables | recipients, cc, message |
GET, POST, PATCH | /api/v1/finance/reminder-sequences[/:id] | read any, write manage receivables | A named sequence of steps with offsets in days, a channel and an escalation target, and the conditions that stop it |
Billing profiles, templates and statutory rules
Section titled “Billing profiles, templates and statutory rules”| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/billing-profiles, /api/v1/finance/clients/:clientId/billing-profile | any invoice capability | The client’s billing contact, terms, credit limit, purchase order requirement, tax treatment and document defaults. Internal notes need finance:manage_settings |
PATCH | /api/v1/finance/clients/:clientId/billing-profile | finance:manage_settings | The same fields, plus onHold and the reminder sequence |
GET, POST, PATCH | /api/v1/finance/document-templates[/:id] and .../archive | read any, write settings or integrations | Templates are an ordered list of blocks with options. System templates are read-only; copy one to change it |
GET | /api/v1/finance/document-templates/:id/preview | any, prices when an invoice is named | HTML, with sample data when no invoice is given |
GET, POST, PATCH | /api/v1/finance/document-styles[/:id] | read any, write settings or integrations | Font, accent colour, logo size, paper size and density |
POST | /api/v1/finance/statutory-rules/confirm | finance:manage_settings | Confirms the statutory rules that ship unverified for a jurisdiction, which is what unblocks issuing for an Australian entity |
Expenses, suppliers and purchasing
Section titled “Expenses, suppliers and purchasing”The purchase side is what the organisation spends: claims from people, suppliers, purchase orders, supplier bills, and the runs that pay people back. See the Expenses, Purchasing and Paying suppliers guides for the product behaviour.
Reading and submitting your own claims is a participation lane, like logging
time: it needs Finance turned on and any of expense:submit,
expense:view_all, expense:approve or expense:manage, but not a Finance
seat. Everything else in this section needs the Finance plan. Your own claim
always carries its amounts and its receipt whatever your lens says, and
somebody else’s claim answers 404 without expense:view_all,
expense:approve or expense:manage.
Purchase reads take any of procurement:manage_pos,
procurement:approve_pos, procurement:manage_bills,
procurement:manage_suppliers or expense:manage. The tables below call that
any purchase capability.
Expenses
Section titled “Expenses”An expense held by an invoice draft or still included in an issued invoice cannot
be unapproved or moved to another budget line. These operations return 409
EXPENSE_INVOICED and keep approval, allocation and budget values unchanged.
Release the draft reservation or fully credit the expense for reissue first.
A partial credit or a write-off credit does not unlock corrections. List and
detail billing statuses update when the related invoice is issued. The same
rules apply through generated-token REST and MCP. If a historical claim is
non-approved but still invoiced, editing, submitting or deleting it also returns
409 EXPENSE_INVOICED; resolve the invoice first.
receiptAssetId must identify a live, readable uploaded file in the same organisation. Missing, foreign, inaccessible, remote and protected Finance sources return 404. Upload an existing cloud file again as a receipt; a generic cloud file cannot be attached while its original public address could still work. Receipt file access follows current expense permissions across file reads, search and downloads. See private expense receipts.
| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/expenses | any expense capability | view (mine, awaiting_my_approval, unallocated, unreimbursed, billable_uninvoiced, this_month, all), projectId, budgetId, supplierId, categoryId, submitterUserId, from, to, q, limit, offset. Without expense:view_all, expense:approve or expense:manage the list is your own. totals appears only with the cost lens |
GET | /api/v1/finance/expenses/:id | any expense capability | The claim, its lines, its policy checks and its extraction runs |
POST | /api/v1/finance/expenses | any expense capability | expenseCategoryId and expenseDate are required, then amount, currency, entryMode, merchantName, supplierId, projectId, taskId, budgetLineId or splits, taxRateId, billable, markupKind, markupBp, markupAmount, paidWith, cardReference, description, note, mileageDistance, mileageVehicleType, perDiemDays, perDiemSalaryBand, perDiemLocation, receiptAssetId, receiptHash. Returns 201 |
PATCH | /api/v1/finance/expenses/:id | own claim, else expense:manage or expense:approve | Any create field, plus reason when the date falls in a soft-closed period. 409 EXPENSE_APPROVED once it is approved |
DELETE | /api/v1/finance/expenses/:id | own claim, else expense:manage | Drafts only. Returns 204 |
POST | /api/v1/finance/expenses/:id/submit | own claim, else expense:manage | reason. Returns an outcome of approved, pending_approval or already_submitted. 409 PURCHASE_INVALID carries the blocking policy findings |
POST | /api/v1/finance/expenses/:id/decision | expense:approve or expense:manage | decision (approve, reject), reason. 409 EXPENSE_STATE or APPROVAL_PENDING, and 403 when segregation of duties refuses the submitter |
POST | /api/v1/finance/expenses/bulk-decision | expense:approve or expense:manage | expenseIds, decision, reason. One record at a time, with a result for each |
POST | /api/v1/finance/expenses/:id/unapprove | expense:approve or expense:manage | reason is required. 409 EXPENSE_STATE, EXPENSE_INVOICED or EXPENSE_IN_RUN |
POST | /api/v1/finance/expenses/:id/reallocate | expense:approve or expense:manage | lineId, budgetLineId, reason. 409 EXPENSE_INVOICED |
POST | /api/v1/finance/expenses/:id/settle-externally | expense:manage | reference. 409 EXPENSE_STATE, EXPENSE_NOT_PERSONAL or EXPENSE_IN_RUN |
POST | /api/v1/finance/expenses/:id/extraction | own claim, else expense:manage | assetId, assetVersion. 202 when a run started, 200 when one was already claimed for the same receipt |
GET | /api/v1/finance/expenses/:id/extractions | any expense capability | The runs with their status, proposals, confidence and corrections |
POST | /api/v1/finance/extractions/:id/apply | any expense capability | fields. Applies only the fields nobody has corrected, and returns applied and skipped |
GET | /api/v1/finance/expenses/:id/policy-checks | any expense capability | The notes, each with a code, a severity and a state |
Expense categories are in the settings table above. They also accept
agencyTreatment, defaultMarkupKind, defaultMarkupAmount,
mileageVehicleType and mileageUnit. A disbursement is an agency payment,
so agencyTreatment defaults to the disbursement flag when you leave it out,
and sending disbursement: true with agencyTreatment: false is a 400.
Suppliers and bank details
Section titled “Suppliers and bank details”| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/suppliers[/:id] | any purchase capability or finance:manage_settings | List filters: q, includeArchived, contractorsOnly, limit. The detail adds contacts and the freelancer link. List spendYearByCurrency needs complete cost access |
POST, PATCH | /api/v1/finance/suppliers[/:id] | procurement:manage_suppliers or finance:manage_settings | name is required, then legalName, code, taxIdentifier, taxIdentifierKind, gstRegistered, statementBySupplier, defaultCurrency, paymentTerms, defaultExpenseCategoryId, defaultTaxRateId, isInternal, internalClientOrganizationId, internalBudgetId, clientOrganizationId, address, notes. 400 when an ABN fails its checksum |
POST | /api/v1/finance/suppliers/:id/archive | procurement:manage_suppliers or finance:manage_settings | archived (true by default). 409 while open purchase orders or bills remain |
POST, DELETE | /api/v1/finance/suppliers/:id/contacts[/:contactId] | procurement:manage_suppliers or finance:manage_settings | name is required, then email, phone, role, isPrimary. 201 on create, 204 on delete |
POST | /api/v1/finance/suppliers/backfill-freelancers | finance:manage_settings | Links every freelancer membership that has no live supplier. Safe to repeat; returns scanned, created, linked and skipped |
GET | /api/v1/finance/bank-accounts/:ownerKind/:ownerId | procurement:manage_bank_details | ownerKind is billing_entity, supplier or employee (a membership id). Masked identifiers only |
POST | /api/v1/finance/bank-accounts/:ownerKind/:ownerId/changes | procurement:manage_bank_details | label, country, accountHolder, currency, identifiers, isDefault, bankAccountId. Returns 201, and a different person with bank details permission has to confirm it within 24 hours through the confirm and reject routes in the settings table. 409 BANK_CHANGE_PENDING when one is already waiting |
For an employee reimbursement account, use the claim’s payeeMembershipId as
ownerId and set isDefault:true. A user ID is not a membership ID. Confirmation
makes the account available for future reimbursement selection. Pending changes
block eligibility; existing run items retain their frozen account version.
Purchase orders
Section titled “Purchase orders”Without the cost lens a purchase order or a supplier bill comes back with its
quantities, unit prices, markups and totals removed rather than set to null.
A reader who has prices but not cost still sees a line’s sell total, and
nothing that would reconstruct the cost behind it.
With cost access, order and bill lists, details and mutation responses retain
supplier unit prices, tax, totals, receipt and payment values, and retained cost
versions even without finance:view_prices. The matching preview follows the
same rule after checking access to the bill and its referenced orders. Selling
totals still require prices; markups require both cost and prices. These rules
also apply to raise_po, draft_supplier_bill, list_supplier_bills and
call_public_api. Existing money shapes are unchanged.
| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/purchase-orders | any purchase capability | view (open, awaiting_approval, awaiting_bills, over_received, closed, all), supplierId, projectId, budgetId, q, limit |
GET | /api/v1/finance/purchase-orders/:id | any purchase capability | The order, its lines, versions, receipts, reversals and policy checks. pendingRevision is null or { baseVersion, approvalRequestId }. totals is absent without the cost lens |
POST | /api/v1/finance/purchase-orders | procurement:manage_pos | supplierId is required, then projectId, currency, orderDate, expectedDelivery, termsText, note, quoteReference, quoteAmount, quoteValidUntil, quoteAssetId, documentTemplateId and lines (description, activityCode, budgetLineId, quantity, unit, unitPrice, taxRateId, billable, markupKind, markupBp, markupAmount, sellAmount). Returns 201 |
PATCH, DELETE | /api/v1/finance/purchase-orders/:id | procurement:manage_pos | Drafts only, before the order is issued. 409 PO_ISSUED, and 204 on delete |
POST | /api/v1/finance/purchase-orders/:id/issue | procurement:manage_pos | idempotencyKey (or the Idempotency-Key header) and reason. Returns an outcome of issued, pending_approval or already_issued. 409 PO_ALREADY_ISSUED, PO_STATE, APPROVAL_STALE or PURCHASE_INVALID |
POST | /api/v1/finance/purchase-orders/:id/revise | procurement:manage_pos | baseVersion, lines (give a line’s id to keep it), changeNote, expectedDelivery, termsText, note. 409 PO_VERSION_STALE, PO_REVISION_BELOW_CONSUMED, APPROVAL_STALE or PO_STATE |
POST | /api/v1/finance/purchase-orders/:id/cancel, .../close | procurement:manage_pos | Cancel needs a reason and takes lineIds to cancel part of the order. Close releases the remaining commitment |
POST | /api/v1/finance/purchase-orders/:id/receipts, .../receipts/:receiptId/reverse | procurement:manage_pos | Receive lineId, quantity, receivedOn, note (201); reverse quantity, reversedOn, reason. 409 RECEIPT_REVERSAL_OVER |
A revision cannot reduce or remove a line below the greater of its net received
quantity and its quantity on-charged on issued invoices. The refusal returns
409 with code PO_REVISION_BELOW_CONSUMED and the message A line cannot go below what has already been received or on-charged. It leaves the order
unchanged and does not include the consumed quantity or line description in
the error. This applies through both the API and call_public_api.
Supplier bills and payments
Section titled “Supplier bills and payments”| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/supplier-bills | any purchase capability | view (to_approve, unpaid, due_this_week, overdue, disputed, all), supplierId, projectId, budgetId, q |
GET | /api/v1/finance/supplier-bills/ageing | any purchase capability, with cost | Buckets current, 1_30, 31_60, 61_90 and 90_plus, with internal suppliers left out. 403 without the cost lens |
GET | /api/v1/finance/supplier-bills/:id | any purchase capability | The bill, its lines, its checks and its payments. The match snapshot and the payments need the cost lens |
POST | /api/v1/finance/supplier-bills | procurement:manage_bills | supplierId is required, then supplierInvoiceNumber, projectId, currency, billDate, dueDate, entryMode, note, attachmentAssetId, attachmentHash and lines (description, purchaseOrderLineId, expenseCategoryId, budgetLineId, quantity, amount, taxRateId, billable, markupKind, markupBp, markupAmount). Returns 201 |
PATCH, DELETE | /api/v1/finance/supplier-bills/:id | procurement:manage_bills | Approved, paid and void bills refuse edits. Delete requires a draft and returns 204. Retained manual invoice attachments use the separate upload action below. |
GET | /api/v1/finance/supplier-bills/:id/match | any purchase capability, with cost | The three-way match line by line. 409 MATCH_BOUNDARY when the bill reaches across suppliers, currencies or billing entities |
POST | /api/v1/finance/supplier-bills/:id/submit | procurement:manage_bills | reason. Returns an outcome of approved, pending_approval or already_approved |
POST | /api/v1/finance/supplier-bills/:id/dispute, .../void | procurement:manage_bills | Dispute takes disputed and note; void needs a reason and takes voidedOn. 409 BILL_SETTLED |
GET | /api/v1/finance/supplier-payments/payable | procurement:manage_bills or expense:manage, with cost | billingEntityId, currency, dueBy. Each row reports bankPending and hasBankAccount. 403 without the cost lens |
POST | /api/v1/finance/supplier-payments | procurement:manage_bills or expense:manage | amount is required, then paidOn, method, reference, note and either billIds or allocations. Returns 201. 409 SUPPLIER_INTERNAL, BILL_STATE or BILL_DISPUTED |
POST | /api/v1/finance/supplier-payments/:id/reverse | procurement:manage_bills or expense:manage | reason is required, and reversedOn is optional |
Reimbursement runs
Section titled “Reimbursement runs”| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/reimbursement-runs/eligible | expense:manage | billingEntityId, currency. Someone who cannot be paid is listed with a reason rather than hidden |
GET | /api/v1/finance/reimbursement-runs[/:id] | expense:manage | status on the list. The detail carries the run, its items and the payment-file history (identities only) |
POST | /api/v1/finance/reimbursement-runs | expense:manage | expenseIds is required, then billingEntityId, currency, paymentDate, note. 409 EXPENSE_NOT_ELIGIBLE or EXPENSE_IN_RUN |
DELETE | /api/v1/finance/reimbursement-runs/:id/items/:itemId | expense:manage | Takes an item out while the run is still being selected. 409 RUN_STATE |
POST | /api/v1/finance/reimbursement-runs/:id/cancel | expense:manage | reason is required. 409 RUN_STATE |
POST | /api/v1/finance/reimbursement-runs/:id/export | expense:manage and procurement:manage_bank_details | format (aba by default, or csv). Returns the file identity, a downloadUrl and every skipped item with its reason. 409 RUN_STATE, RUN_EMPTY, ABA_CURRENCY, ABA_ORIGINATOR_MISSING and the other ABA_ codes |
GET | /api/v1/finance/payment-files/:token | procurement:manage_bank_details | The file bytes, once. A second attempt is 410 DOWNLOAD_SPENT, and a run that changed since the export answers 409 PAYMENT_FILE_STALE, so export it again |
POST | /api/v1/finance/reimbursement-runs/:id/confirm | expense:manage | reference, paidOn. Marks the expenses reimbursed. 409 RUN_STATE or RUN_EMPTY |
POST | /api/v1/finance/reimbursement-runs/:id/reverse-confirmation | expense:manage | Required reason; optional reversedOn and expectedSettlementId. Returns the exported run and dated signed settlement history. Rejects a linked bank match, a stale confirmation, missing original evidence or a date before payment |
Reimbursement CSV downloads contain masked accounts and deterministic row order.
The stored hash must match the rebuilt contents. Changed contents return
409 PAYMENT_FILE_STALE without consuming the link; used or revoked links return
410 DOWNLOAD_SPENT. Export again for a fresh link when needed. Downloading does
not confirm payment. Generated REST and MCP call_public_api use the same checks;
the MCP bridge returns CSV as text.
Accruals, and purchases as invoice sources
Section titled “Accruals, and purchases as invoice sources”| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/accruals/periods/:periodId | any purchase capability, with cost | currency. Returns the stored balances beside the computed ledger, so the two can be compared |
GET | /api/v1/finance/accruals/pass-through | any purchase capability, with cost | projectId, supplierId. What was on-charged against what the supplier billed, per purchase order line |
GET | /api/v1/finance/budgets/:budgetId/purchase-candidates | finance:draft_invoices, finance:view_invoices or any purchase capability, with prices | excludeDraftId. Refused with 403 without the prices lens, and costAmount needs the cost lens |
POST | /api/v1/finance/invoices/:id/purchase-sources | finance:draft_invoices or finance:issue_invoices | budgetId is required, then expenseIds, purchaseOrderLineIds, supplierBillLineIds. 409 SOURCE_RESERVED or SOURCE_UNAVAILABLE |
Quotes and retainers
Section titled “Quotes and retainers”Quotes are the sold side before a budget exists, and retainers are the repeating agreements that fund work period by period. See the Quotes and Retainers guides for the product behaviour.
Reads take any of finance:manage_quotes, finance:send_quotes or
finance:approve_quotes, which the tables call any quote capability. A
quote outside your scope answers 404, never 403. Money appears only with
the prices lens, and the pipeline is refused outright with 403 without it,
because it is nothing but money.
Quotes
Section titled “Quotes”The composition notes field is staff-only. It is retained for authorised quote
readers and omitted from all client document HTML and PDFs. An older quote PDF
that included notes is redacted when downloaded or attached to an email. Its
number, version, commercial content and signer evidence stay unchanged. Rendering
failure prevents delivery of that copy; it never falls back to the older file.
Previously delivered copies cannot be recalled.
| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/quotes | any quote capability | status (comma list), clientOrganizationId, projectId, open, q, limit, offset |
GET | /api/v1/finance/quotes/pipeline | any quote capability, with prices | clientOrganizationId. All open quotes at recorded reporting values, with missing valuation counted and native amounts grouped in unvalued.byCurrency |
GET | /api/v1/finance/quotes/lost-reasons | any quote capability | The organisation’s configurable list |
POST | /api/v1/finance/quotes | finance:manage_quotes | clientOrganizationId and title are required, then currency, projectId, billingEntityId, validUntil, expectedCloseDate, probabilityBp, termsText, scopeText |
GET | /api/v1/finance/quotes/:id | any quote capability | versionId reads an earlier version |
PATCH | /api/v1/finance/quotes/:id | finance:manage_quotes | Header fields. 409 QUOTE_NOT_EDITABLE once the quote has been sent |
PATCH | /api/v1/finance/quotes/:id/composition | finance:manage_quotes | coverText, scopeText, assumptionsText, exclusionsText, notes, termsText, documentLanguage, priceEntryMode, roundingMode, selectionLocked |
POST | /api/v1/finance/quotes/:id/clone | finance:manage_quotes | versionId, title |
POST, PATCH, DELETE | /api/v1/finance/quotes/:id/sections[/:sectionId] | finance:manage_quotes | name is required on create, then description, position, clientVisibility, packageGroup |
POST, PATCH, DELETE | /api/v1/finance/quotes/:id/lines[/:lineId] | finance:manage_quotes | serviceTypeId and name are required on create. The budget line fields, plus optional, packageGroup, requiredInPackage, selectedByDefault and hiddenContingency |
PUT | /api/v1/finance/quotes/:id/schedule | finance:manage_quotes | items, each with kind, label, either amount or percentBp, trigger, dueOn, milestoneRef and lineAllocation |
POST | /api/v1/finance/quotes/:id/request-approval | finance:send_quotes | Returns outcome and approvalRequestId |
POST | /api/v1/finance/quotes/:id/send | finance:send_quotes | recipients, cc, message. Returns outcome, approvalRequestId, hostedUrl and version. 409 ENVELOPE_TOO_WIDE when the selectable options make too many combinations to enumerate, so an approver could not see the worst case: lock the version’s selection or cut the optional items back. Also 409 APPROVAL_STALE or QUOTE_STATE, and 400 QUOTE_INVALID with details.issues |
POST | /api/v1/finance/quotes/:id/resend | finance:send_quotes | recipients, cc, message. Returns the hostedUrl for the version already sent |
POST | /api/v1/finance/quotes/:id/revise, .../withdraw | finance:send_quotes | Both take a reason. 409 REVISION_OPEN when a revision is already open |
POST | /api/v1/finance/quotes/:id/lost-reason | finance:manage_quotes | lostReasonId, note |
POST, DELETE | /api/v1/finance/quotes/:id/link, .../links | finance:send_quotes | Mint a fresh hosted link, or revoke every live one. 409 before the quote has been sent |
POST | /api/v1/finance/quotes/:id/convert | finance:manage_budgets, plus project:create or project:update when a project is involved | mode (new_project, existing_project, standalone), projectId, projectName, budgetName, overrideReason. 201, or 200 when it was already converted. 409 QUOTE_NOT_ACCEPTED |
POST | /api/v1/finance/quotes/price-brief | finance:manage_quotes | briefJobId is required, then projectId, title, validUntil, expectedPlanRevision, expenseAllowances. 201, or 200 when that plan revision was already priced. 409 PLAN_REVISION_STALE. Roles with no rate come back in unpricedRoles rather than being priced at a guess |
GET | /api/v1/finance/quotes/:id/lineage | any quote capability | versionId. The per-task sources behind the price, with role and phase aggregates |
Change orders sent to the client, and the terms library
Section titled “Change orders sent to the client, and the terms library”| Method | Path | Capability | Notes |
|---|---|---|---|
POST | /api/v1/finance/budgets/:budgetId/change-orders/:versionId/send | finance:manage_budgets | recipients, cc, message. Returns hostedUrl and requiresClientAcceptance |
POST | /api/v1/finance/budgets/:budgetId/change-orders/:versionId/cancel-acceptance | finance:manage_budgets | reason. Revokes the link and lifts the acceptance requirement |
GET | /api/v1/finance/budgets/:budgetId/change-orders/:versionId/acceptance | finance:manage_budgets | requiresClientAcceptance, acceptedAt, declinedAt |
GET | /api/v1/finance/terms | any quote capability | kind, clientOrganizationId |
POST, PATCH, DELETE | /api/v1/finance/terms[/:id] | finance:manage_settings | name and body are required on create, then kind, clientOrganizationId, billingEntityId, isDefault. Editing the body bumps the version, and DELETE archives |
Applying a version that is marked as needing client acceptance before the
client has accepted answers 409 CLIENT_ACCEPTANCE_REQUIRED on every path.
A reduction below what has already been recognised or invoiced answers 409
REDUCTION_BELOW_POSTED, and a closed accounting period answers 409
PERIOD_CLOSED.
New retainers default trueUp to true when overuseTreatment is true_up.
Explicit false and existing settings are preserved. trueUp must be a JSON
boolean. overageRateAmount accepts null or a nonnegative minor-unit string
up to 9223372036854775807; malformed values return 400. Explicit true-up
with another recovery treatment also returns 400.
After any consumption window closes, changing trueUp, overuseTreatment,
overagePolicy or overageRateAmount returns 409. Unchanged values and
unrelated edits remain allowed. Before close, selecting another recovery
treatment without a trueUp value turns true-up off.
True-up requires billable overage terms, true-up recovery, an enabled flag and
a closed, non-cancelled period with a positive trueUpAmount. The period fee
and true-up can be invoiced separately. Period-end policies find an outstanding
true-up even after the fee was invoiced. Read reason on successful results:
it explains when requested excess was skipped. These rules apply equally to
REST and MCP. Review older missed charges before making a separate corrective
invoice; saved opt-outs and issued invoices are not changed automatically.
Client billing portal settings
Section titled “Client billing portal settings”Use PATCH /api/v1/finance/clients/:clientId/billing-profile with
finance:manage_settings to change portalEnabled, allowChangeOrderAcceptance
and showBudgetConsumption. Send booleans; null, strings, numbers, arrays and
objects are rejected with 400 and leave the profile unchanged. Omitted values
are kept. Turning the portal off keeps the other settings and saved documents.
The client still needs the relevant portal permission in their own organisation.
Read back the saved profile to confirm a change. The individual profile GET
creates default settings when a valid client has no profile yet; it does not
enable access. List and detail reads include the flags. A visible creditLimit
is a minor-unit string or null; it is omitted from reads and PATCH responses
when the caller cannot see it.
The same change is available through MCP’s call_public_api using
PATCH /finance/clients/:clientId/billing-profile. The named
get_client_billing_profile reads the same flags. See
Client Portal Billing for the manual setup.
Retainers
Section titled “Retainers”| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/retainers | finance:manage_retainers or finance:manage_budgets | status, clientOrganizationId |
POST | /api/v1/finance/retainers | finance:manage_retainers | clientOrganizationId, name, cadence and anchorDate are required, then startDate, endDate, templateBudgetId, generateAheadPeriods, rolloverPolicy, rolloverCapHours, rolloverCapAmount, rolloverExpiryPeriods, overagePolicy, overageRateAmount, overuseTreatment, invoicePolicy, invoiceDayOffset, trueUp, allowAutoIssue |
GET | /api/v1/finance/retainers/:id | finance:manage_retainers or finance:manage_budgets | The series, its period strip with regenerable per period, the next period start and the outstanding carry |
PATCH | /api/v1/finance/retainers/:id | finance:manage_retainers | Header fields. The cadence and the anchor cannot change once periods exist: that answers 409, and the way forward is to end the retainer and start a new one |
POST | /api/v1/finance/retainers/:id/generate | finance:manage_retainers | Catches up any periods that are due. Returns created and skipped, and is safe to repeat |
POST | /api/v1/finance/retainers/:id/pause, .../resume | finance:manage_retainers | Pause takes a reason and records the resume point; resume catches up the periods that were missed |
POST | /api/v1/finance/retainers/:id/end | finance:manage_retainers | settlement is required (invoice_outstanding, forfeit_carry, credit_carry), and endDate is optional |
POST | /api/v1/finance/retainers/:id/apply-to-future | finance:manage_retainers | Returns regenerated and, for each period it left alone, a skipped entry with its periodIndex and reason. Recorded time, manual adjustments, reserved invoice work and financial period locks prevent rebuilding; existing carry is retained |
GET | /api/v1/finance/retainer-periods/:periodId | finance:manage_retainers or finance:manage_budgets | Consumption for the period: allowance, used, included and excess per line with its money, the breakdown by project, and what is still outstanding |
POST | /api/v1/finance/retainer-periods/:periodId/close-window | finance:manage_retainers | reason. 409 PERIOD_CLOSED |
POST | /api/v1/finance/retainer-periods/:periodId/adjustments, /api/v1/finance/retainer-adjustments/:adjustmentId/reverse | finance:manage_retainers | kind and reason are required on an adjustment, then quantity and amount. A reversal needs a reason |
POST | /api/v1/finance/retainer-periods/:periodId/invoice | finance:draft_invoices | includeFee, includeTrueUp, runPolicy. Returns outcome (drafted, issued, pending_approval, already_invoiced, nothing_to_invoice or not_due), a reason and the invoice |
Mid-period proration and termination adjustments answer 501 rather than
guessing a figure.
Metrics, revenue and reports
Section titled “Metrics, revenue and reports”Every finance figure comes from one metric service, so a number in a report, a dashboard, an export and an MCP answer is the same number with the same published definition. See the Revenue, Forecast, Finance reports and Month end guides for the product behaviour.
Reads in this section take any of finance:view_prices, finance:view_cost,
finance:view_profit, finance:manage_recognition or
finance:close_periods. The tables call that any figures capability.
Scope goes into the query rather than on top of the answer: a scoped total is
computed from scoped rows. If your only grant is a project or client scope,
name the subject you are asking about with projectId or
clientOrganizationId. Without a subject your organisation-wide lens applies,
which for a scoped-only holder carries no money, so the money metrics are
absent from the result.
Money comes back on a metric cell shaped
{ metric, unit, amount, decimal, currency, unvalued }: amount is minor
units in the reporting currency, decimal is major units for display and
export, and unvalued counts the rows that had no frozen reporting value and
were left out of the total rather than counted as zero. A figure your lens
does not cover is absent, not null.
invoiced_revenue includes issued standalone and manual invoice lines, less
issued credit notes, excluding tax and dated by issue date. It uses recorded
reporting values. Missing budget/project attribution is unallocated rather than
excluded from organisation or client totals; a specific dimension filter still
requires a matching link. Saved reports, exports and MCP reads use this definition.
The forecast omits currencies[].overdue without prices access. Cost-only reads
retain permitted estimate-at-completion costs, while cash, revenue and profit
amounts remain absent. Handle a missing summary without interpreting it as zero.
Generated REST and MCP call_public_api return the same projection.
Metrics and revenue
Section titled “Metrics and revenue”Each POST /api/v1/finance/revenue/runs evaluates current approved work and method
inputs, including changes made after an earlier run on the same day. It returns
a new run with current coverage and posts only the difference from previously
recognised revenue. Unchanged or concurrent repeats add no duplicate revenue.
A completed run also saves budget and line recognisedAmount and
reportingRecognisedAmount for subsequent authorised budget reads. Zero-delta
and partial runs update these totals from retained entries, including archived
line history. Missing frozen reporting values remain null; unrelated budget
amounts are unchanged. Failed runs commit neither new entries nor changed totals.
The route accepts no retry key. MCP call_public_api uses the same operation
with the same permissions and validation.
| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/metrics/catalogue | any figures capability | Every metric you may read, with its published definition, unit, groupings, dating and report link. The list shrinks with your lens |
GET | /api/v1/finance/metrics | any figures capability | metrics (comma list), from, to, groupBy, billingEntityId, clientOrganizationId, projectId, budgetId, serviceTypeId, revenueLens (recognised, invoiced or cash). Returns reportingCurrency, from, to, groupBy, rows, totals, coverage, definitions and dating, which says per metric whether it was bucketed by the economic period (recognition) or the posting date (invoicing, cash and cost). An unknown metric is a 400, and so is a grouping the metric does not support |
GET | /api/v1/finance/revenue | any figures capability | Recognised, invoiced, WIP and deferred over a range, grouped |
GET | /api/v1/finance/revenue/runs[/:id] | any figures capability | billingEntityId, periodId, limit on the list. The detail carries the run’s frozen inputs, its coverage failures and its entries |
POST | /api/v1/finance/recognition/catch-up | finance:manage_recognition | Historical approved time; optional reason (up to 2,000 characters) for soft-closed source months. Closed months refuse. Returns { posted, scanned } |
POST | /api/v1/finance/revenue/runs | finance:manage_recognition | billingEntityId and period (YYYY-MM), with an optional budgetId. Returns 201 with the run, its entries and its coverage. 409 RECOGNITION_RUN when every period from the economic one onward is closed |
GET | /api/v1/finance/revenue/lines/:lineId/explain | any figures capability | The source rows, their frozen exchange rates, the method formula and basis, the runs that wrote them, the versioned method inputs and a fingerprint. Add since to get changedSince |
PATCH | /api/v1/finance/revenue/lines/:lineId/method-inputs | finance:manage_recognition | effectiveFrom, inputs, reason. 400 when a manual distribution does not add up to sold |
WIP, forecast, costs, journal and tax
Section titled “WIP, forecast, costs, journal and tax”| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/wip | any figures capability | asOf, movementFrom, and entity, client, project and budget filters. Balances with ageing, rollups by budget, client, entity, service type and project manager, the top contributors and the monthly movement |
GET | /api/v1/finance/forecast | any figures capability | asOf, horizonMonths. One view per currency: committed, committedWeekly, pipeline, pipelineWeekly, cash and cashWeekly, plus estimateAtCompletion, forecastUtilisation, revenuePerPerson and lateness |
GET | /api/v1/finance/operating-costs | finance:view_cost or finance:manage_settings | The operating cost register for a billing entity |
POST, PATCH | /api/v1/finance/operating-costs[/:id] | finance:manage_settings | archived: true archives, archived: false restores |
GET, POST | /api/v1/finance/overhead/proposals | read finance:view_cost or finance:manage_settings, write finance:manage_settings | billingEntityId, from and to are required, and billableCapacityHours is optional. A proposal is computed, not stored |
POST | /api/v1/finance/overhead/proposals/:id/publish | finance:manage_settings | :id is the computation key, so send billingEntityId, from and to again: publishing recomputes under a lock. 201 the first time, 200 with created: false and the same rate on a repeat |
GET, POST | /api/v1/finance/journal | read finance:view_prices or finance:close_periods, write finance:manage_recognition | periodId and billingEntityId on the read. POST takes periodId, recognitionRunId, destination (csv or xero) and currencies, and builds the journal against the period’s run and close generation |
POST | /api/v1/finance/journal/:id/export | finance:manage_integrations | Validates first: 409 JOURNAL_INVALID with the validation block when an account mapping is missing. A csv journal comes back as file text, and a xero destination is refused |
GET | /api/v1/finance/tax/bas | finance:view_prices or finance:close_periods | billingEntityId, from, to. Sales and purchases on both the cash and accrual bases, G1, 1A and 1B, grouped by tax rate identity |
Recognition journal rebuilds retain reversed original WIP adjustments and their
posted reversals in their recorded periods. totals.accruedMovement includes
signed adjustment movement; totals.reporting.adjustments uses signed recorded
reporting values with missing-valuation coverage. Draft and validated journals
can rebuild. Exported journals retain their original content. The separate
revenue reconciliation continues to compare recognised and invoiced revenue.
These rules also apply through call_public_api.
For newly created correction journals, totals.accruedMovement,
totals.deferredMovement, totals.revenueMovement, adjustment buckets and
totals.reporting.adjustments reverse the original journal’s signs. Debit and
credit totals remain positive. Recorded period recognition, invoicing, credits
and coverage stay unchanged. totals.reconciliation starts with the original
journal’s projected ledger total, then applies the correction. Its reconciles
flag can be false because the period’s recognised revenue has not changed.
Missing legacy figures are omitted. Existing exported records remain unchanged.
Listing and exporting a saved correction use the routes above, including through
call_public_api. There is no public route or named MCP tool to create a journal
correction. See Month End for reviewing the period.
Saved reports, findings and month end
Section titled “Saved reports, findings and month end”| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/reports | any figures capability | The reports you can see, the report families and their schedules. includeArchived, reportKey |
POST, PATCH | /api/v1/finance/reports[/:id] | any figures capability | reportKey, name, filters, grouping, columns, sort, visibility. archived on the PATCH archives or restores |
GET | /api/v1/finance/reports/:id/run | any figures capability | Runs under your own lens and scope. Returns rows, totals, coverage, definitions for the glossary panel, and link |
GET | /api/v1/finance/reports/:id/export | finance:export | format (csv or xlsx). The file opens with a header block stating the filters, the currency, the coverage and each metric’s tax basis |
POST | /api/v1/finance/reports/:id/schedules | any figures capability | cadence (daily, weekly, monthly), dayOfWeek, dayOfMonth, minuteOfDay (organisation time), format, destinationKind (email, slack, teams), destination. Recipients have to be members of the organisation, and every delivery re-checks the owner’s live authority at run time rather than the authority they held when they saved it |
DELETE | /api/v1/finance/report-schedules/:id | any figures capability | Stops a scheduled delivery. Returns 204 |
GET | /api/v1/finance/findings | any figures capability | Your own findings from the nightly review, re-checked against your live lens and scope. Returns findings, withheld and run |
POST | /api/v1/finance/findings/:id/dismiss | any figures capability | Dismisses one of your own findings |
GET | /api/v1/finance/periods/:id/assistant | Unrestricted organisation-wide finance:view_prices, with no project/client price denies | The month-end brief: blocking, advisory, canClose, invoiceDrafts, movement, closeSummary and nextActions, with draftsOnly: true. It never closes the period |
GET | /api/v1/finance/periods/:id/close-report | finance:close_periods or finance:view_prices, plus unrestricted organisation-wide price and cost access | The report frozen against the period’s close generation, with every earlier generation in history |
GET | /api/v1/finance/periods/:id/export-pack | finance:export, plus unrestricted organisation-wide price and cost access | closeGeneration, generatedAt and files, each with a name and its csv. Every file states its period, filters, currency and coverage before the header row |
Closing a period, POST /api/v1/finance/periods/:id/close in
Uninvoiced work and periods, also takes
runRecognition, which is true unless you send false: the close runs
recognition for the period first, then checks inside its own transaction that
the figures still stand up. If they do not it answers 409
RECOGNITION_NOT_FRESH with period, reason, pendingLines and
notComputable.
A successful close also saves purchase accrual balances separately by currency. Saving those balances and closing the period succeed or fail together. Revenue recognition may already have completed before a close failure. Repeating a close on a closed period leaves the saved balances unchanged. Reopening keeps them until the next close replaces them. Older closed periods are not automatically backfilled. Closing requires the existing period-closing permission; reading accrual balances separately still requires cost access to every contributor.
Online payments and the client portal
Section titled “Online payments and the client portal”Merchant payments run on the organisation’s own Stripe account, joined through Stripe Connect. Charges are direct charges on that account, so the organisation is the merchant of record and Runnit never holds client funds. This shares nothing with Runnit’s own subscription billing: different credentials, different webhook, different records. See the Online Payments, Payment Plans, Late Fees and Client Portal Billing guides for the product behaviour.
Where the platform is not configured for merchant payments, reads answer
normally and payment eligibility comes back with
reason: "not_configured".
Workspace routes here sit on the shared finance router, so each one answers on
both /api and /api/v1. Two lanes deliberately do not: the webhook and the
Connect OAuth callback carry no session and assert no capability.
Connections
Section titled “Connections”| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/payments/connections | finance:manage_integrations | configured and connections. A connection never carries a credential, a webhook secret or an OAuth bundle |
POST | /api/v1/finance/payments/connections | finance:manage_integrations | Starts the Connect flow. Returns authorizeUrl and connectionId, and sets an HttpOnly cookie that binds the browser |
POST | /api/v1/finance/payments/connections/redeem | finance:manage_integrations | code, plus the binding cookie. 403 when the organisation, the person or the browser differs from the one that started it |
POST | /api/v1/finance/payments/connections/:id/refresh | finance:manage_integrations | Pulls the connected account’s live capabilities |
PATCH | /api/v1/finance/payments/connections/:id | finance:manage_integrations | enabled, statementDescriptor, webhookSecret. Enabling re-checks eligibility and answers 409 when Stripe has not enabled charges on the account |
DELETE | /api/v1/finance/payments/connections/:id | finance:manage_integrations | Deauthorises at Stripe, then clears the stored credential |
GET | /api/v1/finance/payments/connections/:id/events | finance:manage_integrations | The last 50 received provider events with their processing state |
The application fee and the policies
Section titled “The application fee and the policies”| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/payments/application-fee | finance:manage_integrations | fee (basisPoints, source, basis) and canOverride |
PUT | /api/v1/finance/payments/application-fee | platform administrator | basisPoints, 0 to 500, or null to clear. 403 for everyone else, including the organisation owner |
PUT | /api/v1/finance/payments/entities/:entityId/late-fees | finance:manage_settings | enabled, annualRateBp, graceDays, intervalDays, minimumFeeAmount, dayCountBasis (actual_365, actual_360, thirty_360), preferStatutory, compensationEnabled, contractReference, taxRateId |
PUT | /api/v1/finance/payments/entities/:entityId/surcharges | finance:manage_settings | enabled, methodRatesBp per method (0 to 1000), fixedAmount, disclosureText, taxRateId. Off by default, and a jurisdiction prohibition wins whatever is set |
The effective application fee is snapshotted onto each payment attempt with its basis and its source, so changing the configuration never alters a retry, and a refund returns the fee in proportion. No public, portal or client-supplied parameter can influence it.
Payments on an invoice
Section titled “Payments on an invoice”Reads take any invoice capability.
| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/invoices/:id/payment-status | any invoice capability | onlinePaymentState (none, available, processing, settled, disputed), paymentProcessingAmount, settlementConfirmedAt, disputeWindowEndsAt, eligible, reason, attempts and disputes |
POST | /api/v1/finance/invoices/:id/payment-attempts | finance:record_payments | Creates or adopts the invoice’s live attempt. Never returns a client secret: that belongs to the payer’s own surfaces |
POST | /api/v1/finance/invoices/:id/charge-mandate | finance:record_payments | mandateId, optional amount and scheduledDebitOn. 202 with waiting.reason when the advance notice for this debit is not satisfied, 201 when it charges |
GET | /api/v1/finance/invoices/:id/debit-notices | any invoice capability | The advance notices raised for this invoice and their delivery state |
GET | /api/v1/finance/payments/mandates | any invoice capability | Stored cards and bank accounts, display detail only |
DELETE | /api/v1/finance/payments/mandates/:id | finance:record_payments | Revokes consent |
An attempt is keyed from the payable balance (the amount due less anything already authorised and still in flight), read under the invoice lock, not from the document version. At most one attempt per invoice is live, so two callers adopt the same one instead of racing two; when the balance moves the intent is updated in place, and when it reaches zero the intent is cancelled. A payment that succeeds after a credit closed the invoice keeps its receipt and becomes client credit.
Money authorised on a bank rail is a collection state, not a status. It
never reaches amountPaid, cash or recognition, and it is subtracted from the
payable balance so it cannot be collected twice. The invoice status union,
receivables and reminders are unchanged.
Payment plans
Section titled “Payment plans”Recording positive cash through POST /api/v1/finance/payments updates the
active plan’s oldest unpaid instalments in the same save. Reusing the receipt’s
idempotencyKey does not settle it twice. Payments recorded before activation
never settle a later or replacement plan. Partial-amount plans retain their
agreed total. These effects also apply through call_public_api.
Activation returns 409 if the proposed amount exceeds the current invoice
balance. Cancel that proposal and create a new one.
Refunds and imported-receipt reversals restore only money settled against the
same live plan. Client-credit applications, credit notes and write-offs reduce
the agreed unpaid amount in the same save. Partial agreements are never expanded
to the whole invoice debt. Paid instalments retain their receipt history. A
fully credited tail completes the plan; zero instalments are cancelled.
Completed, defaulted and cancelled plans are not reopened. The same validation,
permissions and effects apply through call_public_api.
Older unmatched receipts and previously inconsistent schedules still need review as described in Payment Plans.
| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/payments/plans | finance:manage_receivables | Filters by status and clientOrganizationId |
GET | /api/v1/finance/payments/plans/:id | finance:manage_receivables | plan, instalments, settledAmount, outstandingAmount, nextDueOn and missedCount |
POST | /api/v1/finance/payments/plans | finance:manage_receivables | invoiceId, instalmentCount (2 to 60), cadence (weekly, fortnightly, monthly), firstDueOn, optional amount and note. Creates a proposed plan, which collects nothing |
POST | /api/v1/finance/payments/plans/:id/activate | finance:manage_receivables | Activates it and pauses the invoice’s reminders |
POST | /api/v1/finance/payments/plans/:id/cancel | finance:manage_receivables | reason. Resumes reminders |
Instalments always sum to the plan total exactly and create no cash: an instalment is settled only by a real payment allocated to the invoice. A missed instalment defaults the plan and resumes reminders.
Late fees
Section titled “Late fees”| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/payments/late-fees | finance:manage_receivables | Assessments, filterable by invoice and status |
GET | /api/v1/finance/invoices/:id/late-fee-preview | finance:manage_receivables | The next chargeable interval with its rate, basis, day count, interest and compensation, or the reason there is none |
POST | /api/v1/finance/invoices/:id/late-fees | finance:manage_receivables | Assesses the next interval. 409 LATE_FEE_NOT_CHARGEABLE when there is none |
POST | /api/v1/finance/payments/late-fees/:id/invoice | finance:manage_receivables | Raises the fee as a separate invoice through the ordinary drafting and issue path. Idempotent |
POST | /api/v1/finance/payments/late-fees/:id/waive | finance:manage_receivables | reason. Refused once the fee has been invoiced |
An assessment is unique on the invoice and its interval, so a repeated run cannot charge the same days twice, and intervals never double-count the boundary day. A fee invoice cannot itself accrue a fee.
Held settlements and collections
Section titled “Held settlements and collections”| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/payments/held-settlements | finance:record_payments | Settlements held out of a closed period, with their economic date |
POST | /api/v1/finance/payments/held-settlements/:id/resolve | finance:record_payments | postOn, reason. Posts it into an open period, keeping the economic date on the record |
GET | /api/v1/finance/receivables/clients/:id/brief | finance:manage_receivables | The account summary before a collections call |
POST | /api/v1/finance/receivables/clients/:id/draft | finance:manage_receivables | tone, invoiceIds, proposePlan, instalmentCount, cadence. Draft only: answers draftOnly: true, sends nothing and creates no plan |
PATCH | /api/v1/finance/quotes/:id/deposit-at-acceptance | finance:manage_quotes | enabled, percentBp. Refused once the quote is accepted |
A settlement whose economic date falls in a closed period is held rather than discarded or forced in, and the webhook still answers normally.
Token-authorised lanes (not on /api/v1)
Section titled “Token-authorised lanes (not on /api/v1)”| Method | Path | Notes |
|---|---|---|
POST | /api/finance/payments/webhooks/stripe | Verifies the signature over the raw bytes. 401 on a bad signature, 400 on an unparseable body, 200 otherwise. Receiving records the event; processing is separate, so a replay records once |
GET | /finance/payments/oauth/callback | Auth host only. Exchanges the code and redirects to the tenant with a single-use redemption code |
The client portal
Section titled “The client portal”Portal routes are participation routes: a client user authenticates but does not consume a seat on the agency’s plan, so the Finance entitlement does not apply. Authority is resolved live on every request from four things together: an active client relationship with an agency reachable from the request host, an active membership in the client organisation, the capability below held in the client organisation, and the agency’s per-client toggle.
| Method | Path | Client capability |
|---|---|---|
GET | /api/portal/billing/summary | client_portal:billing |
GET | /api/portal/billing/invoices | client_portal:billing |
GET | /api/portal/billing/invoices/:id | client_portal:billing |
GET | /api/portal/billing/invoices/:id/pdf | client_portal:billing |
POST | /api/portal/billing/invoices/:id/pay | client_portal:billing |
POST | /api/portal/billing/invoices/:id/queries | client_portal:billing |
GET | /api/portal/billing/statements | client_portal:billing |
GET | /api/portal/billing/credits | client_portal:billing |
GET | /api/portal/billing/payment-methods | client_portal:billing |
GET | /api/portal/billing/plans/:id | client_portal:billing |
GET | /api/portal/budgets/consumption | client_portal:budgets |
GET | /api/portal/billing/statements/:id/pdf | client_portal:billing |
GET | /api/portal/budgets/retainers | client_portal:budgets |
GET | /api/portal/acceptance, /api/portal/acceptance/:subjectType/:id, /api/portal/acceptance/:subjectType/:id/pdf | client_portal:approve_changes |
POST | /api/portal/acceptance/:subjectType/:id/code, /accept, /decline, /queries | client_portal:approve_changes |
These routes require a signed-in client account and are deliberately absent
from generated-token /api/v1 and MCP. Statement downloads return 409
RENDER_PENDING until their verified PDF is ready. Retainer and pending-acceptance
lists accept offset (0 to 100000) and limit (1 to 50, default 25), returning
items, hasMore, offset and limit. Retainer hours are absent as a group when
hours are hidden; only client-visible period budgets qualify. Period ends and
rollover expiry boundaries are exclusive.
Acceptance addresses an issued version: subjectType is quote_version or
budget_version, and id is that version’s id. The GET supports comma-separated
selection ids. Accept takes signerName, signerEmail, optional selection,
signatureText and code; the document’s signature and OTP requirements still
apply. Code requests take email and action (accept or decline). Decline
accepts reason, declinedByName, declinedByEmail and code. Quote queries take
message and optional from; change orders do not support queries. Acceptance
returns 201, or 200 with the original receipt on replay. Client acceptance keeps
the agency’s internal approval requirement; applied: false means the change has
been signed but not applied. Access is checked again before recording a decision.
Every document is fetched by agency and client together, never by id alone, so
another client’s document answers 404, never 403. Each response carries a
fixed field list rather than a filtered copy of the workspace payload: no cost,
no margin, no internal notes, no staff names, and no provider identifier or
secret. The rendered body is filtered before it is rendered; a PDF cannot
be filtered after the fact, so one whose appendix this client may not see is
refused rather than served in a reduced form. Every response sends
Cache-Control: no-store, Referrer-Policy: no-referrer and
X-Robots-Tag: noindex, nofollow. Paying and asking a question are rate
limited per user, and everything is rate limited per address.
Ledgers, e-invoicing, scenarios, self-billing, suppliers and bank statements
Section titled “Ledgers, e-invoicing, scenarios, self-billing, suppliers and bank statements”A second accounting ledger, e-invoicing over Peppol, scenarios beside a budget or a quote, recipient-created tax invoices, the supplier area, bank statement matching, and the jurisdiction rules that decide what a document may say. See the Accounting Connections, E-Invoicing, Scenarios, Self-Billing, Supplier Portal, Bank Reconciliation and Tax Rules by Jurisdiction guides for the product behaviour.
Each of these ships off: e-invoicing is disabled on every billing entity and
client profile, tax calculation is off on every entity, and self-billing is
off on every supplier. A QuickBooks connection needs Intuit credentials
configured on the deployment.
Workspace routes sit on the shared finance router, so each answers on both
/api and /api/v1. Three lanes deliberately do not: the Peppol callback
receiver and the hosted purchase order page carry no session, and the supplier
area authenticates with its own credential rather than a Runnit session.
A second accounting ledger
Section titled “A second accounting ledger”The phase 3 ledger routes now serve every registered provider, selected per connection, and an entity may hold one connection per provider. An issued document fans out to every enabled connection, and each keeps its own remote identity, mapping set and status.
| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/ledger/connections/:id/receipts | finance:manage_settings or finance:manage_integrations | Provider receipts with their allocation children. Money goes through the finance lens |
GET | /api/v1/finance/ledger/receipt-allocations | same | The reconciliation queue: allocations nothing matched, and allocations more than one local payment could be. An ambiguous identity is never allocated automatically |
GET | /api/v1/finance/ledger/connections/:id/cursors | same | Change feed cursors, carrying windowExpired and recoveryRequired |
PUT | /api/v1/finance/ledger/connections/:id/production | finance:manage_integrations | enabled, evidence, note. 409 LEDGER_NOT_READY while the connection is not ready, 400 LEDGER_EVIDENCE_REQUIRED with no recorded evidence. Records who, when and what was checked |
Behaviour changes on the existing routes:
POST /api/v1/finance/ledger/connections/startwithprovider: "quickbooks"answers 503, a known provider with no credentials configured here.netsuite,sage_intacctandmyobanswer 400: the value is accepted so a future adapter needs no migration, but no authorisation flow exists.POST /api/v1/finance/ledger/connections/redeemanswers 409 when the provider returns a different company file from the one already bound.- Connection reads carry
capabilities,unsupportedOperations,productionEnabledand a reference summary that now counts items, classes and departments. Readiness addsitem_unknown,item_no_income_account,item_inactiveandadapter_not_production_proven.
An operation a provider cannot do is skipped with the reason on the operation row rather than retried against a method that does not exist.
E-invoicing
Section titled “E-invoicing”Reads take finance:view_invoices or finance:manage_settings.
| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/einvoicing/settings | read | The mapped business terms, the implemented rule set, and the registered provider |
PUT | /api/v1/finance/einvoicing/entities/:id/settings | finance:manage_settings | einvoiceEnabled, einvoiceProfile, endpoint scheme and id, legal registration. Off by default |
PUT | /api/v1/finance/einvoicing/clients/:id/settings | finance:manage_settings | The same for a client billing profile, plus einvoiceChannel and buyerReference |
GET, POST | /api/v1/finance/einvoicing/participants | read, finance:manage_settings | Addressable identities for an entity or a client |
DELETE | /api/v1/finance/einvoicing/participants/:id | finance:manage_settings | |
POST | /api/v1/finance/einvoicing/participants/:id/lookup | finance:manage_settings | Resolves through the provider. A participant that does not resolve blocks submission |
POST | /api/v1/finance/einvoicing/participants/:id/primary | finance:manage_settings | |
GET | /api/v1/finance/invoices/:id/einvoice | read | Eligibility. On a draft, problems[] names each missing business term and where it would come from |
GET | /api/v1/finance/invoices/:id/einvoice/snapshot | read, and the prices lens | The data frozen at issue, with frozen. An issued document that never had one answers snapshot: null rather than 404: it is ineligible, and saying so is more useful |
POST | /api/v1/finance/invoices/:id/einvoice/submit | finance:issue_invoices | 202 in flight, 200 when an identical earlier submission is adopted, 409 with the failed rules when the rule set refuses it |
GET | /api/v1/finance/einvoicing/documents/:id | read | Submission and delivery state, with the events behind it |
GET | /api/v1/finance/einvoicing/documents/:id/xml | read, and the prices lens | The transmitted document as evidence, with its X-Content-Sha256 |
POST | /api/v1/finance/einvoicing/documents/:id/recheck | finance:manage_integrations | Administrative recovery for a stuck submission |
POST | /api/v1/finance/einvoicing/documents/:id/cancel | finance:manage_integrations | Refused once the network has delivered it |
GET | /api/v1/finance/invoices/:id/deliveries | read | Every channel the document went out on |
POST | /api/v1/finance/invoices/:id/deliveries/authorise | finance:issue_invoices | channel, recipient, reason. The reason is required and peppol is refused as an alternative to itself. The invoice row is unchanged |
POST | /api/public/peppol/callbacks/:provider | none, signature verified | Public, raw body. An unknown provider and a bad signature answer identically. Not on /api/v1 |
Submission is idempotent on the document version and the content of the
snapshot. Refusals carry a code: SNAPSHOT_MISSING, ENTITY_NOT_ENABLED,
CLIENT_NOT_ENABLED, PARTICIPANT_NOT_FOUND, RECEIVER_ADDRESS_MISSING,
NOT_ISSUED, INVOICE_VOIDED, ALREADY_DELIVERED.
Scenarios
Section titled “Scenarios”Scenario line inputs accept sectionName and pricing: discountBp,
discountAmount, markupBp, capAmount, percentBp, percentageBase,
burnBasis and recognitionMethod. Percentage fields use basis points; amounts
use minor-unit strings. Choose a fixed or percentage discount. Tax evidence is
inherited from source lines. Quote comparisons use default selections.
estimatedCost: null means unknown; an explicit "0" means estimated zero.
missingCostEstimates marks a partial cost subtotal and suppresses margin.
missingHoursEstimates marks incomplete effort and suppresses hours deltas and
modelled duration. Promotion is atomic, creates named sections in the draft,
retains matched quote schedule allocations, and refuses changed pricing with
SCENARIO_PRICE_CHANGED or allocated-line removal with
SCENARIO_SCHEDULE_REVIEW_REQUIRED (409).
A scenario is draft only. Nothing here writes a budget line, a total, a cache, the billing schedule, recognition or an approval, and every apply path refuses a scenario.
| Method | Path | Capability | Notes |
|---|---|---|---|
GET, POST | /api/v1/finance/budgets/:id/scenarios | finance:view_prices or finance:manage_budgets to read, finance:manage_budgets to create | Scenarios are numbered on their own counter, independent of the budget version counter, and do not occupy the one open change order |
GET | /api/v1/finance/budgets/:id/scenarios/comparison | read | Total, cost, margin, hours and duration in days as its own figure, with durationSource and durationBasis. A scenario with no team assumptions answers null, never a number derived from the hours |
GET, PATCH, DELETE | /api/v1/finance/scenarios/:scenarioId | read, finance:manage_budgets | A promoted scenario is read only |
PUT | /api/v1/finance/scenarios/:scenarioId/lines | finance:manage_budgets | |
POST | /api/v1/finance/scenarios/:scenarioId/archive, /restore | finance:manage_budgets | |
POST | /api/v1/finance/scenarios/:scenarioId/promote | finance:manage_budgets | Opens a distinct change order through the ordinary path, with lineage to an immutable snapshot. 409 on a stale base, naming the version the budget is on now. Two concurrent promotions produce one change order |
GET, POST | /api/v1/finance/quotes/:id/scenarios | finance:view_prices or finance:manage_quotes to read, finance:manage_quotes to create | Keeps the selection and package semantics a budget snapshot has no columns for |
GET | /api/v1/finance/quotes/:id/scenarios/comparison | read | |
GET, PATCH, DELETE | /api/v1/finance/quote-scenarios/:scenarioId | read, finance:manage_quotes | |
PUT | /api/v1/finance/quote-scenarios/:scenarioId/lines | finance:manage_quotes | |
POST | /api/v1/finance/quote-scenarios/:scenarioId/archive, /restore | finance:manage_quotes | |
POST | /api/v1/finance/quote-scenarios/:scenarioId/promote | finance:send_quotes | Fills the open draft, or opens the next version as a draft. Sent and accepted versions are untouched, and an accepted quote is refused |
Money on a scenario follows the same lens as everywhere else: a caller holding prices and not cost has no cost or margin key at all. Hours and duration are not money and stay visible.
Self-billing
Section titled “Self-billing”Reads take a purchase read capability. Every figure on a document is cost, so
finance:view_cost is needed to see any of them.
| Method | Path | Capability | Notes |
|---|---|---|---|
GET, POST | /api/v1/finance/self-billing/agreements | purchase read, then finance:manage_settings and procurement:manage_bills | The agreement freezes both parties, the supplier tax identifier, the rules relied on and the retained signed document |
GET | /api/v1/finance/self-billing/agreements/:id | purchase read | |
POST | /api/v1/finance/self-billing/agreements/:id/sign, /activate, /withdraw | both capabilities | Signing is refused while the jurisdiction is unresolved |
GET | /api/v1/finance/self-billing/enablement | purchase read | Answers which of the five conditions is unmet, not just a boolean |
PATCH | /api/v1/finance/self-billing/suppliers/:id | both capabilities | enabled and the supplier’s agreed rate. Off by default |
GET, POST | /api/v1/finance/rcti | purchase read, procurement:manage_bills | |
GET | /api/v1/finance/rcti/:id, /preview, /pdf | purchase read, and the cost lens on the preview and the PDF | |
POST | /api/v1/finance/rcti/:id/approve | procurement:manage_bills | The supplier bill’s own approval is the authority |
DELETE | /api/v1/finance/rcti/:id | procurement:manage_bills | Discards a draft and frees the work it claimed. An issued document cannot be discarded |
POST | /api/v1/finance/rcti/:id/issue | both capabilities | Re-derives the agreement’s validity under the row lock, so a withdrawal racing an approval refuses. Idempotent on issueIdempotencyKey; a key held by another document answers 409 IDEMPOTENCY_KEY_IN_USE |
POST | /api/v1/finance/rcti/:id/void, /correct | both capabilities | An issued document is immutable. A correction voids it and drafts a new one referencing it |
POST | /api/v1/finance/self-billing/runs | procurement:manage_bills | The period draft, on demand. Nothing is scheduled |
Refusal codes: SELF_BILLING_NOT_PERMITTED, SELF_BILLING_RULE_MISSING,
SELF_BILLING_RULES_UNVERIFIED, RCTI_NOT_ISSUED, plus PERIOD_CLOSED.
Signed agreement files and supplier delivery
Section titled “Signed agreement files and supplier delivery”GET /api/v1/finance/self-billing/suppliers/:id returns selfBilling with
enabled, currency, unit and selfBillingRateAmount (only with cost access).
PATCH takes enabled, optional rateAmount in minor units, rateCurrency
and unit (hour, day, item). A changed rate applies to new drafts.
Agreement reads include evidenceRetained. POST to an agreement’s /sign
requires agreementAssetId, optional agreementAssetVersion, and
agreementSha256. It verifies the actual accessible file version before
recording the signature. Remote links, unavailable versions and mismatched
checksums refuse. An identical signature retry preserves the original signature.
POST /api/v1/finance/self-billing/agreements/:id/file-evidence takes the same
file fields and retains missing proof for an already signed agreement. Its
checksum must match the original signature; parties, dates and rules remain.
Both file actions need settings and bill-management capabilities.
GET .../:id/document downloads the retained bytes and additionally needs
cost access. Files are limited to 25 MB. Missing proof returns 404 on download
and blocks further self-billing with AGREEMENT_MISSING_DOCUMENT at enablement.
POST /api/v1/finance/rcti/:id/send takes required UUID idempotencyKey,
recipients (1 to 20 email addresses), optional cc (up to 20), replyTo and
message (up to 5000 characters). It requires both management capabilities
and cost access and returns 202 with sendJob. The document must be issued.
The issued PDF is attached; pending rendering is retried. A matching request
identity returns the same job; changed request details return 409.
GET .../:id/sends returns sendJobs to purchasing readers with cost access.
Delivery rechecks current authority, issued state and document identity and preserves dataset suppression. Provider acceptance is distinct from delivery. An interrupted send with an unknown outcome is marked failed for review before an explicit new request, without automatically sending a duplicate.
Runs create further documents after each 200 lines. A failed page reports
remaining sources in truncated; rerunning resumes without duplicating earlier
drafts. skipped may include count for unpriceable sources. A standalone void
releases time for a later run while preserving its original claim history.
A correction’s void, replacement draft and source claims succeed together.
voidedOn dates both the void and replacement bill; service dates stay as
originally recorded. Source-backed lines must keep their order, budget links
and quantities; price and description may change. Ambiguous mappings return
409 CORRECTION_SOURCE_MAPPING. Missing required line fields return 400.
The new draft still needs approval and issue. Existing generated-token and MCP
call_public_api validation, permissions and organisation binding apply.
The supplier area
Section titled “The supplier area”The supplier surface is not on /api/v1 and carries no Runnit session. A
supplier presents an independently issued credential, scoped to the
organisation, the supplier, the permitted billing entities and the permitted
actions, and exchanges it for a short session. A hosted document token cannot
reach any of it, and a supplier session cannot open a hosted document.
| Method | Path | Action required | Notes |
|---|---|---|---|
POST | /api/public/supplier-portal/sessions | a valid credential | Unknown, wrong, revoked, expired, locked and wrong-organisation answer identically. Five failures lock the credential for 15 minutes |
DELETE | /api/public/supplier-portal/sessions | a live session | |
POST | /api/public/supplier-portal/credentials/recover | none | { email }; uniform 202, queues one-time links to recorded active grants |
POST | /api/public/supplier-portal/credentials/recover/complete | one-time proof | { token }; returns the same session shape as sign-in, or uniform 404 |
GET | /api/public/supplier-portal/purchase-orders, /:id, /:id/pdf | view_purchase_orders | Fixed field lists. 409 RENDER_PENDING while a document is being prepared |
GET | /api/public/supplier-portal/bills | view_bills | Draft bills are not listed, and internal states are collapsed |
GET, POST | /api/public/supplier-portal/uploads | upload_invoice, or view_bills to read | PDF, PNG, JPEG or WEBP, at most 15 MB. The supplier, entity and order are resolved on the server. Produces a proposal only |
GET | /api/public/supplier-portal/remittances | view_remittance | |
GET | /api/public/hosted/purchase-orders/:token, /pdf | a live token | The hosted purchase order page, on the same token contract as the hosted invoice. An invoice token answers 404 here |
POST | /api/public/hosted/purchase-orders/:token/recover | none | Queues the current link and verified PDF to the recorded supplier contact, once per 15-minute cooldown. Revoked links cannot recover |
The action catalogue is four entries: view_purchase_orders, view_bills,
view_remittance and upload_invoice. There is no approve, pay, receive or
bank action. Both public routers send Cache-Control: no-store,
Referrer-Policy: no-referrer and X-Robots-Tag: noindex, nofollow, and both
are rate limited per address and per credential or token.
Managing the area is an ordinary workspace surface:
| Method | Path | Capability | Notes |
|---|---|---|---|
GET, POST | /api/v1/finance/suppliers/:supplierId/portal-credentials | read takes a purchase read capability or finance:manage_settings; issuing needs procurement:manage_suppliers or finance:manage_settings | The secret is returned once, on issue and on rotation, and never again |
POST | /api/v1/finance/supplier-portal/credentials/:id/rotate, /revoke | procurement:manage_suppliers or finance:manage_settings | Rotation issues a new credential with the same scope and kills the old one and its sessions in one transaction |
GET | /api/v1/finance/supplier-portal/credentials/:id/sessions | purchase read or finance:manage_settings | |
GET | /api/v1/finance/supplier-portal/uploads, /:id | purchase read or finance:manage_settings | The proposal’s amounts additionally need the cost lens |
POST | /api/v1/finance/supplier-portal/uploads/:id/accept | procurement:manage_bills | Creates a draft supplier bill with the upload attached and reviewed invoice lines. Accepting twice returns the same bill |
POST | /api/v1/finance/supplier-portal/uploads/:id/reject | procurement:manage_bills | code, reason; the internal reason is not emailed |
POST | /api/v1/finance/purchase-orders/:id/hosted-link | procurement:manage_pos and cost access for the recorded project/client | Freezes the issued version’s document if needed and mints a link bound to it |
Creating a purchase order link requires permission to see that order’s costs,
using its recorded project and client. A cost refusal returns 403; an unknown
or other-organisation order returns 404. Neither creates a document or link.
An authorised request for an unissued draft returns 409 without preparing a
document. Existing supplier links and their recovery rules remain valid when a
staff member’s cost access changes. The same checks apply through
call_public_api with its normal write confirmation.
Acceptance supports an optional review object containing currency, billDate,
supplierInvoiceNumber, nullable dueDate and 1–200 lines. Each line needs
description, positive decimal quantity, amount as a gross whole-line minor
unit string, and explicit taxRateId (a current purchase rate for the selected
billing entity, or null for no tax). Quantity does not multiply amount.
Optional billingEntityId and projectId apply to the draft. The extracted
proposal stays unchanged. Missing fields or unresolved tax return
SUPPLIER_PROPOSAL_REVIEW_REQUIRED; source line/header total mismatch returns
SUPPLIER_PROPOSAL_TOTAL_MISMATCH. Complete untaxed proposals can still be
accepted without review. These contracts also apply through call_public_api.
Recovery is limited to one request per credential every 15 minutes and at most 20 current matching grants per address. A proof lasts up to 30 minutes, is usable once and cannot extend the grant. Each grant retains its own organisation, supplier, entity and action scope. Decision emails omit internal rejection reasons; acceptance only means a draft was created. Provider acceptance does not confirm delivery. Recovery remains outside generated-token REST and MCP access.
Bank statements and matching
Section titled “Bank statements and matching”Reading needs both finance:view_prices and finance:view_cost, because
one statement mixes receivable and payable money and no field name can be
both. Mappings and imports need finance:manage_settings.
| Method | Path | Capability | Notes |
|---|---|---|---|
GET, POST | /api/v1/finance/bank-statements/mappings | read, finance:manage_settings | The column map and the parsing conventions. Versioned, never edited in place |
GET | /api/v1/finance/bank-statements/mappings/:id | read | |
POST | /api/v1/finance/bank-statements/mappings/:id/archive | finance:manage_settings | |
GET, POST | /api/v1/finance/bank-statements/imports | read, finance:manage_settings | The file arrives base64 in the JSON body, capped at 8MB, CSV only, with an optional sourceAssetId. An exact reimport is refused; an overlapping export imports only the new rows and marks the rest duplicate |
GET | /api/v1/finance/bank-statements/imports/:id | read | Carries overlapCount, parseErrors and the opening and closing balances |
POST | /api/v1/finance/bank-statements/imports/:id/revert | finance:manage_settings | Refused once any line has settled money |
POST | /api/v1/finance/bank-statements/imports/:id/rescan | any settlement capability | |
GET | /api/v1/finance/bank-statements/lines, /:id, /:id/candidate | read | Candidates carry confidenceBp, rank, tiedWith, their reasons and a candidateHash. Every candidate sharing the top score is tied and nothing is suggested |
POST | /api/v1/finance/bank-statements/lines/:id/ignore, /rescan | any settlement capability | |
POST | /api/v1/finance/bank-statements/lines/:id/confirm | any settlement capability at the route, and the action’s own capability in the service | Five distinct actions: link_existing_payment, record_receivable_settlement, record_payable_settlement, confirm_reimbursement_run, mark_card_spend_paid |
GET | /api/v1/finance/bank-statements/confirmations/:id | read | |
POST | /api/v1/finance/bank-statements/confirmations/:id/reverse | as confirming | reason required. Refused twice on the same confirmation |
GET | /api/v1/finance/bank-statements/matches/:id | read |
Each action carries its own capability inside the service:
finance:record_payments for linking and a receivable settlement,
procurement:manage_bills for a payable settlement, and expense:manage for
a reimbursement run and card spend. There is no confidence score at which
anything is recorded; confirmation is always an explicit call, and every
check is re-run under the line’s lock.
Refusal codes: STATEMENT_FILE_ALREADY_IMPORTED, STATEMENT_AMBIGUOUS,
STATEMENT_FORMAT, STATEMENT_IMPORT_CONFIRMED, STATEMENT_LINE_DUPLICATE,
STATEMENT_LINE_IGNORED, STATEMENT_ALREADY_CONFIRMED,
STATEMENT_ACTION_MISMATCH, STATEMENT_TARGET_MISMATCH,
STATEMENT_CANDIDATE_REJECTED, STATEMENT_CANDIDATE_STALE,
STATEMENT_TARGET_BALANCE_CHANGED, STATEMENT_EXISTING_PAYMENT,
STATEMENT_EXISTING_PAYMENT_AMBIGUOUS, STATEMENT_SPLIT_TOO_LARGE,
STATEMENT_ALREADY_REVERSED, plus PERIOD_CLOSED.
Jurisdiction rules and automated tax
Section titled “Jurisdiction rules and automated tax”Reads take finance:manage_settings or finance:view_invoices.
| Method | Path | Capability | Notes |
|---|---|---|---|
GET | /api/v1/finance/jurisdictions/rules | read | The rules in force for an entity on a date, with their verification state. Takes billingEntityId, onDate and consumer |
POST | /api/v1/finance/jurisdictions/rules/:ruleId/confirm | finance:manage_settings | Binds the confirmation to the rule row, its effective date and a hash of its value, so a superseded rule is not still confirmed |
GET | /api/v1/finance/jurisdictions/verification | read | What each consumer requires and what it has: unverified, missing, superseded or future_effective |
POST | /api/v1/finance/jurisdictions/reconcile | finance:manage_settings | Re-checks confirmations and revalidates drafts. Issued documents keep the rules they froze |
GET | /api/v1/finance/jurisdictions/coverage | read | The coverage matrix as data, including what is deferred and why |
GET, POST | /api/v1/finance/jurisdictions/mileage-rates, /per-diem-rates | read, finance:manage_settings | Effective dated, with a verified flag. An unverified rate is never used silently |
PATCH | /api/v1/finance/jurisdictions/mileage-rates/:id, /per-diem-rates/:id | finance:manage_settings | An edit closes the open row rather than rewriting history. A second row starting the same day answers 409 |
GET, PUT | /api/v1/finance/billing-entities/:id/tax-calculation-settings | read, finance:manage_settings | off, optional or required. off for every entity |
GET, POST | /api/v1/finance/invoices/:id/tax-calculation | read, finance:draft_invoices | Bound to the draft’s content hash and computed outside any transaction. Any edit supersedes it |
Issue consumes the calculation inside its own transaction and makes no network
call there. In required mode it refuses a missing, expired, superseded,
failed or disagreeing answer; in optional mode it falls back to the
configured rates and the document records which basis it used. Codes:
TAX_CALCULATION_MISSING, _EXPIRED, _SUPERSEDED, _FAILED,
_MISMATCHED, and UNVERIFIED_RULES when a rule the document relies on has
not been confirmed.
The hosted document page
Section titled “The hosted document page”Every issued document has a link the client can open without signing in. It is served outside the API and needs no key:
| Method | Path | Notes |
|---|---|---|
GET | /api/public/hosted/documents/:token | The public view: kind, title, number, status, dates, currency, total, amount due, the parties, whether the PDF is ready, the rendered HTML and the client’s own queries |
GET | /api/public/hosted/documents/:token/pdf | The frozen bytes, or 409 while the document is being prepared |
POST | /api/public/hosted/documents/:token/recover | Always answers the same way. A fresh link goes only to the billing contact Runnit resolves, once per cooldown |
POST | /api/public/hosted/documents/:token/queries | The client asks a question, which lands on the invoice’s timeline |
Tokens are random, stored hashed, bound to one document version, and can be
rotated or revoked. An invalid, expired or revoked token gets the same 404 as
one that never existed. The page never carries cost, margin or internal notes,
and it is not indexed.
The hosted quote page
Section titled “The hosted quote page”The html field contains a complete styled document suitable for a sandboxed
iframe. After acceptance, it and pdfReady refer to the accepted document, with
the agreed selection and signer evidence. Until the accepted document has been created,
html is empty, pdfReady is false and PDF download returns the pending response.
Once its content is saved, HTML is available even if its PDF is still rendering.
Staff quote previews return 409 until the accepted document has been created.
The earlier unsigned PDF is never substituted. The accepted state and summary
remain available. The same behaviour applies in the client portal.
Quotes and client-signed change orders use the same public lane, with the same
headers and the same uniform 404:
| Method | Path | Notes |
|---|---|---|
GET | /api/public/hosted/quotes/:token | The public projection of the version that was sent. ?selection=id,id recalculates the total for a selection the client is trying, and records nothing |
GET | /api/public/hosted/quotes/:token/pdf | The quote PDF with internal notes excluded, or 409 RENDER_PENDING while it is being prepared |
POST | /api/public/hosted/quotes/:token/code | email and action (accept or decline). Answers 202 with required, the same way whether a code was sent or none was needed |
POST | /api/public/hosted/quotes/:token/accept | signerName and signerEmail are required, then selection, signatureText and code. 201 the first time and 200 with the same acceptance hash after that: acceptance is idempotent on the token and never overwrites the original signer evidence. 400 on an illegal selection, 403 on a bad code, and 409 after a decline, a withdrawal, a revision or an expiry |
POST | /api/public/hosted/quotes/:token/decline | reason, lostReasonId, declinedByName, declinedByEmail, code |
POST | /api/public/hosted/quotes/:token/queries | message and from. The question lands on the quote’s timeline |
POST | /api/public/hosted/quotes/:token/recover | Always answers the same way. A fresh link goes only to the contact Runnit resolves, once per cooldown |
The same accept and decline paths serve a change order: the token’s subject
decides which, and a change order’s answer carries applied. The page never
carries cost, margin or a hidden contingency line.
Stability contracts
Section titled “Stability contracts”PO revision submission can return pending_approval while preserving the
issued order. The matching approval applies the stored proposal atomically and
is consumed once. An original issue approval cannot authorise a revision.
Identical resubmission retains the pending request; changed proposal content,
including quantity units, returns 409 APPROVAL_STALE. Resolve that request
before submitting different changes. A period override reason does not change
the approved content. Automatic application requires the original requester to
remain an active member with procurement:manage_pos.
PO currency/order-date edits and supplier bill currency/bill-date/entry-mode
edits need replacement lines; otherwise they return 400. PO budget lines
must use the PO currency. Statutory mileage and per diem claims must use the
rate’s currency.
Matching rejects unissued orders with MATCH_PO_UNISSUED. Receipt shortages
block approval with MATCH_RECEIPTS_REQUIRED. Price tolerance includes prior
bills; inherited fixed markup is shared by partial bills.
An issued invoice replay stays idempotent after its period closes. A concurrent
issue-date edit can return 409 INVOICE_DATE_CHANGED; reload and retry.
Computed invoice lines may include taxAllocation with netAmount, individual
tax components and an optional sourceLineId. Ordinary inclusive credits also
return entryBasis: "gross"; their entry amount equals net plus allocated tax.
An omitted basis means net, including retention-release credits. Ordinary line
inputs cannot provide this calculated allocation. Price-restricted readers do
not receive it, through either REST or MCP.
GET /api/v1/finance/ledger/connections includes providers.quickbooks with
configured and enabled, plus unattemptedLedgers with provider keys and
reasons. These are configuration facts, not production evidence.
Recognition honours the requested organisation-local cutoff, capped at period
end and today. Older targets cannot replace later covered targets. Ratios
calculate internal denominators without exposing unrequested cells. Foreign
sold, pipeline and retainer overage without reporting valuations are unvalued.
Journal rebuilds reuse exported accounting identities; corrections require
compensation. Forecast recognised history is bounded by asOf.
The same validation, permissions and side effects apply through MCP’s
call_public_api. Hosted query routes validate document tokens before message
content and return the same 404 for invalid links.
Finance workflow tools
Section titled “Finance workflow tools”Use get_finance_guide for the Finance User Guide and API reference supplied
with the current release. Omit page for the index; then pass a page id and
follow nextOffset until the page is complete. get_finance_operations lists
areas, operation ids, path parameters and validated fields. Pass area for an
operation list, or operation for its contract.
read_finance_operation accepts reviewed GET operations.
perform_finance_action accepts reviewed mutations with confirm:true after
the user authorises the specific effect. Both accept operation, optional
parameters and query; actions also accept a JSON body or multipart.
Responses preserve HTTP status and the route’s data or error. A confirmation
preview reports confirmationRequired:true and changes nothing.
For example, after reading and reviewing an invoice:
{ "operation": "POST /finance/invoices/:id/issue", "parameters": { "id": "<invoice-id>" }, "body": {}, "confirm": true}These tools use your live route permissions, financial visibility and approval
rules. They are available to interactive workspace chat and unscoped generated
credentials. Component, automation and explicitly scoped sessions keep their
narrower tools. Bank details/payment files, signatures, statutory attestations,
provider connections and deferred provider execution have explicit exclusions
in the catalogue. call_public_api retains its existing external-client
contract. See Finance with Ru for user guidance.
Do not send actor or organizationId when creating a quote or retainer.
These fields come from your authenticated credential; supplying them in the
request body returns 400.
Retainer hour quantities are returned as decimal strings with four places.
rolloverCapHours accepts nonnegative hours with up to four decimal places;
excess precision, negative values and overflow return 400 on create or update.
Retainer period reads include manualAdjustments. Add and reverse routes return
{ adjustment }, with appliedQuantity (signed hours) and budgetLineId.
For hours adjustments, send a positive quantity and optionally the period’s
budgetLineId. For amount-only records, send a positive minor-unit amount.
All adjustments require a reason.
Reversal returns the existing reversal on replay. A 409 with
ADJUSTMENT_EFFECT_UNKNOWN means legacy history lacks the actual effect;
ADJUSTMENT_ALLOWANCE_CONFLICT means later changes prevent exact restoration.
Review the period before recording a separate correction. Closed accounting
periods and closed consumption windows block new changes.
WIP metrics and reporting totals use chronological applied adjustments. Suppressed write-offs do not reduce later recognition. The applied proportion uses the recorded reporting value; missing values are excluded and counted in coverage. Existing response fields and access requirements are unchanged.
Self-billing eligibility and RCTI issue validate confirmations against the current statutory rule version and value. Matching an old rule name alone does not confirm changed wording. Existing unverified-rule refusals apply.
Revenue attribution by month
Section titled “Revenue attribution by month”Recognised revenue grouped by person or role uses approved billable values recorded for the same economic month, including when month is not selected as a grouping. Shares reconcile exactly to the reporting-currency total. Missing or incompatible rate evidence leaves that line and month unallocated; later-month work does not change earlier-month shares. Existing metric access and filters apply before allocation.
WIP write-off reversals restore the source’s applied amount, including when the requested write-off exceeded available WIP. Native balances, ageing and reporting metrics share this behaviour; reversal does not create new revenue.
Retainer hour adjustments and carry change the delivery allowance while
leaving priced quantity and the agreed fee unchanged. Period consumption
returns the resulting allowanceHours. Budget line reads expose the signed,
read-only allowanceAdjustment separately from quantity. Pricing changes
that conflict with an existing allowance adjustment return 409 with
RETAINER_ALLOWANCE_CONFLICT, including at change-order application.
Retainer detail includes carryLots, with originPeriodId,
generatedFromPeriodId, parentRolloverId, originPeriodStart,
expiresAfterPeriodStart, allocated and remaining quantities, and expired.
Forwarded unused hours preserve the original expiry and share the per-line cap
with new carry. Expired lots remain in history but are excluded from available
and end-settlement totals. Money fields follow the price lens.
The sold metric allocates the budget’s recorded reporting value exactly
across its lines before applying line filters. Source currency and amount must
match the applied budget version. Missing or inconsistent valuations remain
unvalued; later exchange-rate changes do not alter this read.
Recognition during source actions
Section titled “Recognition during source actions”Time approval, unapproval, catch-up, invoice issue, credit and void actions
reconcile cumulative recognition within their transaction, including the sold
limit, line cap and posted write-ups. Recognition history includes any required
line adjustment. Repeated partial credits reduce the remaining recognised
share; voided invoices remain excluded from later runs. An older run preserves
later source postings. If budget lines change during run preparation, the run
returns 409 RECOGNITION_RUN; retry with the current budget. The same behaviour
and permissions apply through MCP and supported Ru actions.
Historical catch-up cannot post into a closed source month. Reopening requires
your normal authority and approval. For a soft-closed month, provide a reason;
the change is recorded with that reason. The API accepts an optional reason
string of up to 2,000 characters. Ru can supply your reason for an authorised
catch-up.
Forecast schedule residuals
Section titled “Forecast schedule residuals”The forecast retains the positive unbilled net amount of each schedule item
after issued invoice lines and issued reissue credits. Write-off credits do
not restore an obligation; voiding an invoice does. Percentage schedules exclude
time and materials. Conflicting schedule links or currencies return HTTP 409
with code SCHEDULE_SOURCE_ALLOCATION and invoiceId, invoiceLineId and
scheduleItemId details. Resolve the identified allocation before retrying.
Invoice-based recognition and cumulative credit reversals count issued documents only. Approval alone does not change recognised revenue. These rules apply equally through the public API and MCP.
Composer candidates include previously invoiced schedule items that still have
an unbilled amount. remainingAmount is a money object; it and amount are
omitted without price visibility. Use the existing schedule_item method to
draft the remaining original line allocations. An edited line above its
remaining allocation returns HTTP 409 SCHEDULE_AMOUNT_EXCEEDED at issue.
A schedule item remains exclusively reserved while its draft is live.
Budget entry pricing and source invoices (9 September 2026)
Section titled “Budget entry pricing and source invoices (9 September 2026)”New quote conversions return budget.pricingSettings with priceEntryMode,
taxRoundingMode and roundingMode. Budget lines retain entered unitPrice
and expose price-lens-protected netUnitPrice for net hourly valuation. Sold
and schedule amounts remain net. pricingTaxIdentity is server-maintained;
clients choose taxRateId, not a tax snapshot. Budget scenario snapshots
include their frozen pricingBasis; quantities retain four decimal places.
Invoice composition from finance sources defaults to exclusive entry. Explicit
inclusive source composition, a change to inclusive on a sourced draft, or
adding sources to an inclusive draft returns 409 SOURCE_PRICE_ENTRY_MODE.
Older sourced inclusive invoice drafts have a blocking validation issue until
corrected. Standalone manual inclusive invoices remain supported. The same
validation applies through /api/v1, call_public_api, draft_invoice and
Ru’s reviewed Finance operations. Existing permissions and confirmation apply.
Quote versions expose read-only pricingTaxRates, keyed by rate ID, with
id, name and components (name, rateBp). These terms are included in the
approval and remain unchanged after sending. A settings edit cannot change the
sent quote’s calculated totals through REST or MCP.
On the hosted quote view, a legacy option without recorded tax treatment has a
null amount. Show it as unavailable and prevent selection. A request to price
or accept such a selection returns 409 QUOTE_TAX_SNAPSHOT_MISSING; the sender
must prepare a revised quote with a recorded treatment.
Receipt recoveries
Section titled “Receipt recoveries”| Method | Path | Capability | Result |
|---|---|---|---|
GET | /api/v1/finance/payment-recoveries | finance:view_invoices | { recoveries }; optional client, billing entity, asOf and openOnly filters |
GET | /api/v1/finance/payment-recoveries/:id | finance:view_invoices | { recovery, receipts, payments } |
POST | /api/v1/finance/payment-recoveries/:id/receipts | finance:record_payments | Records recovered cash; returns 201 |
POST | /api/v1/finance/payment-recoveries/:id/receipts/:receiptId/reverse | finance:record_payments | Corrects one receipt; returns 200 |
A receipt requires positive minor-unit amount, calendar receivedOn and an
idempotencyKey of 1 to 120 characters. Optional fields are method, reference
and reason. Use the same key to retry the same receipt. Currency, client and
billing entity come from the recovery; overriding them is refused. A correction
requires calendar reversedOn and a nonblank reason.
Amounts cannot exceed the remaining balance. Dates cannot precede the latest
recovery activity and must pass period rules. State conflicts return 409,
including RECOVERY_EXCEEDED, RECOVERY_DATE and IDEMPOTENCY_CONFLICT.
Cross-organisation IDs return 404; money is omitted without the prices lens.
These operations are also available through Ru and the MCP public API path with
the same validation, authority and side effects.
Receivables reads include separate recoveries; invoice payment reads include
receiptReversals. Statements include recovery and payment_reversal entries.
POST /api/v1/finance/statements also accepts optional ISO currency, alongside
billingEntityId, so a statement can use the recovery’s recorded currency.
Forecast source filters
Section titled “Forecast source filters”GET /api/v1/finance/forecast and get_forecast accept billingEntityId,
clientOrganizationId, projectId and budgetId; supplied filters intersect.
Malformed ids return 400 through REST and the route bridge. Valid ids with no
matching source return empty result arrays. Invoice and supplier-bill residuals
use their attributed lines; reimbursement runs retain expense-split attribution.
General operating costs are excluded from client/project/budget views. Budget
views exclude pipeline quotes and future retainer fees without a budget.
Recovery balances have client/entity attribution only. Capacity bookings use the
same filters; their capacity denominator is the related team’s working hours
less closures and leave, before bookings and external meetings.
Lateness output includes only clients with matching expected receipts.
Weekly API totals cover the whole returned series. To match the browser’s cash
heading, sum cashWeekly.weeks.slice(0, 13) net amounts in the view currency.
Access checks and financial lenses apply identically through the public API,
call_public_api, Ru’s operation reader and the named forecast tool.
Recorded pipeline and overage reporting
Section titled “Recorded pipeline and overage reporting”pipeline_weighted and the Sales pipeline use recorded reporting values. Defaults
are 10%, 15%, 30% and 50% for draft, pending approval, sent and viewed. An explicit
probability, including zero, takes precedence. Weighting rounds half up per quote.
Overage records reporting valuation when a consumption window closes and is dated
to its last included day. Unchanged recalculation or re-close preserves the value;
missing historical foreign evidence remains in coverage.
Metric filters reject malformed UUIDs with 400. Project/client/entity filters intersect caller scope. Budget/service/person filters exclude the unattributed whole-quote price. Overage supports budget/project/client/entity attribution; service/person filters exclude the whole-period amount. Draft quote creation, editing, copying and revision pricing commit together. Copied schedule allocations point to the new line IDs. Existing price lenses and generated-token/MCP authority remain unchanged.
Approved time valuation and realisation
Section titled “Approved time valuation and realisation”The metrics catalogue includes standard_billable_value: approved billable time
at its recorded standard rate in the reporting currency. realisation divides
recognised revenue by that value and returns basis points. Both require the
prices lens. An unvalued source makes the ratio null and increments coverage;
a companion denominator requested only to compute the ratio is omitted from the
result.
Approved used and delivery time cost retain their recorded currency evidence.
Unapproval clears it; reapproval records new values. Transfer keeps the price
unless reprice: true is explicit. A different destination currency without
repricing returns 409 TIME_TRANSFER_CURRENCY. Repricing retains recorded cost
in matching currencies, and marking non-billable clears billable value while
keeping cost. Existing routes expose the same behaviour through MCP’s
call_public_api.
Purchase totals and split valuation
Section titled “Purchase totals and split valuation”Existing expense routes derive header billability and sell totals from the split lines. Fixed markup is allocated only to billable weights; non-billable lines carry zero sell value. Budget used includes approved billable expense and bill lines, and budget cost includes approved purchase cost. Recorded currencies and missing-value coverage apply to both. Internal valuation fields are not added to the public projection.
Supplier-bill voids produce dated negative used/cost facts and preserve earlier report months. Project filters include source-project time funded by a client retainer. REST and MCP calls share these behaviours and their existing permission and organisation checks.
Receipt cost and self-billing sources
Section titled “Receipt cost and self-billing sources”Cost metrics include unbilled receipt accruals, using the same dated values as
budget caches. Receipt reversals cannot consume quantity already matched to an
active supplier bill. Partial movements retain their recorded currency shares,
and the final movement takes the exact remainder. Bill voids use the selected
voidedOn economic date. Self-billing relieves time cost on the bill date and
restores it on the void date, preserving the original work month.
POST /finance/rcti returns 400 for caller-provided consumptions,
correctsRctiId or correctionReason. Sources are reserved by the worker and
replacement identity is set by the correction action. Unapproving or moving
reserved time returns 409 TIME_ENTRY_SELF_BILLED; editing or voiding the underlying
self-billed document through the ordinary bill route returns 409 BILL_SELF_BILLED.
Draft creation and document voiding commit with their source reservations.
Asynchronous supplier-bill approval updates the document status in the same
transaction. REST and MCP share these validations, permissions and effects.
Available hours
Section titled “Available hours”The metric catalogue includes capacity_hours, grouped by month, person
or role, with hours scaled by 10,000. billable_utilisation uses approved
billable hours divided by these available hours, rounded half-up to basis
points; zero available hours returns null. Send explicit from and to dates
for comparison. Without dates, the range starts in the first approved work
month (or the selected end/current month without work) and ends at the later
of today and the last approved work day. Reversed ranges and capacity windows
over a century return 400.
Forecast forecastUtilisation includes userName and unbooked months, using
only days after asOf through the exact horizon date. Subject filters select
the related team once per person; this is not additive project capacity.
Capacity uses the current default organisation role, while time uses its
recorded role. Existing access and financial lenses apply equally through
REST and MCP, including get_forecast and call_public_api.
Change-order actions return CHANGE_ORDER_SCOPE_CHANGED (409) if the budget’s
project or billing entity changes during the request. The request rolls back;
refresh the budget and retry. Preparing and withdrawing drafts remains possible
in closed accounting periods, but application retains its period-closed refusal.
The same rules apply through generated API keys and MCP.
Bank confirmations and reversals commit their settlement and match together.
A split client settlement returns one payment with multiple allocations. An
identical replay returns the existing confirmation; changing the target, date,
method, candidate hash or splits returns 409. A reversed line can receive a
fresh settlement. Both supplied opening and closing balances must reconcile
against all rows, including overlaps; unread rows or a difference return
STATEMENT_BALANCE_MISMATCH without importing.
Reimbursement detail includes run.paidOn and settlements[]. Each settlement
has id, kind, economicOn, signed amount, reversesSettlementId, reference
and reason. Confirmation and correction obey the same permissions and period
rules through generated-token REST and call_public_api.
Retainer close and invoice operations keep hourly allowances separate from
monetary carry credits. The invoice source kind retainer_carry identifies
the signed credit or correction. Existing permissions and price visibility
apply through REST and MCP. See Retainers.
Pricing adoption is available through the shared Finance routes. Read
GET /finance/budgets/:id/quote-pricing-review, then send its reviewHash and a
reason to POST /finance/budgets/:id/adopt-quote-pricing. Active budgets open a
correction for normal approval. For templates, read
GET /finance/budget-templates/:id/pricing-review?sourceBudgetId=..., then send
sourceBudgetId, reviewHash and reason to
POST /finance/budget-templates/:id/adopt-pricing. Direct REST paths use the
/api/v1 prefix. Generated-token MCP callers use the same paths through
call_public_api. Management scope, price visibility and evidence validation
apply to both. See Budgets for the review workflow.
The accountant export endpoint
GET /api/v1/finance/periods/:id/export-pack accepts format=csv|pdf. CSV is
the default and returns the files envelope. PDF returns application/pdf, an
attachment filename and X-Close-Generation. Both formats require export
permission and unrestricted organisation-wide price and cost access. The same
access requirement applies to the complete JSON close report and its history.
Any project or client restriction on prices or costs returns 403, including a
restriction outside the selected period. No partial report is returned. These
checks apply before creating a report or rendering a download, including older
report generations. They also apply through call_public_api. Unknown formats return
400 and insufficient access returns 403. The PDF uses the same recorded
generation as the CSV pack.
Review records behind a period checklist
Section titled “Review records behind a period checklist”GET /api/v1/finance/periods/:id/review/:key returns rows, total, limit,
offset and scope. Rows contain id, label, status, date and link.
Use limit from 1 to 200 (default 50) and a non-negative offset. The total
remains available when a requested page is empty. Results contain no amounts.
Use the checklist item’s key, except recognition_run, whose link opens
Revenue with the entity and month selected. The scope is period, or
billing_entity for ready schedules, received purchase orders and old drafts.
The latter queues include outstanding records across all dates for that entity.
You need a period-read capability and an organisation-wide capability for the source: time approval for time; expense review, approval or management for expenses; purchasing read for purchase orders; invoice read for invoices; payment or receivables management for payments; budget, invoice drafting, issuing or recognition for uninvoiced work; budget or recognition for WIP; time approval or recognition for missing valuations. A scoped grant or period closing alone does not grant source access. Invalid keys or paging return 400; inaccessible source queues return 403; foreign periods return 404.
GET /api/v1/time/approvals accepts segmentId to read one exact segment within
your current status filters and approval access. These reads are also available
through call_public_api with the same permissions and validation.
Internal chargebacks
Section titled “Internal chargebacks”Supplier create/update accepts nullable UUIDs internalBudgetId and
internalCreditBudgetLineId with isInternal: true. The credit line must be
active, non-billable and on the selected active internal budget. Supplier
management permissions apply. New bill drafts freeze the destination; saving a
legacy draft without one adopts the current supplier default.
Bill approval requires complete receiving allocations, no purchase tax and
writable receiving and providing periods. Missing or invalid setup returns 409
INTERNAL_CHARGE_SETUP_REQUIRED; foreign credit lines return 404. Existing
allocation validation may return PURCHASE_INVALID first.
Bill detail includes internalCharges with the cost lens. Each entry has id,
supplierBillLineId, providingBudgetId, providingBudgetName,
providingBudgetLineId, date, description, reversesChargeId, and signed
internalCreditAmount and internalReportingCreditAmount money values. A null
value means missing recorded currency evidence. Cost access is sufficient to
read these values without price access. No cost access omits the collection.
Approval records both sides together; retries do not duplicate credits. Voids
record exact inverses on their correction date. Internal bills cannot be paid,
even after the supplier’s classification changes. Credit history also prevents
archiving or changing its line to billable (INTERNAL_CHARGE_HISTORY, 409).
Generated-token REST and call_public_api share these contracts. The named
draft_supplier_bill tool creates a draft and freezes the same destination.
See Purchasing.
Source values and omitted scenario estimates
Section titled “Source values and omitted scenario estimates”Money metric cells may include documentValues, with separate
currency, amount, decimal and unvalued entries. Amounts are minor-unit
strings. Reporting-currency fields retain recorded values. Do not sum entries
across currencies. Used and cost source values use the recorded budget currency.
Budget lineage supports sold, used, cost, invoiced, committed, forecast,
remaining, margin and hours in figure, with optional UUID lineId.
BAS sales and purchase sides follow price/cost permissions; net GST needs both.
These contracts also apply through MCP’s call_public_api.
When replacing scenario lines, send the existing scenario-line id to retain an
omitted unitPrice or estimatedCost. Send null cost to clear its estimate.
A stale or foreign line ID is refused. New lines can omit id.
Billing-entity logo
Section titled “Billing-entity logo”Billing-entity create/update accepts nullable UUID logoAssetId. Upload the file
using the Assets API, then save its ID on the entity.
The asset must belong to the organisation, have organisation or public visibility,
and contain an active PNG/JPEG image up to 500 KB. Invalid image input is 400;
unavailable or foreign assets are 404. Finance settings access is required.
New invoice, quote and change-order documents retain their own copy of the logo.
Invoice templates, previews and email delivery
Section titled “Invoice templates, previews and email delivery”Read templates with GET /api/v1/finance/document-templates and styles with
GET /api/v1/finance/document-styles. If a list request fails, keep its error
visible and retry the read when appropriate. Treat only a successful empty
response as an empty list. Each request checks current access and organisation
binding, including through call_public_api. Simultaneous first reads share the
same built-in templates and style.
Copy a template with POST /api/v1/finance/document-templates and copyOf. Omitting
styleId creates an independent copy of its style in the same transaction.
An explicit styleId selects an existing style. Select a template on a draft
with PATCH /api/v1/finance/invoices/:id and documentTemplateId. Issued documents
keep their saved template, branding and renderer version.
POST /api/v1/finance/document-templates/:id/preview returns { html } using sample
invoice details, without saving changes. It accepts optional language,
blocks[{ blockType, options }] and style containing font, accentColor,
logoSize, paperSize and density. It requires finance:manage_settings or
finance:manage_integrations. Invalid settings return 400; an unknown or foreign
template returns 404. Template and style reads also allow these capabilities.
Invoice preview and PDF reads still require invoice access and the price lens. PDF downloads return the saved bytes. A document retry also checks that the document belongs to the invoice named in the URL.
Invoice sends with mode: "schedule" require a future scheduledFor; missing
or past dates return 400. Email sends return a queue job, not a delivery receipt.
Inspect sendJobs from GET /api/v1/finance/invoices/:id/documents for the result.
Email includes the PDF and a secure document link; a declined transport does
not mark the invoice sent.
These operations are available through call_public_api with the same
permissions and organisation binding. Its standard confirmation requirement
applies to POST/PATCH calls, including the unsaved sample preview. Hosted document
links use separate document-token authority and do not grant workspace API access.
Reading document previews
Section titled “Reading document previews”New document line tables show saved quantities with up to four decimal places, without trailing zeros. Earlier issued documents keep their saved presentation and PDF bytes. Amounts and endpoint schemas are unchanged across generated-token REST and MCP.
These GET operations return standalone html, title, language and frozen:
| Path | Required access |
|---|---|
/api/v1/finance/quotes/:id/preview | Quote read access and prices; optional UUID versionId selects a version |
/api/v1/finance/purchase-orders/:id/preview | Purchase read access and cost |
/api/v1/finance/statements/:id/preview | Invoice read access and prices for the statement’s client |
/api/v1/finance/rcti/:id/preview | Purchase read access and cost; also retains payload and hash |
Draft previews use current saved values. Sent and issued previews use the retained
document’s contents and layout. Statements preview their saved ledger. Responses
use Cache-Control: no-store. Missing retained document history returns 409;
invalid identifiers return 400, unavailable records 404, and missing access
403. Opening a preview does not number, issue or send anything.
The same operations are available through MCP call_public_api, with paths
relative to /api/v1, method: "GET" and the same access checks. For a selected
quote version, pass query: { "versionId": "<version-id>" }.
Deleting a populated quote section with hidden, subtotal-only or package
behaviour returns 409. Move its lines out, or explicitly choose full visibility
and clear the section package before removing it. This preserves the client’s
agreed visibility through both REST and MCP.
Document-template listing and creation accept change_order and rcti alongside
the other document kinds. A template copy must keep the source kind. These
operations also work through MCP’s call_public_api with the same settings
permissions.
PATCH /finance/billing-entities/:id accepts matchTolerancePercentBp,
matchToleranceAmount and abaOriginator. These are also the settings used by
the Purchasing policy forms. A percentage is an integer from 0 to 10,000; the
cash amount is a non-negative minor-unit string. This operation does not change
a confirmed bank account.
The invoice composer accepts an empty entryIds array alongside selected
expenseIds, purchaseOrderLineIds or supplierBillLineIds. A purchase-only
draft reserves its sources through both REST and MCP; a second draft cannot
reserve the same work. Standalone invoices can specify clientOrganizationId
without budgetIds.
Line selection and day hours
Section titled “Line selection and day hours”Quote and budget line writes accept nullable hoursPerDay: a number greater than
zero and at most 24, with up to four decimal places. New day lines use the
organisation’s daily capacity unless you supply an override. Send null on a day
line to reset it to the current default. Saved day hours remain stable if the
organisation changes its default later.
Changing serviceTypeId adopts the activity’s default billing type, unit and
rate-card role. Explicit fields in the same request take precedence. Without a
linked role, the old rate is cleared to zero. Hour/day changes convert the saved
rate; an explicit unitPrice overrides that calculation. Selecting percentage
billing starts at zero when no percentage is recorded. Enter percentBp and
percentageBase to calculate it. Invalid hours or unavailable activity and role
references return 400 without saving a partial change.
For example, PATCH a line with { "unit": "day" } to convert its rate, then
{ "hoursPerDay": 6 } to use a six-hour day. To set a daily price directly, send
{ "unitPrice": "120000" } for 1,200.00 in a two-decimal currency.
Scenario pricing.hoursPerDay retains a copied line’s day basis when the
scenario is promoted.
These rules also apply to MCP call_public_api. update_quote_draft exposes
serviceTypeId, billingType, unit, hoursPerDay, rateCardRoleId, percentBp
and percentageBase inside line. Sent quotes remain frozen, and active budget
price changes use change orders. Existing permissions and organisation scope
apply. See editing quote lines.
Australian BSB validation
Section titled “Australian BSB validation”For country: "AU", identifiers.bsb must contain six digits, either 123456
or 123-456. Outer whitespace is trimmed. Both bank proposal routes apply this
to billing entities, suppliers and employees. Invalid BSBs return 400 with
code: "BANK_BSB_INVALID" and a six-digit format explanation in error.
Confirmation also checks older pending proposals. A refused confirmation leaves
the current account, default and pending proposal unchanged. Withdraw or reject
the pending change, then submit corrected details. Existing confirmed accounts
and document snapshots are unchanged. Other country formats keep their existing
rules. This is a format check, not verification that a branch or account exists.
The same validation and failure behaviour apply through MCP call_public_api.
Bank proposal decisions
Section titled “Bank proposal decisions”Supplier and employee bank proposals use the same confirm/reject routes as billing
entity accounts. Confirmation returns { change, account }, with masked account
identifiers. The proposer cannot confirm their own change (403). Rejection
accepts an optional reason of up to 500 characters; a proposer can use rejection
to withdraw their pending change. Rejection retains existing accounts.
After an uncertain response, read the owner’s bank accounts and change history
before trying again. A repeated or expired confirmation returns 409. Reads and
decisions require current bank-details permission in the credential organisation.
The same operations and validation are available through MCP call_public_api.
Statutory rule review
Section titled “Statutory rule review”Read GET /api/v1/finance/jurisdictions/rules with billingEntityId and optional
onDate (YYYY-MM-DD). Each row includes the version applicable on that date,
verification state and valueHash. Read access needs finance:manage_settings
or finance:view_invoices.
After reviewing applicability, call
POST /api/v1/finance/jurisdictions/rules/:ruleId/confirm with billingEntityId,
expectedValueHash from the read and an optional note (up to 2000 characters).
Confirmation needs finance:manage_settings. The hash is optional for existing
clients; supply it to protect the reviewed value. A changed value returns409;
a malformed hash returns400. Reload and review again before a new decision.
Foreign billing entities return404 and missing permission returns403.
Identical repeated confirmations return the original record, including concurrent
requests. Confirmation does not issue a document or bypass its other checks.
MCP call_public_api exposes the same operations, fields and permissions. Supply
read parameters in query; mutations require confirm: true.
Manual supplier invoice files
Section titled “Manual supplier invoice files”POST /api/v1/finance/supplier-bills/:id/attachment attaches a PDF, PNG, JPEG or
WebP to an ordinary draft bill. Supply fileName, contentType, canonical
contentBase64 and expectedEvidenceId. Use null for the first file, or the
current attachment’s ID when replacing it. The decoded file must contain between
1 byte and 10 MB and match its declared type. An optional reason of up to 2000
characters permits an otherwise authorised change in a soft-closed period.
Upload requires procurement:manage_bills and cost access for the bill’s project and its current client.
Finance and the workspace entitlement must be enabled. The result includes
attachment: { id, fileName, contentType, byteSize, sha256, uploadedAt } in the
bill detail. Metadata is absent without cost access; file bytes are never included
in ordinary bill responses.
An explicit repeat of the same current bytes returns the same attachment. A stale
expected ID with different bytes returns 409. Replacement is refused after
submission, on self-billed or already asset-linked bills, and in closed periods.
Older versions remain retained until an explicit draft deletion. Generic bill
PATCH fields cannot overwrite a retained manual file. An attachment does not
submit, approve or pay the bill; its content is bound to the later approval.
GET /api/v1/finance/supplier-bills/:id/attachment downloads the current file.
It requires purchasing read permission and scoped cost access, with private,
no-store response headers. Each read uses consistent permission, document and
file information. Completed access or scope changes apply on the next read. Unavailable bills/files return 404,
missing permission 403, malformed input 400, unsupported file contents 415
and oversized files 413.
Both routes are available through MCP call_public_api. POST takes the same JSON
body and confirm: true; GET returns bodyBase64, encoding and byteLength.
After an uncertain upload result, read the bill and check its attachment before
sending another request. See attaching an invoice.
Match saved manual bill lines
Section titled “Match saved manual bill lines”POST /api/v1/finance/supplier-bills/:id/matches changes purchase order links on
an ordinary draft. It needs procurement:manage_bills and cost access to the
bill’s project and current client. Send the current expectedContentHash, optional period reason,
and mappings containing every current bill line exactly once:
{ "expectedContentHash": "<current-content-hash>", "mappings": [{ "billLineId": "<bill-line-id>", "purchaseOrderLineId": "<purchase-order-line-id>", "usePurchaseOrderBudget": true, "expectedPurchaseOrderBudgetLineId": "<reviewed-budget-line-id>" }]}Use null purchaseOrderLineId to remove a link. Omit the two budget fields when
no allocation is being adopted. An unallocated bill line can explicitly adopt
an eligible order allocation in the same currency. The expected budget-line ID
must still match when saved. An existing allocation must match the order line.
Orders must be issued and share supplier, project, billing entity and currency.
Each order line can appear once; a request accepts up to 200 bill lines.
The response contains the saved supplierBill. Quantities, amounts, tax,
currency valuations, invoice attachment and on-charge choices stay unchanged.
Saving neither submits nor approves the bill. Read it again to confirm the saved
links, then GET /:id/match to review receipt and tolerance checks. Preview needs
cost access to every referenced order as well as the bill.
Malformed or incomplete line sets return400. Missing or foreign records return404,
permission denial returns403, and changed bill or allocation returns409
BILL_MATCH_CHANGED. State, period, allocation, order and fixed-markup conflicts
also return409. Self-billed invoices use their own workflow. A repeat of the
completed mapping does not repeat the matching change; permission checks for
every selected order and period audit rules still apply. Both operations are available through call_public_api, with
confirm:true for the POST.
Retained supplier bill matching visibility
Section titled “Retained supplier bill matching visibility”Bill detail, bill mutation responses and GET /api/v1/finance/supplier-bills
apply cost permissions separately for each bill’s project and current client. Retained matching
figures also require current cost access to every referenced purchase order’s
project and client. If an order is unavailable or access is denied,
matchSnapshot and policy-check details are omitted and matches is empty.
The bill’s own permitted figures remain available. Stale check details are
omitted, and reading does not change saved history or totals.
These rules also apply to MCP call_public_api, list_supplier_bills and
draft_supplier_bill responses. Existing inputs and write permissions apply.
See matching visibility.
Save a manual budget as a template
Section titled “Save a manual budget as a template”POST /api/v1/finance/budgets/:id/save-as-template accepts a non-empty name
and returns 201 with { template }. Manual budgets may have null
pricingSettings; saving preserves that absence along with currency and billing
entity. Older quote-converted budgets without recorded pricing return 409 until
quote pricing has been reviewed and adopted. A failed save creates no partial
template. Invalid input returns 400, foreign budgets return 404, and missing or
revoked template-management permission returns 403. MCP callers use the same
operation through call_public_api with mutation confirmation.
Task budget line mapping
Section titled “Task budget line mapping”Use PUT /api/v1/finance/tasks/:taskId/budget-line with
{ "budgetLineId": "<line-uuid>" } to map one task, or null to clear it. The
response returns taskId, budgetLineId and previousLineId. For several tasks
in one project, use PUT /api/v1/finance/tasks/budget-line with taskIds and
budgetLineId; the response returns the number mapped.
These writes require finance:manage_budgets for the affected scope. The line
must fund the task’s project. Invalid IDs, foreign records and denied permission
are refused. Task details, personal task lists and project task reads then return
the saved nullable budgetLineId. Unrelated task updates retain it.
Both writes and reads are available through call_public_api, using paths
without /api/v1. Full get_task and list_tasks records also include the ID;
compact task lists omit it. Budget amount visibility retains its own permissions.
Purchasing document permissions
Section titled “Purchasing document permissions”Bill detail, lists and mutation responses use the bill’s project and that project’s current client for cost access. Purchase order responses and document previews use the project and client recorded on the order. Creating an order or explicitly changing its draft project sets the client from that project; clearing the project also clears the client. Later project-client changes do not rewrite existing orders. Organisation, client and project grants apply in that order, followed by the normal Finance lens rules.
Each list row uses its own scope. Purchase read and management capabilities stay
separate from cost visibility. Foreign bill projects return 404 before saving.
Manual attachment and matching writes recheck the bill’s project and client. If
either changed after permission was checked, the request returns 409
BILL_ATTACHMENT_CHANGED or BILL_MATCH_CHANGED without saving. Reload before
trying again. A repeated mapping also requires cost access to each selected order.
Generated REST and MCP call_public_api share these rules. Named raise_po,
draft_supplier_bill and list_supplier_bills return the same scoped document
figures. See Purchasing for the browser workflow.
A bill’s payment history shows the amount allocated to that bill when you can view its costs. A payment covering several bills shows its full total only when you can view costs for all of them. Viewing any unallocated amount also needs organisation-wide cost access. Reimbursement-linked payment totals are omitted from bill history.
Supplier-payment record and reversal responses show amounts only with cost
access to every contributing bill, using each bill’s current project and client.
Selling-price access alone is insufficient. A record response also checks every
selected bill, including bills that receive no allocation; allocations takes
precedence over billIds when both are supplied. An unallocated remainder needs
organisation-wide cost access as well. Reimbursement-linked totals, reversals
without a valid original and amounts with incomplete source information are omitted.
An authorised write still succeeds when its amounts are hidden. The response
keeps payment identity and the successful HTTP status but omits amount, reportingAmount,
realisedFxAmount, allocated and unallocated. Visible payment amounts retain
{ amount, currency }; the two allocation totals remain minor-unit strings.
Explicit API overpayments remain supported, including payments entirely
unallocated to an already-paid bill. These need organisation cost access and
cost access to every selected bill to show the record result. Reversing wholly
unallocated ordinary cash needs organisation cost access. The bill’s own visible
allocation in payment history remains independent of the whole-payment total.
The same rules apply through MCP call_public_api.
Purchasing aggregate permissions
Section titled “Purchasing aggregate permissions”Supplier spend needs organisation-wide cost access and access to every bill
included in the period. The supplier list and named get_supplier return
spendYearByCurrency, sorted by currency, as { amount, currency } pairs. Amounts
are exact minor-unit strings in the bill’s original currency, including tax.
For example, [{ "amount": "10000", "currency": "AUD" }, { "amount": "5000", "currency": "USD" }]
means AUD 100 and USD 50. Do not add the currencies or label them with the supplier’s
default. No exchange-rate conversion is applied.
The period runs from 365 days ago through today in the organisation timezone, including those dates. Drafts,
voids and future bills are excluded. An empty array means no included bills;
a zero total retains its currency. The legacy spendYear scalar remains for
zero or one currency and is omitted for mixed currencies. Use the array for new
integrations. Both spend fields are omitted when any included bill is restricted,
including in get_supplier; you never receive a partial total. A supplier beyond
the first page has the same rules. The supplier detail route continues to return
master data, contacts and the freelancer link. Open counts and last-bill date
are independent of the spend period. These reads do not change saved bills.
These complete reports return 403 if any contributing order or bill is
restricted:
GET /api/v1/finance/supplier-bills/ageingGET /api/v1/finance/accruals/periods/:periodIdGET /api/v1/finance/accruals/pass-through
A project or supplier filter does not grant access to linked bills. Period
access includes earlier receipts, historical and voided bills, and zero-net
balances. The currency query selects the computed balance; it does not limit
the stored currency rows returned. All returned currencies need access. Without
a requested or stored currency, the report computes the first currency, in
alphabetical order, from receipts dated on or before the period end.
GET /api/v1/finance/supplier-payments/payable lists only bills whose costs you
can view. It keeps its organisation cost and payment capability requirements.
An empty list is not a complete statement of what the organisation owes.
PO detail can omit line billed, line accruedAmount and totals.billed when a
linked bill is restricted, while retaining the order’s permitted amounts. PO
list rows do not include computed detail totals. Saved matching figures need
access to their historical bill contributors; live previews need access to
current contributors. A denied refresh does not substitute zero.
Permitted purchase report amounts need cost access independently of selling
prices. These rules apply equally to generated REST credentials and MCP through
call_public_api. See Purchasing and
Paying suppliers for the browser workflow.
Scoped profitability through MCP
Section titled “Scoped profitability through MCP”Use get_profitability with projectId or clientOrganizationId when your
financial access is limited to that subject. Amounts follow its price, cost and
profit permissions. Cost-only access keeps delivery cost amounts but omits revenue
and margins. If you omit the subject, organisation-wide visibility applies, so a
scoped-only reader can receive no money metrics.
The tool uses the same metrics, filters and date validation as
GET /api/v1/finance/metrics. Its default grouping remains client. Successful
responses keep rows, totals, definitions and coverage at the top level with
success: true. A route failure includes success: false, HTTP status and the
route’s response in data, with error and code when supplied by the route.
Invalid dates or filters return 400. Expired grants,
revoked access and inactive membership do not retain report access.
A session limited to one client always uses that client, and refuses a different
client argument. A tool allowlist must include get_profitability. Job-bound
sessions cannot use this report because it has no job-level financial scope;
use your ordinary generated API key or unscoped MCP session. See
MCP Server for credentials and
Finance Reports for metric definitions.
Supplier upload cost scope
Section titled “Supplier upload cost scope”Staff GET /finance/supplier-portal/uploads and GET /finance/supplier-portal/uploads/:id
return a cost lens for each row. Proposal amounts, nested quantities/notes, original
asset/hash and internal rejection details are absent when any linked purchase
order or bill denies cost access. Purchase orders retain their client scope; bills
follow the current client of their project. Unattached uploads use organisation
cost access. Cost-only access preserves purchase amounts without requiring prices.
POST /finance/supplier-portal/uploads/:id/accept needs bill-management access.
Creating a draft from a linked upload also needs source cost access, including
when review fields or a different target project are supplied. Denial changes
nothing. Complete untaxed unattached proposals retain acceptance without cost
visibility when creating an unscoped draft. Supplying a target projectId for
an unattached upload requires organisation cost access. Changing or clearing an
existing draft bill’s projectId requires cost access to its original scope.
A repeated accepted request returns the existing bill; response
projection follows current access. Decline keeps bill-management authority and
returns the same protected projection. REST and call_public_api share these rules.
Original file reads follow supplier invoice source access.
Live month-end scope
Section titled “Live month-end scope”GET /api/v1/finance/periods/:id/assistant returns a complete live brief only
with unrestricted organisation-wide prices. Any active project or client price
deny returns 403, including a restriction outside the selected month. A scoped
allow alone is insufficient. Cost restrictions do not block this sales/WIP brief.
A successful response reads policy and figures from one consistent snapshot and
creates no records.
Period list/detail and ensure, soft-close, close and reopen responses omit current and saved checklist amounts under those price restrictions. Counts, status and review links remain available under the route’s entry permissions. Period audit history applies the same omission. Stored history is unchanged, and closing permission remains independent of price visibility.
These rules also apply through call_public_api. Named get_period_checklist
uses the same checklist projection for either selector. Complete close reports
and exports retain their separate price-and-cost requirements. See
Month End.