GET /api/agent/invoices
Returns AR invoices (CashItem source = XERO_INVOICE) for the caller's org. Filterable by Xero status, contact, and date range. Two pagination modes: (1) cursor pagination via cursor / nextCursor (default, max 200 rows per page — caller MUST loop until nextCursor is null), or (2) ?autoPaginate=true opt-in which loops pages server-side and returns the combined set in one response (hard-capped at 5,000 rows; truncated: true is set when the cap is hit so the caller knows to fall back to cursor mode). For sum/total calculations always use ?autoPaginate=true or loop the cursor to completion — reporting partial-page sums as totals is the most common agentic failure mode this endpoint guards against. The response always includes totalCount (pre-redaction match count) so agents can decide up-front whether to paginate. Opt-in totals (?totals=true): when set, the response carries an aggregate totals.byStatus + totals.grandTotal block computed across the full filtered set (not just the page); prefer this over ?autoPaginate=true for pure aggregate questions ("how much have I invoiced YTD?") as it avoids transporting the rows. Set lines=true to include each invoice's XeroInvoiceLine[]. Line items carry per-unit pricing (quantity, unitAmount) plus line-level discount data (discountRate 0–1, discountAmount, lineSubtotal) so callers can reconcile lineSubtotal * (1 - discountRate) ≈ lineAmount without going back to Xero. Sensitive contacts and (when lines=true) sensitive account codes drop invoices from the response unless the key carries READ_SENSITIVE; the redactedItemCount field surfaces the count of dropped rows. This endpoint EXCLUDES credit notes (CN-####) — they are a separate Xero document type. Use /api/agent/credit-notes (and /api/agent/credit-notes/{id}) to look those up. A 'not found' here does NOT mean a claimed credit doesn't exist; check the credit-note endpoint before concluding a credit is absent.
Auth
- Required scope:
READ_INVOICES - Header:
Authorization: Bearer cr_live_<prefix>_<secret>
Query parameters
| Name | Type | Required | Notes |
|---|---|---|---|
status | string | no | Comma-separated list of Xero invoice statuses: DRAFT, SUBMITTED, AUTHORISED, PAID, VOIDED, DELETED. Unknown values are ignored. |
contactId | string | no | Filter by Xero contact external id. |
dateFrom | string | no | Inclusive lower bound on expectedDate (YYYY-MM-DD). |
dateTo | string | no | Inclusive upper bound on expectedDate (YYYY-MM-DD; expanded to end-of-day UTC). |
cursor | string | no | Opaque cursor returned by a prior request's nextCursor. |
limit | integer | no | Max rows per page. Capped at 200. |
lines | boolean | no | When true, include XeroInvoiceLine[] per invoice. |
autoPaginate | boolean | no | When true, the server loops pages internally and returns the combined matched set in one response (hard-capped at 5,000; truncated: true is set on overflow). Mutually exclusive with cursor — cursor is silently ignored when autoPaginate=true. Use this for sum / aggregate reads to avoid partial-page-totalling bugs. |
totals | boolean | no | When true, the response carries an aggregate totals block computed across the FULL filtered set (NOT just the page). Shape: { byStatus: { PAID: { count, amount }, AUTHORISED: {...}, ... }, grandTotal: { count, amount }, currency }. Mixed-currency sets short-circuit to { mixedCurrency: true, primaryCurrency, byStatus: null }. Over the 100K aggregation cap, totals is omitted and totalsTruncated: true is set instead. Off by default — the aggregation runs an extra GROUP BY status, SUM(amount) query so reserve it for the questions that need it. |
Responses
200 — Page of invoices
Body: 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 |
401 — Unauthorized
Body: ErrorResponse
| Field | Type | Required | Notes |
|---|---|---|---|
error | string | yes |
403 — Key lacks the required scope
Body: ErrorResponse
| Field | Type | Required | Notes |
|---|---|---|---|
error | string | yes |
429 — Rate-limited or quota-exhausted
Body: ErrorResponse
| Field | Type | Required | Notes |
|---|---|---|---|
error | string | yes |
Response headers
Every successful response carries X-CashRunway-Subscription, X-CashRunway-Plan, X-CashRunway-Quota-Remaining, and X-CashRunway-Quota-Reset. Trialing subscriptions also include X-CashRunway-Trial-Days-Remaining. See the overview for details.