Who receives a statement

Every contact has two possible address lists, and exactly one of them is used. Getting this wrong is the one mistake in this API that stops statements arriving without anything looking broken, so it is worth five minutes before you write any code.

The two lists

List Where it comes from Survives a sync from Xero?
Xero The contact's email address and contact people in Xero No — replaced every time we sync
Statey Addresses added in Statey, or through this API Yes

GET /api/v1/contacts/{id}  shows you both, and tells you which one is live:

{
  "contact": {
    "id": "9f8e7d6c-...",
    "name": "Acme Ltd",
    "recipient_source": "xero",
    "email_addresses": ["ap@acme.test"],
    "contact_people": {
      "xero":   [{ "first_name": "Ann", "email_address": "ap@acme.test", "included": true, "primary": true }],
      "statey": [{ "id": "b3f1...", "first_name": "Ada", "last_name": "Lovelace", "email_address": "ada@acme.test" }]
    }
  }
}
  • recipient_source  is which list is live — "xero"  or "statey" .
  • email_addresses  is who a statement would actually go to right now. It is always the live list, resolved.
  • contact_people  shows both lists in full, so you can see what you would be switching to.

Xero's list includes people marked not to receive email in Xero. They are flagged with "included": false  rather than hidden, because if you are comparing the two lists you need to see everything that is there.

You cannot write to Xero's list

Only the Statey list is writable here. Xero's is replaced on every sync, so anything written to it would disappear without warning — usually within the hour. Manage those addresses in Xero.

Adding addresses never changes who gets emailed

This is the part worth reading twice.

POST /api/v1/contacts/{id}/people  adds to the Statey list. If the contact's recipient_source  is "xero"  — which is the default — then adding an address changes nothing about who receives a statement. The address sits there, unused, until you switch the source.

curl -X POST https://app.statey.app/api/v1/contacts/{id}/people \
  -H "Authorization: Bearer stky_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"first_name": "Ada", "last_name": "Lovelace", "email_address": "ada@acme.test"}'

PATCH  and DELETE /api/v1/contacts/{id}/people/{person_id}  edit and remove them. All three need the write  permission.

This separation is deliberate. If adding an address also switched the source, a contact with three Xero addresses would drop to emailing one person the moment you added your first Statey address — and nothing would tell you that two people had stopped receiving statements.

Switching the source is its own call

curl -X PATCH https://app.statey.app/api/v1/contacts/{id} \
  -H "Authorization: Bearer stky_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"recipient_source": "statey"}'

Switching copies nothing. The Statey list is whatever you have put in it. If it is empty and you switch to it, that contact now emails nobody.

The response shows you the consequence immediately — check it:

{ "contact": { "recipient_source": "statey", "email_addresses": [] } }

An empty email_addresses  after a switch means you have just silenced that contact. Switch back, or add addresses, before your next send.

A safe order of operations

  1. GET /api/v1/contacts/{id}  — read contact_people.xero  and note who is there.
  2. POST .../people  for each address you want on the Statey list.
  3. GET /api/v1/contacts/{id}  again — check contact_people.statey  is what you intended.
  4. PATCH /api/v1/contacts/{id}  with recipient_source: "statey" .
  5. Check email_addresses  in the response is not empty.

Then confirm with a dry run before a real send:

curl -X POST https://app.statey.app/api/v1/statement_sends/preview \
  -H "Authorization: Bearer stky_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"contact_ids": ["9f8e7d6c-..."]}'

A contact that now emails nobody comes back under skipped  with the reason no_email_address . See Asynchronous work.

Finding contacts that email nobody

GET /api/v1/contacts?filter=missing_email  lists contacts whose live list is empty, whichever source they are on. Worth running after any bulk change to recipients.

There is a short delay after a write before this filter reflects it — the underlying figures are recalculated in the background. See Data freshness.