Data freshness — why a statement can come back empty

Statey does not read Xero live. It keeps its own copy of your customers, invoices and balances, and refreshes that copy throughout the day. Everything the API returns comes from that copy.

Most of the time this is invisible. It matters in one case: a statement generated while a refresh is running can legitimately come back blank. Not an error, not a failure — an empty statement, for a customer who definitely owes you money.

This guide is about noticing that and not acting on it.

The freshness block

Every successful read carries one, as does every send:

"meta": {
  "data_freshness": {
    "last_completed_at": "2026-09-17T04:12:33Z",
    "rebuild_in_progress": false,
    "sync_paused": false,
    "retry_after": null
  }
}
Field Meaning
last_completed_at When a refresh last finished completely
rebuild_in_progress Whether one is running right now
sync_paused Whether Xero's rate limits have paused refreshing altogether
retry_after Seconds until that pause lifts, or null when nothing is paused

A person using Statey sees a banner while this is happening. Your script cannot, which is why the block rides along with the data instead of needing a second call.

Three things do not carry it, so do not read meta  unconditionally: error responses, the contact-people writes (POST , PATCH  and DELETE  on /contacts/{id}/people ), and any 204 No Content .

What to do about it

For reads — balances, contact lists, campaign history — carry on. rebuild_in_progress  being true  is normal. Refreshes run through the day, and waiting for a quiet moment would mean waiting a long time. The figures you get are the last completed ones.

For statements and sends, stop and think. These are built from per-contact cached calculations that are rebuilt one contact at a time. Partway through a rebuild, some contacts have a finished calculation and some do not — and a contact without one produces an empty statement.

So:

You are about to Do this
Read balances or lists Proceed
Generate a statement document Prefer rebuild_in_progress: false
Send statements to customers Wait for rebuild_in_progress: false

Sending is the one that really matters. A blank statement you downloaded is a wasted call; a blank statement is an email your customer receives asking them to pay nothing.

Waiting properly

Do not poll in a loop. Subscribe to organisation.data_rebuilt  and act when it arrives:

{
  "event": "organisation.data_rebuilt",
  "data": {
    "outcome": "completed",
    "contacts_total": 912,
    "contacts_rebuilt": 909
  }
}

Check outcome , and check the counts. The event fires whenever a refresh finishes, successfully or not — so completed  with 909 of 912 rebuilt is fine to send on, while failed  with 4 of 912 is not.

If you would rather poll, use GET /api/v1/sync_status  — it is the long form of the same information:

{
  "sync_status": {
    "state": "idle",
    "rebuild_in_progress": false,
    "last_completed_at": "2026-09-17T04:12:33Z",
    "last_outcome": "completed",
    "last_xero_sync_at": "2026-09-17T04:01:11Z",
    "last_caches_built_at": "2026-09-17T04:12:31Z",
    "retry_after": null
  }
}
state What it means
idle Ready. Safe to send
rebuilding A refresh is running. Reads are fine; hold off sending
paused Xero's rate limits have forced a pause. retry_after says for how long, in seconds
never_synced The organisation has never finished a first sync. There is no data yet

A pause does not take the API away. No endpoint here calls Xero, so a Xero rate-limit pause freezes your data without making anything unavailable — every endpoint keeps answering, and every read carries sync_paused  and retry_after  in its freshness block. /sync_status  is the fuller version of the same fact, not a lifeline during an outage.

Timing

Refreshes are more frequent and lighter during the day, with a fuller rebuild overnight. The busiest period is the start of a month, when many organisations rebuild at once and a refresh can take noticeably longer than usual.

If you run a monthly statement job, the safe shape is: subscribe to organisation.data_rebuilt , and send when it arrives — rather than picking a fixed time and hoping the data is ready.

A worked example

1. Your job wakes on the 1st.
2. GET /api/v1/sync_status
   → { "state": "rebuilding", "rebuild_in_progress": true }
3. Do not send. Wait for the organisation.data_rebuilt webhook.
4. Webhook arrives: outcome "completed", 909 of 912 rebuilt.
5. POST /api/v1/statement_sends with an Idempotency-Key.

Step 3 is the one people skip. It is the difference between a clean statement run and a mailbox full of replies asking why the statement was blank.