Error reference

Every error response has the same shape:

{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not have the required scope.",
    "request_id": "a1b2c3d4-e5f6-4789-0abc-def123456789"
  }
}
Field Use it for
code Branch on this. It is stable and will not change for a given condition.
message Showing a human. It gets reworded without notice.
request_id Quote it to support — it identifies the exact request in our logs.
details Present only on validation failures. See below.

Codes

Authentication and permissions

Code HTTP What happened What to do
invalid_token 401 The key is missing, malformed, or has been revoked. Check the Authorization: Bearer … header. If the key was revoked, create a new one. Do not retry.
insufficient_scope 403 The key is valid but lacks the permission this endpoint needs. Create a key with the right permissions. Do not retry.

Account state

Code HTTP What happened What to do
subscription_inactive 402 The organisation has no active subscription. The account owner needs to fix billing. Do not retry.
plan_too_low 402 The organisation is below the Pro plan. Upgrade. Do not retry.
organisation_unavailable 410 The organisation is being deleted. Stop. The key will not start working again.
xero_disconnected 409 Statey's connection to Xero has expired, so the data behind this key is frozen. Nothing to fix on your side. The account owner reconnects Xero in Statey, and your next call succeeds. Retry on your normal schedule.

Timing

Code HTTP What happened What to do
initial_sync_in_progress 409 The organisation has never finished its first sync from Xero, so there is no data yet. Retry later — minutes to hours, depending on size.

A Xero rate-limit pause is not an error. If your organisation exhausts its own Xero budget, Statey stops syncing until it recovers — but every endpoint keeps answering, because none of them call Xero. What changes is that your data stops being refreshed, so it is reported as a freshness fact rather than a failure: meta.data_freshness.sync_paused  is true  and retry_after  says how many seconds are left. GET /api/v1/sync_status  reports the same thing as "state": "paused" .

Limits

Code HTTP What happened What to do
request_throttled 429 Too many HTTP requests in the last minute. Honour Retry-After, then back off. See below.
quota_exceeded 429 The organisation's daily recipient allowance is used up. Stop sending until tomorrow, in your organisation's timezone. Retrying will not help.

Both are 429, and they mean different things. request_throttled  counts requests and clears within a minute. quota_exceeded  counts recipients queued and clears at your organisation's midnight. Branch on code , not on the status.

quota_exceeded  responses carry X-Quota-Limit  and X-Quota-Remaining , so you can see where you stand without sending a probe request.

The request itself

Code HTTP What happened What to do
not_found 404 No such record — or it belongs to another organisation. Check the id. Do not retry.
validation_failed 422 The request was understood but not acceptable. Read details, fix, resend.
idempotency_key_reused 409 An Idempotency-Key was reused with a different body. Use a new key for a new request. See Idempotency.

Running a schedule on demand

These only come back from POST /schedules/{id}/runs . Three codes rather than one, because the fix is different in each case.

Code HTTP What happened What to do
schedule_already_run_today 409 This schedule already produced a campaign today, whether from the timetable or from an earlier call. Wait until tomorrow. A second run today would send the same statement twice.
schedule_unrunnable 409 The schedule is misconfigured — usually the Xero contact group it sends to has been deleted. Fix the schedule in the app. Retrying will not help.
schedule_has_no_recipients 409 Nobody currently matches the schedule's filter, or nobody who matches has an email address. Check the filter and your contacts' email addresses.

Firing a schedule this way does not use up its next occurrence — it still runs on its own timetable as well.

On not_found : we return 404, not 403, when a record belongs to a different organisation. That is deliberate — a 403 would confirm the record exists, which tells you something about another Statey customer. You cannot distinguish "does not exist" from "not yours", and that is the point.

Validation failures

validation_failed  carries a details  array, one entry per problem:

{
  "error": {
    "code": "validation_failed",
    "message": "The request was not valid.",
    "request_id": "a1b2c3d4-e5f6-4789-0abc-def123456789",
    "details": [
      { "field": "contact_ids", "code": "invalid", "message": "is required" }
    ]
  }
}

field  and code  are for your code; message  is for a human.

Retrying

A rule of thumb:

Status Retry?
401, 403, 404, 410, 422 No. Nothing about resending will change the answer.
409 Read the code. initial_sync_in_progress and xero_disconnected clear on their own — retry later. schedule_already_run_today clears at your midnight. idempotency_key_reused, schedule_unrunnable and schedule_has_no_recipients need something changed first.
402 No — a person has to change the subscription.
429 Yes, after Retry-After. For quota_exceeded, not until tomorrow.
500 Yes, with exponential backoff. Tell us if it persists.

When you retry anything that sends, send an Idempotency-Key  so a retry cannot email someone twice. Two endpoints accept it — POST /statement_sends  and POST /schedules/{id}/runs . See Idempotency.

Every other write — notes, contact people, statement exports, webhook subscriptions — ignores the header, so a retry there creates a second record. None of them send email, so the cost is a duplicate rather than a second statement in a customer's inbox, but check before you retry blindly.