x0 Home Xero API simulator

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

  1. Be on x0 Professional (or Enterprise).
  2. Request a simulator OAuth client + seeded tenant:
  3. Portal contact or email support@x0.co.nz
  4. Include your redirect URI and what you are testing
  5. 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

  • Paginationpage / pageSize (max 100). Journals use offset.
  • 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}/History on 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, journal JournalLineID, credit note/PO/quote line ids, TrackingCategories option ids, …) is a uuid5 derived from its parent's own id plus an index, not a random uuid4. 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_STRINGS in seed_xerosim.py). This exercises char_sanitize and 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.

Sign in to see connection status