Searching, sorting and paging

GET /api/v1/contacts  can search, sort and page — the same three things the Statey dashboard does, with the same options.

Paging

Every paginated collection uses page  and page_size :

curl "https://app.statey.app/api/v1/contacts?page=2&page_size=100" \
  -H "Authorization: Bearer stky_your_key_here"
{
  "contacts": [ … ],
  "meta": {
    "page": 2,
    "page_size": 100,
    "total_pages": 6,
    "total_count": 573,
    "data_freshness": { "last_completed_at": "…", "rebuild_in_progress": false }
  }
}

page_size  defaults to 50 and is capped at 200. Ask for more and you get 200 rather than an error.

Searching

search  matches part of a customer's name or account number, ignoring case:

curl "https://app.statey.app/api/v1/contacts?search=acme" \
  -H "Authorization: Bearer stky_your_key_here"

It is a plain substring match, not a pattern — a %  in your search term matches a literal % .

Sorting

sort  takes one of six columns, and direction  is asc  (the default) or desc :

sort Orders by In the response?
xero_name Customer name. The default as name
balance_due What they owe in total yes
amount_outstanding What is outstanding no
amount_overdue What is overdue yes
most_days_overdue How late their oldest overdue item is yes
available_credit Credit available to them yes
curl "https://app.statey.app/api/v1/contacts?sort=most_days_overdue&direction=desc" \
  -H "Authorization: Bearer stky_your_key_here"

amount_outstanding  is the one to watch: you can order by it, but the listing does not return the value, so you cannot show or re-sort it yourself. It is there because the dashboard offers the same sort. If you need the number, balance_due  is returned and is what most integrations actually want.

An unrecognised sort  or direction  is refused with 422 validation_failed  rather than quietly ignored, so a typo cannot hand you correct-looking data in the wrong order.

The sharp edge, and how to avoid it

Read this before writing anything that pages through your whole customer list.

Statey recalculates every customer's balances in the background. When it does, a customer's summary row is replaced — the old one is deleted and a new one written. Partial syncs are queued every hour, on the fifth minute, and the recalculation trails a little behind that.

That matters when you are paging. If the numbers change while you are on page 3 of 12, rows can move between pages: a customer you have already seen can reappear, and one you have not seen can slip past you. Nothing errors. You simply end up with a slightly wrong list.

Three ways to avoid it, in order of how much they help.

1. Sort by name when you want everything

A customer's name does not change during a recalculation, so their position in a name-ordered list does not either. Paging is stable.

# Safe for a full export: position does not move underneath you.
curl "https://app.statey.app/api/v1/contacts?sort=xero_name&page_size=200"

Sorting by an amount ranks correctly at the moment of each request, but those amounts are exactly what a recalculation changes. Use amount sorts to answer "who are my worst twenty debtors" — a single page — rather than to walk the whole list.

2. Check the sync status before you start

GET /api/v1/sync_status

If a rebuild is running, wait for it to finish before starting a long export. See Data freshness.

3. Watch rebuild_in_progress  while you go

Every page you fetch carries it:

"meta": { "data_freshness": { "rebuild_in_progress": false } }

If it is false  when you start and true  on a later page, the data moved while you were reading. The safe response is to start again rather than trust the pages you already have.

A worked example

Exporting every customer who owes you anything, safely:

1. GET /api/v1/sync_status
   → if it is not idle, wait.

2. GET /api/v1/contacts?filter=outstanding&sort=xero_name&page_size=200
   → note meta.total_pages, and meta.data_freshness.rebuild_in_progress

3. Repeat for page=2 … page=N
   → if rebuild_in_progress ever becomes true, stop and start again from step 1

4. Compare meta.total_count against how many you collected.
   → a mismatch means the set changed while you read it

Step 4 is the cheap safety net: total_count  is recalculated on every request, so comparing it with what you actually collected catches a shifting list without any extra calls.

Filtering

filter  narrows to one of the dashboard's groups, and combines with search and sort:

filter Customers who
overdue Have something overdue
outstanding Have an outstanding balance
has_credit Have available credit
missing_email Have nobody to email — see Who receives a statement
has_issues Have something wrong with their record

currency  narrows to one currency code, for customers who trade in several.