GET /api/agent/contacts
Returns XeroContact rows for the caller's org. Filter by type (customer|supplier|both, default both) and sort (revenue_desc|spend_desc|recent_desc, default recent_desc). Two pagination modes: cursor (cursor / nextCursor, default — caller MUST loop until nextCursor is null) or ?autoPaginate=true to receive the entire matched set in one response (hard-capped at 5,000; truncated: true on overflow). The response always includes totalCount (pre-redaction match count) so agents can answer 'how many customers do we have?' without iterating. Opt-in totals (?totals=true): adds a totals.byType block (customer / supplier / both / neither counts) honouring the type filter; count-only (contacts have no $ axis — LTV / spend live on TrendBaseline). Contacts flagged sensitive (isSensitive) are dropped unless the key carries READ_SENSITIVE; the dropped count surfaces as redactedItemCount.
Auth
- Required scope:
READ_CONTACTS - Header:
Authorization: Bearer cr_live_<prefix>_<secret>
Query parameters
| Name | Type | Required | Notes |
|---|---|---|---|
type | string | no | |
sort | string | no | revenue_desc / spend_desc sort the returned page in-memory by TrendBaseline.ltvCumulative (nulls last). recent_desc orders by lastSyncedAt — the default. |
cursor | string | no | |
limit | integer | no | |
autoPaginate | boolean | no | When true, the server loops pages internally and returns every matching contact in one response (hard-capped at 5,000; truncated: true on overflow). Mutually exclusive with cursor. Prefer for aggregate reads (e.g. "how many customers do we have?"). The totalCount response field gives the count without forcing autoPaginate. |
totals | boolean | no | When true, the response carries a totals block with byType counts (customer / supplier / both / neither) + grandTotal.count. NO $ axis (contacts have no aggregable amount — LTV / spend live on TrendBaseline). Honours the type filter. Over the 100K cap → totalsTruncated: true. |
Responses
200 — Page of contacts
Body: 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 |
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.