Getting started with the Statey API

The Statey API lets your own systems read your Xero-backed customer data, generate statements and send them — without anyone signing in.

This guide takes you from nothing to your first successful request.

Before you start

You need:

  • A Statey organisation on the Pro plan. The API is not available on lower plans.
  • Permission to administer that organisation — you are its owner, or a Xero user with Standard or Financial Adviser access.

1. Create an API key

The API screen in Statey, under Account then API

  1. Sign in to Statey and go to Account → API.

  2. Choose New key.

  3. Give it a label you will recognise later — the name of the system that will use it, not "key 1".

  4. Choose its permissions:

    Permission What it allows
    read Read contacts, balances, statements and send history
    write Change contact settings, such as excluding a contact from scheduled statements
    send Send statements to your customers

    Grant only what the system needs. A reporting tool needs read and nothing else.

  5. Decide whether you want a test key. A test key does everything a real one does except deliver the email, so you can build an integration without contacting your customers. See Test keys.

  6. Choose Create key.

The New key form, showing the label, the read, write and send permissions, and the test key option

Copy the key now. It is shown once. We store only a scrambled copy, so we cannot show it to you again — if you lose it, create a new key and revoke the old one.

The new key shown once, with a Copy button and a warning to store it safely

Keys look like this, with test keys carrying their own prefix:

stky_kZq3n8VYc2sL5pR1tW7xJb4hD6gF9mA0eU        ← live
stky_test_kZq3n8VYc2sL5pR1tW7xJb4hD6gF9mA0eU   ← test

Treat it like a password. Anyone holding it can do whatever its permissions allow, to your real customer data. Give each system its own key, so you can revoke one without disrupting the others.

Call this API from your server, never from a browser

The Statey API is server-to-server. Your backend holds the key and calls us; your users' browsers never do.

Putting a key into JavaScript — a single-page app, a website widget, a mobile app bundle — exposes it to anyone who opens developer tools or unpacks the bundle. They then hold a credential that can read your whole debtor book and email your customers.

A browser will not let this work in any case: we send no CORS headers at all, so a browser discards our response no matter which origin the page came from. Do not rely on that as your protection, though — the key is still exposed to anyone reading your JavaScript, and a non-browser client ignores CORS entirely.

If you need this data in a browser, fetch it on your server and pass through only what that page actually needs.

2. Make your first request

Send the key as a bearer token:

curl https://app.statey.app/api/v1/organisation \
  -H "Authorization: Bearer stky_your_key_here"

A successful response confirms which organisation the key belongs to and what it may do:

{
  "organisation": {
    "id": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
    "name": "Northwind Trading",
    "base_currency": "NZD",
    "timezone": "Pacific/Auckland"
  },
  "api_key": {
    "label": "Warehouse system",
    "scopes": ["read"],
    "sandbox": false
  },
  "meta": {
    "data_freshness": {
      "last_completed_at": "2026-09-17T04:12:33Z",
      "rebuild_in_progress": false
    }
  }
}

This endpoint is the one to call when you want to check a key works. It needs no permissions beyond read  and touches nothing.

timezone  is worth noting: daily allowances and schedules roll over at your organisation's midnight, not UTC.

3. Fetch some contacts

curl "https://app.statey.app/api/v1/contacts?filter=overdue" \
  -H "Authorization: Bearer stky_your_key_here"
{
  "contacts": [
    {
      "id": "9f8e7d6c-5b4a-4392-8172-6a5b4c3d2e1f",
      "name": "Acme Supplies",
      "currency": "NZD",
      "account_number": "ACM-014",
      "balance_due": "4820.00",
      "amount_overdue": "1200.00",
      "available_credit": "0.00",
      "most_days_overdue": 47,
      "oldest_due_date": "2026-08-01",
      "last_invoice_date": "2026-09-09",
      "missing_email": false,
      "total_issues": 0
    }
  ],
  "meta": {
    "page": 1,
    "page_size": 50,
    "total_pages": 12,
    "total_count": 573,
    "data_freshness": {
      "last_completed_at": "2026-09-17T04:12:33Z",
      "rebuild_in_progress": false
    }
  }
}

Two things to know about this response:

  • Money comes back as a string, not a JSON number. A float cannot hold a decimal balance exactly, and silently losing cents on a debtor balance is not acceptable. Parse it with a decimal type.
  • A contact appears once per currency. If Acme trades with you in NZD and AUD, you get two entries with the same id  and different currency . Key your own records on the pair, not on id  alone. Add ?currency=NZD  to narrow it.

The list does not include email addresses — fetching those needs a join, so they live on the individual contact endpoint: GET /api/v1/contacts/{id} .

Available filters: overdue , outstanding , has_credit , missing_email , has_issues . An unrecognised filter is rejected rather than ignored — returning the unfiltered list would hand you the wrong data with no way to notice.

4. Page through results

Collections are paged with page  and page_size . meta.total_pages  tells you how many there are:

curl "https://app.statey.app/api/v1/contacts?page=2&page_size=100" \
  -H "Authorization: Bearer stky_your_key_here"

page_size  defaults to 50 and is capped at 200.

You can also search  by name or account number, and sort  by name or by any of the balance columns.

One thing to know before you page through everything: Statey recalculates balances in the background, roughly hourly, and a recalculation can move rows between pages while you are reading them. Sorting by xero_name  avoids it, because names do not change when balances do. Searching, sorting and paging explains this properly and is worth ten minutes before you write an export.

5. Check the data is fresh

Every read carries meta.data_freshness :

"data_freshness": {
  "last_completed_at": "2026-09-17T04:12:33Z",
  "rebuild_in_progress": true
}

Statey refreshes from Xero throughout the day. While a refresh is running, figures are still returned — they are simply the last completed ones. A person sees a banner telling them this; your script cannot, which is what this block is for.

rebuild_in_progress  being true  is not an error and not a reason to stop. If you need a consistent snapshot — an end-of-month report, say — wait for it to be false , or subscribe to the organisation.data_rebuilt  webhook instead of polling.

Rotating and revoking keys

There is no rotation in place. To roll a key:

  1. Create a new key with the same permissions.
  2. Move your system across to it.
  3. Revoke the old one.

Revoking takes effect immediately — the next request using that key is rejected. Revoked keys stay listed so your audit trail still makes sense.

You can revoke a key on any plan, including after a downgrade. Turning off a leaked credential is never blocked by billing.

What to expect when something is wrong

Errors carry a stable machine-readable code :

{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not have the required scope.",
    "request_id": "a1b2c3d4-e5f6-4789-0abc-def123456789"
  }
}

Branch on code , never on message . Messages get reworded; codes do not. Quote request_id  when you contact support — it identifies the exact request in our logs.

See the error reference for the full list.

Next steps

  • Test keys — build and test without emailing your customers
  • Searching, sorting and paging — read before exporting your whole customer list
  • Who receives a statement — the two address lists, and how to change which is used
  • Idempotency — how to retry a send safely
  • Rate limits and quotas — the ceilings and how to back off
  • Webhooks — get told when something happens instead of polling
  • Async sends — the request-then-poll pattern for statement documents