verification
use it
# add to stunt.yaml
services:
xero:
adapter: embedded:xero-style
# then
$ stunt upThe embedded: source extracts this adapter from the binary — no git clone, fully offline.
how to mock the Xero Accounting API locally
Install stunt, paste the service block above into stunt.yaml, run stunt up — then point your client at localhost instead of Xero Accounting API. It works offline, keeps state across requests and restarts, and every response is synthetic: the route shapes, status codes, pagination, and webhook delivery follow the real Xero Accounting API 2.0 surface — the data is fake, the behavior is real.
backing stores
contactsinvoicesaccounts
api surface (15)
generated from the adapter manifest — exactly what boots, nothing more · covers 4% of the real API's 235 routes (derived from spec Xero-OpenAPI xero_accounting @ 17.0.0)
15 routes
GET/connections
GET/api.xro/2.0/Contacts
PUT/api.xro/2.0/Contacts
GET/api.xro/2.0/Contacts/{id}
PUT/api.xro/2.0/Contacts/{id}
GET/api.xro/2.0/Invoices
PUT/api.xro/2.0/Invoices
GET/api.xro/2.0/Invoices/{id}
DELETE/api.xro/2.0/Invoices/{id}
POST/api.xro/2.0/Invoices/{id}/Payments
GET/api.xro/2.0/Accounts
GET/api.xro/2.0/BankTransactions
GET/api.xro/2.0/Items
GET/api.xro/2.0/TrackingCategories
POST/webhooks
VM-verified behaviors (29)
- ✓a missing or unknown bearer gets the same 401 TokenExpired envelope; the seeded static token passes
- ✓GET /connections needs only the bearer and lists the two demo tenants
- ✓api.xro calls without xero-tenant-id are 400 TenantRequired, and the header matches case-insensitively
- ✓the seeded static token expires on the virtual clock, and the bearer gate outranks the tenant gate
- ✓PUT /Invoices sums every line into the totals and echoes each computed LineAmount
- ✓a single-object body creates one invoice with clock-derived Date and DueDate defaults
- ✓GET /Invoices/{id} round-trips the stored invoice; unknown ids are the 404 envelope
- ✓payments accumulate on the ledger and are echoed with their own PaymentID
- ✓a negative amount is a refund bounded by what was paid
- ✓over-payment (and an over-refund) is Xero's validation error and leaves the ledger untouched
- ✓the invoice flips to PAID exactly at a zero balance and then refuses further payments
- ✓DRAFT invoices are not payable and unknown ids are the 404 envelope
- ✓DELETE voids to 204 and the record survives with Status VOIDED and a zeroed balance
- ✓the voided invoice stays listable via the Statuses filter
- ✓re-voiding, voiding a PAID invoice, and unknown ids are rejected
- ✓PUT /Contacts creates with Xero defaults and reads back by id
- ✓the Contacts array upserts by ContactID and ContactNumber, merging over the stored record
- ✓duplicate active names are Xero's real validation error, in Elements
- ✓archive is an update: the contact stays readable and releases its name
- ✓invalid statuses and unknown ids are the plain ValidationError / NotFound envelopes
- ✓where ANDs clauses in Xero's grammar, and Statuses/InvoiceNumber/ContactID filter the invoice list
- ✓order sorts by any field in both directions (dates sort as instants)
- ✓contacts search matches name and email case-blind, while where Name.Contains is case-sensitive
- ✓the seeded chart of accounts and the static catalogs speak the same grammar
- ✓page/pageSize walk every contact exactly once, with nextPage only between pages
- ✓without pageSize paging is off; a page past the end is an empty 200 page
- ✓a malformed page falls back to page 1 (as-is: no 400)
- ✓a delivery signed with base64(HMAC-SHA256(key, raw bytes)) is accepted
- ✓missing, tampered, and wrong-key signatures all answer the 401 envelope Xero requires
Not implemented (6)
- No Credit Notes, Quotes, or standalone Payments endpoints
- No Journals, Bank Transfers, Employees, or Expense Claims
- No Organisations, Currencies, or TaxRates endpoints
- Accounts, Items, TrackingCategories, BankTransactions are read-only (GET only)
- No attachments or history/notes endpoints
- No invoice update, email, or repeating-invoice endpoints
Differs from the real API (5)
- No tax-rate registry; line tax is the given TaxAmount, not computed from TaxType
- Responses carry a sim-only nextPage field; real Xero signals the last page by item count
- GET /connections wraps entries in {connections:[...]} with tenantName; real Xero returns a bare array without it
- Malformed page falls back to page 1; real Xero answers 400
- nextPage envelope field is a simulator extension; 401 ErrorNumber is the string TokenExpired, not numeric
want more of the Xero Accounting API surface? adapters are YAML + Starlark — the authoring guide covers adding routes, fixtures, and state machines. Open an issue for anything big first, or just file a PR — merged routes ship in the next release.