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.