Xero API simulator
Hosted, read-only drop-in for the Xero Accounting API. Point OAuth + Accounting base URLs at the simulator instead of api.xero.com / identity.xero.com and exercise a real client against seeded orgs, nested JSON, pagination, attachments, and Xero-shaped errors — without a live Xero organisation or partner app review.
Plan: Professional (and Enterprise). Extra feature of the Pro-level service — not on Lite or Standard.
Limits: Soft caps match Xero (60/min, 5000/day, ~1 GiB egress/day) and return Xero-shaped 429 responses. Customers reset soft counters from the portal dashboard to keep training clients. Hard caps (~10×) block thrashing and need staff reset.
Public overview (no credentials): /sim/. Guide slug: this page.
Who it is for
- CI / local dev for anything that talks to Xero Accounting (Excel connectors, ETL, agents)
- Proving pagination,
If-Modified-Since, 429 backoff, and attachment downloads - Onboarding engineers before they get Xero demo-company access
If you want flat Excel / OData tables rather than raw Xero JSON, that is the main x0 product — this simulator is the Xero-shaped sandbox used to build and test it.
How to get access
- Be on x0 Professional (or Enterprise).
- Request a simulator OAuth client + seeded tenant:
- Portal contact or email support@x0.co.nz
- Include your redirect URI and what you are testing
- Staff provision credentials (not published on
/sim/or in docs) — or the customer enables the sim from Dashboard → Xero API simulator.
Gated by option xero_api_simulator (PricingTierOption + seed_options).
Soft vs hard limits
| Cap | Soft (training) | Hard (~10×) |
|---|---|---|
| Calls / minute | 60 | 600 |
| Calls / day | 5 000 | 50 000 |
| Egress / day | ~1 GiB | ~10 GiB |
Soft limit → 429 with Xero-shaped JSON + X-DayLimit-Remaining / X-MinLimit-Remaining. Dashboard Reset soft limits clears counters. Hard limit → same 429 shape but reset requires staff (Mission Control → Ops → Xero Sim).
Base URL
Public host (example): see /sim/ for the live hostname (often https://sim.dev.x0.co.nz).
| Endpoint | Path |
|---|---|
| Health | GET /_sim/health |
| OpenAPI | /docs |
| Authorize | /identity/connect/authorize |
| Token | /connect/token |
| Connections | /connections |
| Accounting | /api.xro/2.0/… |
Point a Xero client at the sim
Override the four Xero hosts (names vary by SDK):
XERO_API_BASE_URL=https://<sim-host>/api.xro/2.0
XERO_CONNECTIONS_URL=https://<sim-host>/connections
XERO_TOKEN_URL=https://<sim-host>/connect/token
XERO_AUTHORIZE_URL=https://<sim-host>/identity/connect/authorize
OAuth2 authorization code + refresh; client auth is HTTP Basic (client_id:client_secret) or form fields. Accounting calls need Authorization: Bearer … and xero-tenant-id: <tenant guid>.
Writes return 403 (read-only v1).
Behaviour you can rely on
- Pagination —
page/pageSize(max 100). Journals useoffset. - If-Modified-Since — supported on most collections.
- Dates — Xero
/Date(milliseconds)/strings, not ISO. - Attachments — list + binary by id or filename where seeded.
- History —
/{Resource}/{id}/Historyon selected resources. - Rate limits — 60 / minute and 5000 / day per tenant.
Headers:X-DayLimit-Remaining,X-MinLimit-Remaining. Over limit →429+Retry-After. - Errors — Xero-shaped JSON (
Type,Message,Detail, …).
Not a Xero product and not affiliated with Xero Limited. Surface compatibility is intentional; data is synthetic.
Seed
python manage.py migrate
python manage.py seed_options
python manage.py seed_pricing_tiers
Overnight / customer-dev data volume
Never point overnight jobs or customer-dev scenario tests at live Xero. Use seed_xerosim (synthetic, linked, deterministic) or import_xerosim_from_api (a dev customer's own real x0 cache, replayed locally) instead — both write straight into the simulator's Postgres, no Xero API calls, no quota burned.
seed_xerosim — synthetic data, volume knobs
# Small/default dataset (fast, good for CI): contacts=120 invoices=15 journals=250 items=1
.venv/bin/python manage.py seed_xerosim --reset
# Explicit volume knobs
.venv/bin/python manage.py seed_xerosim --reset --contacts 500 --invoices 300 --journals 100 --items 50
# Stress-test shorthand: contacts=1000 invoices=1000 journals=500 items=200
.venv/bin/python manage.py seed_xerosim --reset --volume
# --volume + an explicit flag: the explicit flag wins for that field only
.venv/bin/python manage.py seed_xerosim --reset --volume --contacts 50
| Flag | Default | With --volume |
|---|---|---|
--contacts |
120 | 1000 |
--invoices |
15 | 1000 |
--journals |
250 | 500 |
--items |
1 | 200 |
Notes on the generated data:
- Stable linkage — every child/line-item id (invoice
LineItemID, journalJournalLineID, credit note/PO/quote line ids,TrackingCategoriesoption ids, …) is auuid5derived from its parent's own id plus an index, not a randomuuid4. Re-running the seed produces byte-identical child ids, so scenario fixtures and snapshot tests stay stable across reseeds. - Bogon/edge-case strings — a handful of contacts and invoices are deliberately seeded with quotes, ampersands, JSON-looking text, embedded newlines/tabs, and emoji/unicode (see
BOGON_STRINGSinseed_xerosim.py). This exerciseschar_sanitizeand JSON (de)serialization the same way real-world messy Xero data would, without needing a live org to find those bugs.
import_xerosim_from_api — replay a real x0 cache locally
A dev customer (or staff) can pull their own already-synced x0 cache from api.x0.co.nz (or api.dev.x0.co.nz) using their normal Bearer API key, and load it straight into a simulator org. This gives you realistic, customer-shaped linked data for local scenario testing — again, without ever calling Xero.
# Pull a customer's own cache and load it into a fresh simulator org
.venv/bin/python manage.py import_xerosim_from_api \
--api-base https://api.x0.co.nz \
--token x0_live_XXXXXXXXXXXX.secret \
--tables Contacts,Invoices,Accounts,Items \
--limit 500
# Re-use a JSON export instead (fully offline, no network at all)
curl -H "Authorization: Bearer x0_live_..." https://api.x0.co.nz/odata/v1/Contacts > contacts.json
.venv/bin/python manage.py import_xerosim_from_api --from-json contacts.json --tables Contacts
| Flag | Purpose |
|---|---|
--api-base |
x0 API base URL. Env fallback X0_API_BASE_URL or API_BASE_URL. |
--token |
Bearer token (x0_live_…/x0_test_… key). Env fallback X0_IMPORT_API_TOKEN. |
--tables |
Comma list of OData entity sets, e.g. Contacts,Invoices,Accounts,Items |
--org-id / --org-name |
Target SimOrganisation — existing pk, or get-or-create by name (default Imported Dev Cache) |
--limit / --page-size |
Rows per table / page size ($top) |
--tenant-id |
Optional ?tenant_id= forwarded upstream if the customer has multiple tenants |
--from-json |
Skip HTTP entirely and import a saved OData JSON export |
Pagination follows x0's own $top/$skip query params, but also follows a standards-compliant @odata.nextLink if a response includes one. Rows are upserted into SimResource keyed by (organisation, resource_type, resource_id) using each row's own GUID field (AccountID, ContactID, InvoiceID, …) when present, so re-running the import is idempotent — no duplicate rows.
Staff / x0-dev ops
Internal runbook (IPs, systemd, separate Postgres). Customers use /sim/ + support for credentials.
cd ~/internal/x0/shim
.venv/bin/python manage.py migrate
.venv/bin/python manage.py seed_xerosim --reset
.venv/bin/uvicorn xerosim.main:app --host 10.1.40.12 --port 8050
# or: systemctl --user start x0-dev-xerosim
Point x0 itself at the sim via XEROSIM_BASE_URL / XERO_*_URL in shim/.env, then manage.py rewrite_xero_endpoint_urls. Mission Control → Ops → Xero Sim. Optional DB: XEROSIM_DATABASE_URL. Tests: pytest tests/test_xerosim.py tests/test_import_xerosim_from_api.py -q.
Related
- Documentation index · Glossary
- pricing.md · customer-options.md
- ai-agents.md · odata.md · yaak.md
- Portal:
/sim/· Pricing:/pricing/