API Reference
Read-only agentic surface for cash position, bank balances, and 13-week forecasts. Bearer-token auth (API keys start with cr_live_). All decimal values are serialised as canonical strings — JSON-safe and drift-free across IEEE-754.
Endpoints
| Endpoint | Scope | Summary |
|---|---|---|
GET /api/agent/account-balances | READ_ACCOUNTS | Balance-sheet account balances as-at a date |
GET /api/agent/bank-accounts | READ_BANK | List active bank accounts |
GET /api/agent/bills | READ_INVOICES | List AP bills |
GET /api/agent/cash-position | READ_FORECAST | Read the current cash position |
GET /api/agent/cohorts/retention | READ_SUMMARY | Full cohort retention matrix |
GET /api/agent/cohorts/summary | READ_SUMMARY | Cohort KPI summary |
GET /api/agent/contacts | READ_CONTACTS | List customers / suppliers |
GET /api/agent/contacts/summary | READ_SUMMARY | Contact-population aggregates (byType, topByRevenue, topBySpend, ABC) |
GET /api/agent/contacts/{id} | READ_CONTACTS | Single-contact drill |
GET /api/agent/credit-notes | READ_INVOICES | List AR credit notes |
GET /api/agent/credit-notes/{id} | READ_INVOICES | Get a single AR credit note |
GET /api/agent/daily-summary | READ_SUMMARY | Composite daily summary — the agent killer endpoint |
GET /api/agent/expenses/summary | READ_SUMMARY | Aggregate expense summary (AP-side mirror) |
GET /api/agent/forecast | READ_FORECAST | Fetch the latest 13-week forecast |
GET /api/agent/invoices | READ_INVOICES | List AR invoices |
GET /api/agent/orgs | any | Reachable client orgs + key kind (partner discovery) |
GET /api/agent/revenue/summary | READ_SUMMARY | Aggregate revenue summary (the killer agent endpoint) |
GET /api/agent/status | any | Billing + quota + key context for session bootstrap |
Authentication
Every agent endpoint requires a Bearer token in the Authorization header. Tokens start with cr_live_ and are issued from Settings → API keys. Scopes are assigned per key — an endpoint's required scope is listed alongside its URL.
Rate limits
Per-key burst cap: 60 requests / minute / endpoint. Query credits are metered per (user, account) in a rolling 5-hour window — Free 10 / Pocket 50 / Wallet 250 / Vault 1,000 / Reserve 5,000 per window — and reported in the response usage block. Credits free up continuously as each call ages past 5 hours; run dry and you can upgrade your plan or let Cushion auto-top-up. The /api/agent/status endpoint is exempt from query-credit counting. See Plans, query credits & accounts.
Response headers
Every successful response carries:
| Header | Value |
|---|---|
X-CashRunway-Subscription | trialing / active / past_due / canceled / trial_ended |
X-CashRunway-Plan | Subscription display name (legacy field). For the current query-credit plan, read the usage block. |
X-CashRunway-Trial-Days-Remaining | integer, only when trialing |
X-CashRunway-Reset-Minutes | integer — minutes until the next query credit frees up (mirrors usage.nextCreditInMinutes). |
X-CashRunway-Quota-Remaining | integer (legacy per-org daily bucket — prefer usage.remaining). |
X-CashRunway-Quota-Reset | ISO timestamp (legacy daily bucket — prefer usage.windowResetAt). |
When an escalation trigger fires (trial ≤5d, quota ≥90%, status ≠ active/trialing) a billingAlert field is folded into the JSON body. Its absence on a healthy call is the positive signal.
Schemas
Click to expand all schemas
BillingAlert
Present only when an escalation trigger fires (trial ≤5 days, quota ≥90%, or subscription status ∈ {past_due, canceled, trial_ended}). Absence of this field on a healthy response is the positive signal — LLMs don't need to check every call.
| Field | Type | Required | Notes |
|---|---|---|---|
severity | string · one of warn critical | yes | |
message | string | yes | |
cta | object | yes |
XeroAlert
Present only when the org's Xero connection needs operator attention. Absence = healthy connection. Three kinds: reconnect_required (critical — token expired or circuit breaker tripped, data is stale and won't update); service_issue (warn — Xero returning errors but auto-retrying, data may be up to 12 hours old); no_connection (warn — no Xero account connected at all). Always tell the user when this field is present.
| Field | Type | Required | Notes |
|---|---|---|---|
severity | string · one of warn critical | yes | |
kind | string · one of reconnect_required service_issue no_connection | yes | |
message | string | yes | |
lastSuccessfulSync | string,null (date-time) | yes | ISO timestamp of the last successful Xero sync, or null if never synced. |
cta | object | yes |
AgentNote
Plain-language context an agent / LLM can reason over without re-deriving signals from raw numbers.
| Field | Type | Required | Notes |
|---|---|---|---|
kind | string · one of seasonality trend pipeline ar concentration | yes | |
window | string · one of this_month next_4w this_quarter | yes | |
severity | string · one of info warn | yes | |
text | string | yes | |
sourceInsightId | string | no | Set when the note was produced by the insight engine; omitted for fallback-detector notes. |
ErrorResponse
| Field | Type | Required | Notes |
|---|---|---|---|
error | string | yes |
BankAccount
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | |
name | string | yes | |
currency | string | yes | 3-letter ISO code. |
reconciledBalance | string | yes | |
statementBalance | string | yes | |
balanceSource | string · one of STATEMENT RECONCILED | yes | |
balanceSyncedAt | string (date-time) | no |
CashPositionResponse
| Field | Type | Required | Notes |
|---|---|---|---|
asOfDate | string (date) | yes | |
startingCash | string | yes | Effective opening cash (post-override, post-auto-adjust). Equal to effectiveStartingCash today; surfaced under both names for clarity. |
source | string · one of manual xero_reconciled | yes | |
xeroReconciledTotal | string | yes | Bare sum of Xero-reconciled bank balances across the org's RECONCILED accounts. This is the honest ledger total — no operator override or auto-detected unreconciled-transaction adjustment applied. Compare against effectiveStartingCash to detect drift driven by overrides or pending Xero categorisation. |
effectiveStartingCash | string | yes | Post-override and post-auto-adjust opening cash, used by the forecast engine. Diverges from xeroReconciledTotal when an operator override or auto-detected unreconciled-transaction adjustment is in play. |
currency | string | no | |
accounts | array of object | yes | |
redactedItemCount | integer | yes | Number of accounts rows whose name was rewritten to "Sensitive account" for this caller. Always 0 when the key carries the READ_SENSITIVE scope. Lets agentic consumers know they're operating on a masked view without inspecting every name. |
notesForAgent | array of AgentNote | no | |
billingAlert | BillingAlert | no | |
xeroAlert | XeroAlert | no |
ForecastWeek
| Field | Type | Required | Notes |
|---|---|---|---|
weekStart | string (date) | yes | |
weekEnd | string (date) | yes | |
opening | string | yes | |
inflow | string | yes | |
guaranteedInflow | string | no | Subset of inflow from guaranteed sources (paid AR, recurring, manual, pipeline). Drives the dashboard's conservative runway line. Optional; absent on engineVersion=1 snapshots written before this split. |
forecastInflow | string | no | Subset of inflow from STATISTICAL items only (predicted POs from the hazard model). Drives the dashboard's optimistic runway line, layered on top of conservative. Optional; absent on engineVersion=1 snapshots, in which case readers default to 0 (collapses optimistic onto conservative). |
outflow | string | yes | |
net | string | yes | |
closing | string | yes | |
itemIds | array of string | yes | |
overdueItemIds | array of string | no | |
probabilityWeighted | boolean | no | |
categories | object | no |
ForecastResponse
| Field | Type | Required | Notes |
|---|---|---|---|
scenarioId | string | yes | |
scenarioName | string | no | |
scenarioKind | string · one of BASE BEST WORST CUSTOM | no | |
computedAt | string (date-time) | yes | |
weeks | array of ForecastWeek | yes | |
startingCash | string | yes | |
endingCash | string | yes | |
minClosingCash | string | no | |
minClosingCashWeekIndex | integer | no | |
totalInflow | string | no | |
totalOutflow | string | no | |
redactedItemCount | integer | yes | Sum across all weeks of (week, account) pairs whose account name was rewritten to "Sensitive account" inside weeks[].categories.accounts[id].name for this caller. Always 0 when the key carries READ_SENSITIVE. The net cash trajectory (opening/inflow/outflow/closing) is never masked: a sensitive account's totals still feed weekly amounts (Stripe restricted-key pattern). |
notesForAgent | array of AgentNote | no | |
billingAlert | BillingAlert | no | |
xeroAlert | XeroAlert | no |
OrgsResponse
| Field | Type | Required | Notes |
|---|---|---|---|
keyKind | string · one of PARTNER USER | yes | PARTNER for an advisory partner key (multi-client); USER for an ordinary org key. |
orgs | array of object | yes | Client orgs this key can reach, sorted by name. For PARTNER keys these are the advisory-linked clients (intersected with the key's scope ceiling); for USER keys, the orgs the key's creator is a member of. |
StatusResponse
| Field | Type | Required | Notes |
|---|---|---|---|
subscription | object | yes | |
quota | object | yes | Legacy per-org daily quota bucket (retained for backwards-compat). For the query-credit model — credits remaining AND minutes until they free up — read the usage block instead. |
usage | object | no | Rolling-5h query-credit window for the key's (user, billing-account-of-target-org). remaining is credits left; nextCreditInMinutes is the wait until the FIRST credit frees (0 when under cap — a credit is available now); fullResetInMinutes is the wait until EVERY credit returns (0 for an empty window); windowResetAt is the ISO instant the window fully empties. The same nextCreditInMinutes value is mirrored on the X-CashRunway-Reset-Minutes response header. null when the key has no billable seat or the target org has no owning billing account (a status probe still succeeds; usage is simply unresolvable). |
key | object | yes | |
environment | string · one of dev prod | yes | |
billingAlert | BillingAlert | no | |
xeroAlert | XeroAlert | no |
InvoiceLine
Line-item shadow of a Xero Invoice.LineItems[] (or Bill.LineItems[]) entry.
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | |
lineNumber | integer | yes | |
accountCode | string | no | |
accountName | string | yes | |
lineAmount | string | yes | Ex-GST line subtotal as a canonical decimal string. |
taxAmount | string | yes | GST / tax charged on the line, as a canonical decimal string. |
quantity | string | no | Units sold on this line (Xero LineItem.Quantity) as a canonical decimal string. null when Xero omits the field (rare; manual-journal-style invoices) or when the line was ingested before this column was added and its parent invoice hasn't been re-synced yet. Treat null as 'unknown' rather than 'zero'. |
unitAmount | string | no | Per-unit list / RRP ex-GST price (Xero LineItem.UnitAmount, pre-discount) as a canonical decimal string. Same null-semantics as quantity. Combine with discountRate or discountAmount to derive the effective per-unit price. |
discountRate | string | no | Line-level discount as a fraction 0–1 (e.g. "0.5" for 50% off). Normalised from Xero's LineItem.DiscountRate percent at ingest. null means the line is at list price OR the discount was entered as an absolute amount (see discountAmount). Sales invoices only — always null on bills. |
discountAmount | string | no | Line-level discount as an absolute amount in invoice currency (Xero's LineItem.DiscountAmount). Mutually exclusive with discountRate. Sales invoices only — always null on bills. |
lineSubtotal | string | no | Pre-discount list-price subtotal — quantity * unitAmount — captured at ingest. null when either factor is null (manual-journal-style line). Distinct from lineAmount which is the billed amount AFTER discount. |
taxType | string | no | |
currency | string | yes | |
lineDescription | string | no |
Invoice
Agent-API projection of a CashItem row (source = XERO_INVOICE or XERO_BILL). xeroStatus collapses the internal status into a canonical Xero status value.
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | |
externalId | string | no | |
invoiceNumber | string | no | |
reference | string | no | |
amount | string | yes | |
currency | string | yes | |
expectedDate | string (date-time) | yes | |
actualDate | string (date-time) | no | |
issueDate | string (date-time) | no | |
status | string · one of FORECAST ACTUAL CANCELLED | yes | Internal CashItem lifecycle. Agents filter by xeroStatus instead; status is surfaced for diagnostic transparency. |
xeroStatus | string · one of DRAFT SUBMITTED AUTHORISED PAID VOIDED DELETED | yes | |
contactId | string | no | |
contactName | string | no | |
category | string | no | |
lines | array of InvoiceLine | no | Present only when lines=true was supplied on the request. |
ListTotalsBucket
Single status / type bucket inside totals.byStatus. count is the row count; amount is the gross sum (canonical decimal string, trailing zeros trimmed). For contacts, only count is meaningful; the schema is shared but contacts totals.byType uses plain integers (no buckets) since there is no $ axis.
| Field | Type | Required | Notes |
|---|---|---|---|
count | integer | yes | |
amount | string | yes | Canonical decimal string, e.g. "1099419.91". "0" when the bucket is empty. Trailing zeros trimmed per serializeDecimal(). |
InvoiceBillTotals
Opt-in aggregate block returned on /api/agent/invoices and /api/agent/bills when ?totals=true is set. Three variants: (1) single-currency — byStatus carries every known Xero status as a key ({count:0, amount:"0"} when unused; agents are sensitive to absent keys), grandTotal is the sum across buckets, and currency carries the org's transacting currency for the matched set. (2) multi-currency — mixedCurrency: true, primaryCurrency set, byStatus: null (a cross-currency sum would mislead more than no sum). (3) empty matched set — byStatus carries all-zero buckets and grandTotal is {count:0, amount:"0"}. Note: when the matched set exceeds 100,000 rows the totals block is OMITTED and the parent response sets totalsTruncated: true instead — fall back to a tighter filter (date range / status) to bring the scan back under the cap.
| Field | Type | Required | Notes |
|---|---|---|---|
byStatus | object | no | Map of Xero status → bucket. ALWAYS includes every known status key zero-filled when the matched set is non-empty single-currency. null when mixedCurrency: true. |
grandTotal | ListTotalsBucket | no | |
currency | string | no | Single currency of the matched set (e.g. "AUD"). Present when the set is non-empty and spans exactly one currency; omitted on the multi-currency and empty-set branches. |
mixedCurrency | boolean | no | Present and true only when the matched set spans >1 currency. When set, byStatus is null and primaryCurrency carries the (lexicographically-first) currency code purely for the response shape. |
primaryCurrency | string | no | Set only when mixedCurrency: true. |
ContactsTotals
Opt-in aggregate block returned on /api/agent/contacts when ?totals=true is set. Distinct shape from invoices/bills — contacts have NO $ axis (LTV / spend live on TrendBaseline, not aggregable inline), so this surfaces only count buckets. ALWAYS includes every byType key (customer / supplier / both / neither) zero-filled when unused.
| Field | Type | Required | Notes |
|---|---|---|---|
byType | object | yes | |
grandTotal | object | yes | Sum across byType buckets — reconciles to totalCount. |
InvoicesResponse
| Field | Type | Required | Notes |
|---|---|---|---|
invoices | array of Invoice | yes | |
nextCursor | string | yes | Opaque base64url cursor — pass back as ?cursor=... on the next request. Always present; null signals 'this is the last page in this view'. Both the natural last page AND an ?autoPaginate=true response (truncated or not) emit null — truncation is signalled via the separate truncated field, not via the cursor. |
totalCount | integer | yes | Total number of rows matching the request filters (pre-redaction). Reconciles to Xero source-of-truth; the page array may be shorter if sensitive rows were dropped (the delta is redactedItemCount). Always present. |
redactedItemCount | integer | yes | Number of rows withheld in this page due to sensitive-data gating. Always 0 when the key carries READ_SENSITIVE. |
truncated | boolean | no | Present and true only when ?autoPaginate=true was requested and more than 5,000 rows matched. The response carries the first 5,000 rows; fall back to cursor pagination (drop autoPaginate=true) to read the rest. Absent on healthy responses. |
totals | InvoiceBillTotals | no | |
totalsTruncated | boolean | no | Present and true only when ?totals=true was requested AND the matched set exceeded the 100,000-row aggregation cap. The totals block is omitted in this case; refine the filter (date range / status) to bring the scan back under the cap. |
billingAlert | BillingAlert | no | |
xeroAlert | XeroAlert | no |
BillsResponse
| Field | Type | Required | Notes |
|---|---|---|---|
bills | array of Invoice | yes | |
nextCursor | string | yes | |
totalCount | integer | yes | Total bills matching the request filters (pre-redaction). See InvoicesResponse.totalCount. |
redactedItemCount | integer | yes | |
truncated | boolean | no | Present and true only when ?autoPaginate=true was requested and more than 5,000 rows matched. See InvoicesResponse.truncated. |
totals | InvoiceBillTotals | no | |
totalsTruncated | boolean | no | Present and true only when ?totals=true was requested AND the matched set exceeded the 100,000-row cap. See InvoicesResponse.totalsTruncated. |
billingAlert | BillingAlert | no | |
xeroAlert | XeroAlert | no |
ContactSummary
Agent-API projection of a XeroContact row. ltv is populated for customer rows where the nightly baseline worker has run; null otherwise (warming, or supplier-only contacts).
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Xero contact external id (the same value returned on invoices / bills as contactId). |
name | string | yes | |
isCustomer | boolean | yes | |
isSupplier | boolean | yes | |
isSensitive | boolean | yes | |
lastSyncedAt | string (date-time) | yes | |
ltv | string | no | HISTORICAL cumulative gross-profit LTV from the nightly TrendBaseline.ltvCumulative (all realised revenue × GP%). This is a DIFFERENT metric from the contact-drill endpoint's projectedLtv (a live, churn-adjusted FORWARD projection): this list field is backward-looking and refreshed by the nightly worker, the drill's projectedLtv is computed live per request. Null when the baseline hasn't computed yet or the contact isn't a customer. |
ContactsListResponse
| Field | Type | Required | Notes |
|---|---|---|---|
contacts | array of ContactSummary | yes | |
nextCursor | string | yes | Opaque base64url cursor — pass back as ?cursor=.... Always present; null signals the last page in this view. ?autoPaginate=true responses always carry null here — truncation is signalled separately via truncated: true. |
totalCount | integer | yes | Total number of contacts matching the request filters (pre-redaction). See InvoicesResponse.totalCount. |
redactedItemCount | integer | yes | Number of contacts withheld due to sensitive-data gating. Always 0 when the key carries READ_SENSITIVE. |
truncated | boolean | no | Present and true only when ?autoPaginate=true was requested and more than 5,000 contacts matched. See InvoicesResponse.truncated. |
totals | ContactsTotals | no | |
totalsTruncated | boolean | no | Present and true only when ?totals=true was requested AND the matched contact set exceeded the 100,000-row cap. |
billingAlert | BillingAlert | no | |
xeroAlert | XeroAlert | no |
ContactDrill
Single-contact drill payload. Revenue + lifetime-value signals: revenueTrailing12m / revenueAllTime are realised inflow sums; projectedLtv is the churn-adjusted forward gross profit; historicalLtv is earned-to-date gross profit. Other derived signals (payment-days median, cadence, abcClass) come from TrendBaseline; the engine falls back to direct computation on warming customers.
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | |
name | string | yes | |
isCustomer | boolean | yes | |
isSupplier | boolean | yes | |
isSensitive | boolean | yes | |
lastSyncedAt | string (date-time) | yes | |
availableCredit | string | yes | Unallocated AR credit this customer currently holds — sum of remainingCredit across their AUTHORISED ACCRECCREDIT credit notes in the org base currency, as a canonical decimal string ("0" when none). Foreign-currency credits are excluded from this scalar; see /api/agent/credit-notes for the per-currency breakdown. Answers 'does this customer hold a credit?' in one call. |
revenueTrailing12m | string | yes | Trailing-12-month realised revenue (sum of ACTUAL inflows over the last 365 days), as a canonical decimal string. Doubles as the annual run-rate feeding projectedLtv. Always present ("0" when no inflows). |
revenueAllTime | string | yes | All-time realised revenue (sum of every ACTUAL inflow to date), as a canonical decimal string. Feeds historicalLtv. Always present ("0" when no inflows). |
projectedLtv | string | no | Churn-adjusted forward gross-profit lifetime value: run-rate × gross-margin × expected-lifetime-years × churn-retention-factor (Active 1.0 / At-risk 0.5 / Churned 0.0). Null when the org has no COGS convention (grossMarginPercent null); Decimal "0" for a churned customer. |
historicalLtv | string | no | Earned-to-date gross-profit lifetime value: all-time realised revenue × gross-margin. Null when the org has no COGS convention (grossMarginPercent null). |
grossMarginPercent | number | no | Org-level gross-margin as a fraction in [0,1] (0.42 = 42%). Null when the org has neither a GP% override nor enough COGS history to derive one. |
expectedLifetimeYears | number | yes | Org-derived expected customer relationship length in years — the median engaged-customer (cadence ≠ NONE) lifetime, clamped to [1,5]. Drives the projectedLtv horizon and is identical across the customer card, customers list, and this API. |
paymentDelayMedianDays | string | no | Median days late on paid invoices, in canonical decimal string form. |
cadence | string · one of WEEKLY FORTNIGHTLY MONTHLY QUARTERLY ANNUAL NONE | no | |
cadenceConfidence | integer | no | |
abcClass | string · one of A B C | no | 80/20 customer tier. Null for non-customer contacts or pre-worker baselines. |
lastInvoice | object | no | |
lastBill | object | no | |
recentPaidInvoices | array of object | yes | Up to 50 most recent paid invoices. Entries on sensitive accounts are kept in the array (count preserved) but their accountName is replaced with the sentinel Sensitive account and accountCode is null. redactedItemCount on the wrapper reports how many entries were masked. |
ContactDrillResponse
| Field | Type | Required | Notes |
|---|---|---|---|
contact | ContactDrill | yes | |
redactedItemCount | integer | yes | Number of recentPaidInvoices[] entries masked due to sensitive-account gating. |
billingAlert | BillingAlert | no | |
xeroAlert | XeroAlert | no |
CreditNote
An AR credit note (Xero ACCRECCREDIT). status is the cash-flow label (FORECAST/ACTUAL/CANCELLED, same convention as invoices); xeroStatus is the raw Xero status. allocatedAmount = total − remainingCredit (the portion already applied to invoices). Amounts are canonical decimal strings.
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | |
externalId | string | yes | Xero CreditNoteID (GUID). |
creditNoteNumber | string | no | Xero CreditNoteNumber (e.g. "CN-0441"). |
contactId | string | no | |
contactName | string | no | |
issueDate | string (date-time) | yes | |
status | string · one of FORECAST ACTUAL CANCELLED | yes | |
xeroStatus | string · one of DRAFT SUBMITTED AUTHORISED PAID VOIDED DELETED | yes | |
total | string | yes | |
remainingCredit | string | yes | Unallocated portion still available to apply. |
allocatedAmount | string | yes | total − remainingCredit. |
currency | string | yes |
CreditNoteAllocation
An invoice this credit has been applied to.
| Field | Type | Required | Notes |
|---|---|---|---|
invoiceNumber | string | no | |
invoiceExternalId | string | no | |
amount | string | no | |
date | string | no |
CreditNoteLine
| Field | Type | Required | Notes |
|---|---|---|---|
description | string | no | |
quantity | string | no | |
unitAmount | string | no | |
lineAmount | string | no | |
itemCode | string | no | |
accountCode | string | no |
CreditNoteDrill
any
CreditNotesResponse
| Field | Type | Required | Notes |
|---|---|---|---|
creditNotes | array of CreditNote | yes | |
nextCursor | string | yes | |
totalCount | integer | yes | |
redactedItemCount | integer | yes | |
truncated | boolean | no | |
totals | InvoiceBillTotals | no | |
totalsTruncated | boolean | no | |
billingAlert | BillingAlert | no | |
xeroAlert | XeroAlert | no |
CreditNoteDrillResponse
| Field | Type | Required | Notes |
|---|---|---|---|
creditNote | CreditNoteDrill | yes | |
redactedItemCount | integer | yes | |
billingAlert | BillingAlert | no | |
xeroAlert | XeroAlert | no |
DailySummaryContactEvent
A single inflow or outflow event from the trailing 7-day window. contactName is rewritten to "Sensitive contact" when the underlying contact is flagged sensitive and the key lacks READ_SENSITIVE; amount is NEVER masked.
| Field | Type | Required | Notes |
|---|---|---|---|
contactName | string | yes | |
amount | string | yes | |
date | string (date-time) | yes | ISO timestamp of the actual money-moved date (CashItem.actualDate). |
SummaryBucket
| Field | Type | Required | Notes |
|---|---|---|---|
count | integer | yes | |
amount | string | yes |
SummaryMonthBucket
| Field | Type | Required | Notes |
|---|---|---|---|
month | string | yes | Calendar month in the org's Organization.timezone (IANA, default Australia/Sydney), formatted YYYY-MM. NOT UTC - a late-evening Sydney transaction lands in the local month, not the prior UTC one. |
count | integer | yes | |
amount | string | yes | Ex-GST amount in the org's base currency (per the line splitter). |
SummaryTopContact
| Field | Type | Required | Notes |
|---|---|---|---|
contactId | string | yes | Xero contact external id. Blank when the contact is sensitive AND the key lacks READ_SENSITIVE (paired with name = 'Sensitive contact'). |
name | string | yes | Contact display name. Rewritten to Sensitive contact for non-sensitive-scope keys (redact mode; amount + count preserved). |
count | integer | yes | |
amount | string | yes |
RevenueSummaryResponse
Aggregate AR summary for an org. PAID values are ex-GST (cash-basis P&L), reconciles to Xero within +-1%. AUTHORISED values are gross (what's owed). grandTotal is PAID-only - the byStatus buckets do NOT sum to it.
| Field | Type | Required | Notes |
|---|---|---|---|
asOfDate | string (date) | yes | |
currency | string | yes | |
window | object | yes | |
grandTotal | SummaryBucket | yes | |
byStatus | object | yes | Keyed by Xero invoice status (PAID, AUTHORISED, ...). PAID = ex-GST cash-basis per the line splitter. AUTHORISED = gross outstanding amounts from source-doc rows. |
byMonth | array of SummaryMonthBucket | yes | |
topContacts | array of SummaryTopContact | yes | |
redactedItemCount | integer | yes | Number of topContacts rows whose name was rewritten to Sensitive contact. Always 0 when the key carries READ_SENSITIVE. Other aggregates (byMonth, byStatus, grandTotal) are NEVER masked. |
grossProfit | object | yes | Org-wide gross profit. COGS is derived from per-line P&L coding on Xero bill lines (accounts typed DIRECTCOSTS), NOT bank-feed category descriptors. This is a STABLE trailing-window reading (buffered last-3-full-months) and intentionally does NOT honour the request's dateFrom/dateTo — it reports the org's structural margin, not the margin over the requested window. The same number drives the dashboard GP% panel and the customer LTV card. Present only on the revenue summary (the expenses summary has no gross-profit reading). |
billingAlert | BillingAlert | no | |
xeroAlert | XeroAlert | no |
CohortSummaryResponse
Acquisition-cohort KPI summary. Four headline KPIs (new customers, repeat-order rate, avg revenue/customer, 90-day retention) each with a period-over-period delta vs the immediately preceding 6-month window, the top-3 cohorts by M3 retention, and plain-English notesForAgent. Cohorts aggregate by acquisition month, so no per-contact rows are emitted (redactedItemCount is always 0). window.from/to span the full loaded cohort history (up to 24 acquisition months); KPI deltas compare the most-recent 6-month half vs the prior 6-month half.
| Field | Type | Required | Notes |
|---|---|---|---|
asOfDate | string (date) | yes | |
currency | string | yes | |
window | object | yes | Span of the loaded cohort history. from is the earliest acquisition month, to is the latest. Both are YYYY-MM month strings (not full dates). Empty string when the org has no cohorts yet. |
kpis | object | yes | The four headline cohort KPIs. Each is a {value, delta, deltaKind} tuple; delta is the period-over-period change and is null when there is no prior base to compare against. |
topCohorts | array of CohortTopEntry | yes | Up to three cohorts ranked by M3 (90-day) retention. |
redactedItemCount | integer | yes | Always 0 — cohorts aggregate by acquisition month, so no per-contact name is ever masked. Present for envelope parity with the other summary endpoints. |
notesForAgent | array of AgentNote | yes |
CohortKpi
A single cohort KPI: its current value, the period-over-period delta (null when there is no prior base), and the deltaKind describing how to read the delta.
| Field | Type | Required | Notes |
|---|---|---|---|
value | number | yes | The KPI's current value. A count for newCustomers, a fraction in [0,1] for repeatOrderRate/retention90d, and an amount for avgRevenuePerCustomer. |
delta | number,null | yes | Period-over-period change, or null when there is no prior base. Read per deltaKind: a rate delta is a relative fraction (0.15 = +15%); a pct delta is an absolute percentage-point change. |
deltaKind | string · one of pct rate | yes | rate → delta is a relative change fraction; pct → delta is an absolute percentage-point change. |
CohortTopEntry
One entry in topCohorts — an acquisition month, its cohort size, and its M3 (90-day) retention fraction.
| Field | Type | Required | Notes |
|---|---|---|---|
acquisitionMonth | string | yes | Acquisition month, YYYY-MM. |
cohortSize | integer | yes | Distinct customers first seen in the cohort month. |
retention3 | number | yes | M3 retention fraction in [0,1] — the share of the cohort still active 90 days after acquisition. |
CohortRetentionResponse
The FULL acquisition-month × period-offset cohort matrix — the raw grid behind the analytics cohort heatmap. Each cohort row carries its per-offset values, projected into one of three metrics via ?metric=. The not-yet-elapsed sentinel (null) is preserved verbatim for every metric, so an agent can distinguish 'offset has not happened yet' from a real churned-to-zero (0 / "0"). Cohorts aggregate by acquisition month — no per-contact PII is on the wire (redactedItemCount is always 0) and raw-matrix responses carry no notesForAgent (use /cohorts/summary for narrative insight).
| Field | Type | Required | Notes |
|---|---|---|---|
asOfDate | string (date) | yes | |
currency | string | yes | |
metric | string · one of retention customers revenue | yes | Which metric cohorts[].values[] is projected into. retention (default) = survival fraction 0–1; customers = distinct transacting customers (integer); revenue = cohort revenue as a canonical decimal string. |
periodLabels | array of string | yes | Column headers for the offset grid, e.g. ["M0","M1",…]. Length matches each cohort's values length (capped by ?periods=). |
cohorts | array of CohortRetentionRow | yes | |
redactedItemCount | integer | yes | Always 0 — cohorts aggregate by acquisition month, so no per-contact name is ever masked. |
notesForAgent | array of AgentNote | yes | Always empty for the raw matrix — use /cohorts/summary for narrative insight. |
CohortRetentionRow
One acquisition-month cohort row of the retention matrix.
| Field | Type | Required | Notes |
|---|---|---|---|
cohort | string | yes | Acquisition month, YYYY-MM. |
customers | integer | yes | Distinct customers first seen in the cohort month. |
revenue | string | yes | Canonical decimal string — total revenue summed across every elapsed offset for the cohort. |
activeNow | integer | yes | Distinct customers from the cohort that are still active as of asOfDate. |
values | array of number,string,null | yes | Per-offset projection of the selected metric, aligned to periodLabels. number for retention, integer-valued number for customers, decimal string for revenue, and null for offsets that have not yet elapsed for this cohort. |
ExpensesSummaryResponse
AP-side mirror of RevenueSummaryResponse. Same shape, same reconciliation guarantees (ex-GST PAID per splitter, gross AUTHORISED).
| Field | Type | Required | Notes |
|---|---|---|---|
asOfDate | string (date) | yes | |
currency | string | yes | |
window | object | yes | |
grandTotal | SummaryBucket | yes | |
byStatus | object | yes | |
byMonth | array of SummaryMonthBucket | yes | |
topContacts | array of SummaryTopContact | yes | |
redactedItemCount | integer | yes | |
billingAlert | BillingAlert | no | |
xeroAlert | XeroAlert | no |
ContactsSummaryResponse
Contact-population aggregates. byType + abcDistribution are population counts (never masked). topByRevenue + topBySpend rows are subject to name redaction.
| Field | Type | Required | Notes |
|---|---|---|---|
asOfDate | string (date) | yes | |
currency | string | yes | |
byType | object | yes | |
abcDistribution | object | yes | |
topByRevenue | array of SummaryTopContact | yes | Top customers by TrendBaseline.ltvCumulative (cumulative gross-profit LTV). |
topBySpend | array of SummaryTopContact | yes | Top suppliers by aggregate XERO_BILL gross amounts. The per-supplier spend baseline isn't materialised in TrendBaseline yet; v1 computes this in-memory at request time. |
redactedItemCount | integer | yes | Aggregate count of topByRevenue + topBySpend rows whose name was rewritten to Sensitive contact. Always 0 with READ_SENSITIVE. |
billingAlert | BillingAlert | no | |
xeroAlert | XeroAlert | no |
DailySummaryResponse
Composite agent-facing daily summary — the 'how's the business doing today?' surface. Combines cash position, last-7d activity, AR aging snapshot, 13w forecast headline, agent notes, and a deterministic prose narrative an LLM can read verbatim. Totals across the response reconcile against the operator's ledger regardless of scope; only display names are gated by sensitive-data redaction (redactedItemCount is the aggregate sum across topInflows + topOutflows).
| Field | Type | Required | Notes |
|---|---|---|---|
orgName | string | yes | |
generatedAt | string (date-time) | yes | |
asOfDate | string (date) | yes | |
cashToday | object | yes | |
bankAccounts | integer | yes | |
last7d | object | yes | |
ar | object | yes | |
forecast13w | object | yes | |
narrative | string | yes | Deterministic LLM-ready 2-4 sentence prose summary. Threshold-driven phrasing for the 7-day net signal (net positive / net negative / roughly flat). |
notesForAgent | array of AgentNote | no | |
redactedItemCount | integer | yes | Sum of topInflows[] + topOutflows[] rows whose contactName was rewritten to "Sensitive contact". Always 0 when the key carries READ_SENSITIVE. Totals (net, in, out, AR bands, cashToday) are NEVER masked. |
billingAlert | BillingAlert | no | |
xeroAlert | XeroAlert | no |