Xero Proxy

Developer documentation

Errors

How to tell our refusal from Xero's, and what every proxy error code means.

View as markdown

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

CodeStatusCauseFix
missing_credential401No Authorization header, or it was not a Bearer credential.Send Authorization: Bearer xpk_live_<keyid>_<secret>.
invalid_key401The 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_revoked401The key was revoked. This takes effect on the very next request.Mint a replacement key.
key_expired401The key passed its expiry, or its post-rotation grace window closed.Use the replacement key issued at rotation.
account_suspended403The owning account is suspended.Contact the account owner.
ip_not_allowed403The 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_required400The 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_allowed403The 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_orgs403The key is not scoped to any active organisation.Add an organisation to the key, or reconnect the Xero organisation.
scope_denied403The 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_mapped403The 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_unavailable502The Xero connection behind this organisation is expired or disconnected.Reconnect the organisation. Existing keys and org identity are preserved.
upstream_unreachable502Xero could not be reached.Retry with backoff.
rate_limited429The 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_exhausted429The 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_reserved429The 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_invalid400The 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_conflict422This 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_progress409An 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_large413A 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_found404No 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_failed502The 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_expired410The 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_cancelled410The 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_invalid400X-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.