Architecture
What happens between an app calling us and Xero answering
The proxy is a transparent, one-to-one mirror of Xero’s API. Same paths, same query strings, same bodies. The only thing that changes on the way in is the credential, and the only thing added is a set of checks that happen before your request ever leaves the platform.
The life of a request
Resolve the key
The platform key from the Authorization header is looked up by its public key ID, then verified against its stored hash. No table scan, constant-time comparison.
Validate the tenant
The Xero-Tenant-Id header is checked against the key's allowed orgs. A single-org key can omit it and we fill it in; a multi-org key without it gets a 400 explaining why.
Check the scope
The method and path are matched against the route → required-scope table. The key must hold that scope, and the connection must still hold it too - we enforce the intersection, not just the key.
Swap the token
The connection's current Xero access token is attached and the request is forwarded, untouched, to Xero.
Pass the response through
Status code, body and headers come back verbatim, including Xero's own rate-limit headers, so callers can see their real budget.
The hard part
Token refresh under concurrent load
This is the part of a Xero proxy that breaks naive implementations. A single connection serving many downstream apps means an expired access token gets hit by all of them at once, not one at a time.
Single-flight, per connection
Dozens of downstream apps can hit an expired token in the same instant. Only one refresh call goes to Xero per connection; every other request waits on that result instead of racing it.
Refresh tokens rotate
Every refresh returns a new refresh token and invalidates the old one. A losing request that refreshed independently would kill the whole connection - the lock is what prevents that.
Proactive, not just reactive
Tokens are refreshed ahead of their ~30 minute expiry on a timer, so a live request hitting a stale token is the exception, not the normal path.
Health, tracked and surfaced
Each connection reports healthy, expiring, expired or revoked based on scope and last-refresh time, with a reconnect flow that keeps the same org identity and existing keys.
Enforcement
Deny by default
Every proxied endpoint is keyed on HTTP method and path pattern to a required Xero scope. A GET against an invoices endpoint needs a read scope; a POST against the same path needs the write scope. A route that isn’t in the table is refused, not silently allowed, and the miss is logged so the table can be extended.
Scope failures come back as a 403 that names the scope that was required and the scopes the key actually holds, because a developer integrating against this will hit it while they’re still working out their scope list.
Passthrough
What moves through untouched
- Query strings, request and response bodies, and Content-Type
- Accept: JSON, XML, and PDF for report and invoice endpoints
- If-Modified-Since, pagination and filters
- Idempotency-Key, which is forwarded and can also be honoured here
- Multipart uploads and binary downloads, streamed both ways
- Xero's rate-limit headers, including Retry-After on a 429
See how a key’s scope is actually enforced
Org scope, permission scope, and network scope, checked on every single request.
Review security