Developer documentation
Errors
How to tell our refusal from Xero's, and what every proxy error code means.
Proxy errors versus Xero errors
Any response the proxy generated itself carries X-Proxy-Error: true and a stable error.code. Without that header, the response came from Xero and is exactly what Xero returned.
{
"error": {
"code": "scope_denied",
"message": "This request requires the 'accounting.transactions' scope, which this key does not hold for this organisation.",
"requiredScope": "accounting.transactions",
"grantedScopes": ["accounting.transactions.read", "accounting.contacts.read"],
"documentation": "/developers/errors",
"requestId": "4f2c1e8a-9b3d-4c7e-a1f6-2d8b5e0c9a34"
}
}requestId is on every refusal and on every success as X-XeroProxy-Request-Id. Log it: whoever administers the account can look that exact request up and see which gate produced this. See [diagnosing a request](/developers/diagnostics).
Error codes
| Code | Status | Cause | Fix |
|---|---|---|---|
| missing_credential | 401 | No Authorization header, or it was not a Bearer credential. | Send Authorization: Bearer xpk_live_<keyid>_<secret>. |
| invalid_key | 401 | The key ID is unknown or the secret did not verify. | Check the credential was copied whole. Unknown key and wrong secret are deliberately indistinguishable. |
| key_revoked | 401 | The key was revoked. This takes effect on the very next request. | Mint a replacement key. |
| key_expired | 401 | The key passed its expiry, or its post-rotation grace window closed. | Use the replacement key issued at rotation. |
| account_suspended | 403 | The owning account is suspended. | Contact the account owner. |
| ip_not_allowed | 403 | The key has a network allowlist that does not include the calling address. | Add the address to the key's allowlist, or call from an allowed range. |
| tenant_required | 400 | The key spans multiple organisations and no Xero-Tenant-Id header was sent. | Send Xero-Tenant-Id. The response lists which tenant IDs this key may reach. |
| tenant_not_allowed | 403 | The requested tenant is not one this key may reach. | Use a tenant from the key's own set. GET /connections returns exactly that set. |
| no_orgs | 403 | The key is not scoped to any active organisation. | Add an organisation to the key, or reconnect the Xero organisation. |
| scope_denied | 403 | The endpoint requires a scope the key does not hold for this organisation. The response names the required scope and lists what the key holds. | Grant the scope on the key, and make sure the connection itself still holds it: the effective permission is the intersection of the two. |
| route_not_mapped | 403 | The method and path are not in the route table. Unmapped routes are denied rather than forwarded. | Check the path against the route reference. If a legitimate Xero endpoint is missing, it needs adding to the table. |
| connection_unavailable | 502 | The Xero connection behind this organisation is expired or disconnected. | Reconnect the organisation. Existing keys and org identity are preserved. |
| upstream_unreachable | 502 | Xero could not be reached. | Retry with backoff. |
| rate_limited | 429 | The key is over its own per-minute rate limit. The allowance refills continuously, so this clears on its own. | Honour Retry-After. X-XeroProxy-RateLimit-Remaining shows the budget on every response, so you can pace ahead of it. |
| quota_exhausted | 429 | The key has spent its daily quota. Quotas reset at midnight UTC. | Wait for the reset, raise the key's quota, or cut the call count: reads served from the cache spend no quota at all. |
| budget_reserved | 429 | The organisation is near its Xero daily limit and this key's priority stops above the remaining budget, so what is left stays available for higher-priority work. | Retry later, or have the account raise the key's priority. This is the account's own policy, not a limit Xero applied. |
| idempotency_key_invalid | 400 | The Idempotency-Key header was empty, longer than 255 characters, or contained a space or a non-printable character. | Send a UUID, a ULID or any other printable ASCII token with no spaces. |
| idempotency_conflict | 422 | This Idempotency-Key was already used for a different request. The response names the method and path it was first used for. | Generate a fresh key for each distinct write and reuse it only when retrying that same write. This refusal is protecting you: answering with the first request's response would have been silently wrong. |
| idempotency_in_progress | 409 | An earlier request with this Idempotency-Key is still being processed. Sending it again is exactly the duplicate write the key exists to prevent. | Honour Retry-After. The retry receives the original request's result once it lands. |
| idempotency_body_too_large | 413 | A request sent with an Idempotency-Key has to be read whole to be fingerprinted, and this one is over the account's limit. | Send it without the header to stream it through unprotected, or raise the limit on the write safety page. Attachments and file uploads are the usual case. |
| deferred_not_found | 404 | No deferred request with that id was created by this API key. A job belonging to another key reads the same way, deliberately. | Use the id from the 202's X-XeroProxy-Deferred header, with the same credential that received it. GET /deferred lists everything this key has queued. |
| deferred_failed | 502 | The work was accepted and could not be carried out: Xero stayed unreachable, or a key, scope or connection it depended on was withdrawn after it was queued. | Read the failure field for which of those it was. A withdrawn permission needs fixing before the request is worth making again; an unreachable Xero usually does not. |
| deferred_expired | 410 | The request was accepted but the budget never freed up within the account's deferral window, so it was given up on. | A queue that expires work regularly is a quota that is too small for the traffic, not a scheduling problem. Raise the window, raise the limits, or send less. Re-sending a write needs a NEW Idempotency-Key: it is a new decision now. |
| deferred_cancelled | 410 | The job was cancelled with DELETE /deferred/<id> before it was carried out. | Nothing to fix. Send the request again if it is still wanted. |
| callback_token_invalid | 400 | X-XeroProxy-Callback-Token was empty, longer than 200 characters, or contained bytes that are not printable ASCII. | Send a short opaque identifier of your own choosing (your job reference, a uuid). It is echoed back on the completion callback and is not a credential. Checked on every request that carries it, not only ones that end up queued, so you find out now rather than when the callback arrives with nothing to match. |