x0 OData API (Excel native)
The x0 API exposes an OData v4 service for Excel, Power BI, and other Microsoft clients.
Service root
https://api.x0.co.nz/odata/v1
| Endpoint | Purpose | Upstream docs |
|---|---|---|
GET /odata/v1 |
Service document (lists entity sets) | |
GET /odata/v1/$metadata |
CSDL metadata for Excel schema discovery | |
GET /odata/v1/X0 |
x0 connection card — customer, tenant, key scopes, docs URLs | |
GET /odata/v1/Accounts |
Chart of accounts | Accounts |
GET /odata/v1/Accounts_Table |
Excel: accounts + validation-error rows | same |
GET /odata/v1/Settings |
Organisation settings | Organisation |
GET /odata/v1/Contacts |
Contacts | Contacts |
GET /odata/v1/Contacts_Table |
Excel: one row per contact address | same |
GET /odata/v1/Invoices |
Invoices (header) | Invoices |
GET /odata/v1/Invoices_Table |
Excel: one row per invoice line | same |
GET /odata/v1/Items |
Inventory / product items | Items |
GET /odata/v1/CreditNotes |
Credit notes (header) | Credit notes |
GET /odata/v1/CreditNotes_Table |
Excel: one row per credit-note line | same |
GET /odata/v1/PurchaseOrders |
Purchase orders (header) | Purchase orders |
GET /odata/v1/PurchaseOrders_Table |
Excel: one row per PO line | same |
GET /odata/v1/Quotes |
Sales quotes (header) | Quotes |
GET /odata/v1/Quotes_Table |
Excel: one row per quote line | same |
GET /odata/v1/RepeatingInvoices |
Repeating invoice templates | Repeating invoices |
GET /odata/v1/RepeatingInvoices_Table |
Excel: one row per repeating-invoice line | same |
GET /odata/v1/Overpayments |
Overpayments (header) | Overpayments |
GET /odata/v1/Overpayments_Table |
Excel: one row per overpayment line | same |
GET /odata/v1/Prepayments |
Prepayments (header) | Prepayments |
GET /odata/v1/Prepayments_Table |
Excel: one row per prepayment line | same |
GET /odata/v1/BatchPayments |
Payment batches | Batch payments |
GET /odata/v1/BatchPayments_Table |
Excel: one row per payment in a batch | same |
GET /odata/v1/LinkedTransactions |
Bill/invoice allocation links | Linked transactions |
GET /odata/v1/BankTransfers |
Inter-account bank transfers | Bank transfers |
GET /odata/v1/Budgets |
Budgets (lines expanded on sync) | Budgets |
GET /odata/v1/Budgets_Table |
Excel: one row per budget line | same |
GET /odata/v1/TaxRates |
Tax rates | Tax rates |
GET /odata/v1/TrackingCategories |
Tracking categories | Tracking |
GET /odata/v1/TrackingCategories_Table |
Excel: one row per tracking option | same |
GET /odata/v1/Currencies |
Organisation currencies | Currencies |
GET /odata/v1/BrandingThemes |
PDF/email branding themes | Branding themes |
GET /odata/v1/Users |
Organisation users | Users |
GET /odata/v1/InvoiceReminders |
Invoice reminder settings | Invoice reminders |
GET /odata/v1/ContactGroups |
Contact groups | Contact groups |
GET /odata/v1/ManualJournals |
Manual journals | Manual journals |
GET /odata/v1/ManualJournals_Table |
Excel: one row per manual-journal line | same |
GET /odata/v1/BankTransactions |
Bank transactions | Bank transactions |
GET /odata/v1/BankTransactions_Table |
Excel: one row per bank line | same |
GET /odata/v1/Payments |
Payments | Payments |
GET /odata/v1/Journals |
GL Journals raw (nested lines; tier-gated) | Journals |
GET /odata/v1/Journals_Table |
Excel: one row per journal line | same |
GET /odata/v1/Tenants |
Connected organisations | |
GET /odata/v1/Tables |
Catalog (includes DocsUrl per entity) |
The portal dashboard lists the same catalog from SourceEndpoint (seed: manage.py seed_sources). See it as an ER diagram: Data schema.
Legacy JSON: GET /v1/tables/{name} for entity cache tables (e.g. accounts, invoices).
Authentication
Any one of:
- Header:
Authorization: Bearer x0_live_<id>.<secret> - Header:
X-Api-Key: x0_live_<id>.<secret> - Query (Excel Web API):
?api_key=x0_live_<id>.<secret>
Also accepted as query names:X-Api-Key,ApiKey,apikey,key
Query-string keys appear in access logs / proxies — prefer Bearer when you control the client. Excel’s native Web API mode only knows how to send query params, so we support that on purpose.
Tenant selection
Accounts, Settings, Contacts, Invoices, and other source entity sets are tenant-specific. Provide one of:
- Query:
?tenant_id=<tenant-guid> - Header:
X-Xero-Tenant-Id: <xero-tenant-guid>
If omitted, the first connected tenant is used.
X0 also accepts an optional tenant (fills TenantId / ConnectedAt / LastSyncAt) but still returns a row if none is connected.
OData query options
Supported on all entity sets:
| Option | Example |
|---|---|
$filter |
Type eq 'REVENUE', contains(Name,'Sales') |
$select |
Code,Name,Type |
$orderby |
Code asc |
$top |
100 |
$skip |
50 |
$count |
true |
Example:
GET /odata/v1/Accounts?tenant_id=...&$filter=Status eq 'ACTIVE'&$select=Code,Name&$orderby=Code asc&$top=500
Xero-compatible query params (cache filter)
x0 also accepts Xero Accounting GET–style options on the same OData URLs. These filter the warm cache (no Xero call per request). See Xero HTTP GET.
| Style | Example | Effect |
|---|---|---|
| Path key | /odata/v1/Accounts/{AccountID} |
One row (404 if missing) |
| Singular / multi id | ?AccountID=… · ?ContactIDs=a,b · ?QuoteIDs=… · ?TaxType= (TaxRates) · ?Code= (Items/Currencies) |
Filter by PK (or ContactID / Code where supported) |
order= |
?order=EmailAddress%20DESC |
Same as $orderby |
summaryOnly= |
?summaryOnly=true |
Project to seeded slim field set |
If-Modified-Since |
Header (HTTP-date or yyyy-mm-ddThh:mm:ss[Z]) |
Rows with UpdatedDateUTC strictly after |
| Combine | Path/$filter/order/summaryOnly |
All applied; $select further narrows after summary |
Response headers when used: X-X0-Modified-Filter: applied, X-X0-Summary-Only: true.
Not in v1: full Xero where= expressions (timeout-prone). Prefer $filter.
Diagnostic: ?xero_pass=1 forwards supported params to live Xero for that request only (does not replace cache unless you also pass refresh=true). Quota-sensitive — not for Excel schedules.
Probe / refresh summary field seeds:
python manage.py probe_xero_summary_only --all
python manage.py probe_xero_summary_only --entity Accounts --customer-id N --print-python
Excel desktop — From Web + Bearer (recommended)
Excel’s Web API credential type appends ?api_key=… to the URL — it does not set an HTTP header.
It also often rejects our x0_live_<ulid>.<secret> keys with “An API key value wasn't specified.”
Use From Web instead:
- Data → Get Data → From Web → Advanced
- URL:
https://api.x0.co.nz/odata/v1/Accounts - Header:
Authorization=Bearer x0_live_….secret(full key afterBearer) - Credentials → Anonymous
Optional — OData.Feed with Bearer in the formula
let
Source = OData.Feed(
"https://api.x0.co.nz/odata/v1",
[#"Authorization" = "Bearer PASTE_FULL_KEY_HERE"],
[Implementation = "2.0"]
),
Accounts = Source{[Name = "Accounts", Signature = "table"]}[Data]
in
Accounts
Optional — Web API query param (ApiKeyName = "api_key")
let
Source = OData.Feed(
"https://api.x0.co.nz/odata/v1",
null,
[Implementation = "2.0", ApiKeyName = "api_key"]
),
Accounts = Source{[Name = "Accounts", Signature = "table"]}[Data]
in
Accounts
“Access to the resource is forbidden”
HTTP 403. Usual causes: missing scope xero.accounts.read, expired key, or no credential sent.
Confirm in Yaak: GET /odata/v1/Accounts + Bearer.
See excel-addin.md (guided Office.js wizard) or excel-desktop.md for the full checklist.
Scopes
Entity sets enforce API key scopes:
| Entity set | Scope |
|---|---|
| X0, Settings, Tenants, Tables | xero.settings.read |
| Accounts | xero.accounts.read |
| Items | xero.items.read (Xero OAuth: accounting.settings.read) |
| Contacts, Contacts_Table | xero.contacts.read |
| ContactGroups | xero.contactgroups.read |
| Invoices, Invoices_Table | xero.invoices.read |
| CreditNotes, CreditNotes_Table | xero.creditnotes.read (Xero OAuth: accounting.transactions.read) |
| PurchaseOrders, PurchaseOrders_Table | xero.purchaseorders.read (Xero OAuth: accounting.transactions.read) |
Quotes, Quotes_Table, RepeatingInvoices (+ _Table), Overpayments (+ _Table), Prepayments (+ _Table), BatchPayments (+ _Table), LinkedTransactions, BankTransfers |
matching xero.<table>.read (Xero OAuth: accounting.transactions.read) |
| Budgets, Budgets_Table | xero.budgets.read (Xero OAuth: accounting.budgets.read) |
| Attachments | xero.attachments.read (Xero OAuth: accounting.attachments.read) |
| Reports, Reports_Table | xero.reports.read (Xero OAuth: accounting.reports.read or per-report scopes) |
| HistoryNotes | xero.historynotes.read (crawls parents; parent OAuth scopes on each GET) |
| PaymentServices | xero.paymentservices.read (Xero OAuth: paymentservices — partner) |
| BankStatements, BankStatements_Table | xero.bankstatements.read (Xero OAuth: bankfeeds — Bank Feeds API, partner) |
| Organisation | xero.settings.read (alias of Settings / upstream Organisation) |
| Types | xero.types.read (SourceType enum catalogue; also /v1/types) |
| ResponseCodes, ReleaseNotes | xero.settings.read (static reference — not tenant-synced from Xero) |
TaxRates, TrackingCategories (+ _Table), Currencies, BrandingThemes, Users, InvoiceReminders |
matching xero.<table>.read (Xero OAuth: accounting.settings.read) |
| Journals, Journals_Table | xero.journals.read |
Wildcard grants such as xero.*.read are supported.
X0 metadata entity
GET /odata/v1/X0 returns one row so Excel and agents can see the portal connection:
- Service / version /
ApiBaseUrl/PortalUrl/DocsUrl - Customer name, email, plan, created at
- Resolved tenant id/name,
ConnectedAt,LastSyncAt(when a Xero org is connected) - Presenting API key label, prefix, environment, scopes
ExcelHint(e.g. preferJournals_Table)
This is an x0-only entity — not a Xero resource.
Data freshness
OData reads are served from x0’s local cache when warm (not live Xero on every Excel refresh). Pass ?refresh=true to force a Xero pull (see data-freshness.md). Responses may include X-X0-Cache, X-X0-Fetched-At, X-X0-Sync, and X-X0-Stale headers.
Journals vs Journals_Table
Xero’s GET /Journals returns nested JournalLines. x0 keeps that raw shape in cache and on /odata/v1/Journals.
For Excel, use /odata/v1/Journals_Table: one row per line, with header fields (JournalID, JournalDate, JournalNumber, CreatedDateUTC, plus Reference / Source*) repeated on every line. That is an x0-derived entity — not a separate Xero resource.
Requires Xero OAuth scope accounting.journals.read on the connected org, and API key scope xero.journals.read.
Note: accounting.journals.read needs Advanced+ and use-case approval (unlikely on Starter — see xero-quirks.md). x0’s app is Starter; Connect does not request GL Journals. Product path: stitch a pseudo-journal from transaction/report data (incomplete vs real Journals). Manual journals use accounting.manualjournals.read.
Attachments
Xero has no top-level /Attachments list. On sync, x0 crawls warm parent caches (Invoices, Contacts, Accounts, …) where HasAttachments is true, calls GET /{Parent}/{id}/Attachments, and stores metadata in the Attachments OData entity.
Each row includes:
RowKey— stable{ParentType}:{ParentID}:{AttachmentID}ParentType,ParentID,FileName,MimeType,ContentLengthDownloadPath— e.g./v1/attachments/Invoices/{id}/{file_id}
Download bytes (any parent type):
GET https://api.x0.co.nz/v1/attachments/Invoices/{InvoiceID}/{FileID}
Authorization: Bearer x0_live_….…
Account-specific write/list routes remain on /v1/accounts/{id}/attachments (see accounts-write.md).
Contacts & Invoices writes
REST + thin OData aliases for Contacts, draft Invoices, Items, draft PurchaseOrders, and CreditNotes — see contacts-invoices-write.md, items-purchase-orders-write.md, and credit-notes-payments-write.md. Lifecycle actions (authorise/void/payments) are REST-only on /v1/invoices, /v1/purchase-orders, and /v1/payments.
PUT /v1/contacts?tenant_id=…
POST /odata/v1/Contacts
PUT /v1/invoices?tenant_id=…
Requires xero.contacts.write / xero.invoices.write.
Requires parent caches to be warm first (sync Invoices, Contacts, etc.), then refresh Attachments.
Excel: metadata + download help live in the Reports & Attachments workbook (excel-workbook.md). Account write/upload macros stay on the Accounts “include scripts” pack.
Reports vs Reports_Table
On sync, x0 fetches a configured set of reports (Balance Sheet, P&L, Trial Balance, Bank Summary, Budget Summary, Executive Summary, Aged AR/AP) using default date windows (prior month-end / month-to-date for P&L-style reports).
/odata/v1/Reports— one row per report type (nestedRowsstripped for Excel scalars)/odata/v1/Reports_Table— one row per nested report row/cell group (ReportRowID,Cell0,Cell1,Account,Amount, …)
Use Reports_Table in Excel Power Query. Re-sync with ?refresh=true to pull fresh report dates.
Requires Xero OAuth report scopes (accounting.reports.read umbrella or individual accounting.reports.*.read scopes matching each report type).
Excel: use the dashboard workbook type Reports & Attachments (not the basic Accounts / All-endpoints packs) — see excel-workbook.md.
HistoryNotes
Like Attachments, Xero has no top-level History list. Sync crawls warm parent caches (Invoices, Contacts, Payments, …) and calls GET /{Parent}/{id}/History, storing rows in HistoryNotes (RowKey, ParentType, ParentID, Changes, User, Details, DateUTC).
Cap: ~80 parents per type per sync (not a full org audit dump). Warm the parent tables first.
PaymentServices
GET /PaymentServices → OData PaymentServices. Requires Xero OAuth scope paymentservices (partner / certification). Simulator seeds a Custom service for local testing.
BankStatements vs BankStatements_Table
Bank statement lines come from the Bank Feeds API (bankfeeds.xro/1.0), not Accounting api.xro/2.0.
- Sync pages
GET /Statements(and expandsGET /Statements/{id}when list rows omit lines) /odata/v1/BankStatements— one row per statement (balances + counts; nested lines stripped for Excel scalars)/odata/v1/BankStatements_Table— one row perStatementLinesentry (StatementLineID,PostedDate,Amount,PayeeName, …)
Requires OAuth scope bankfeeds (partner certification). Optional env XERO_BANKFEEDS_BASE_URL (defaults derived from XERO_API_BASE_URL). Simulator serves /bankfeeds.xro/1.0/Statements.
Kept out of the basic All endpoints Excel pack (heavy). Prefer Power Query on BankStatements_Table.
Organisation, Types, ResponseCodes, ReleaseNotes
| Entity | Behaviour |
|---|---|
| Organisation | Alias of Settings (same /Organisation cache) |
| Types | Lists SourceType enum tables (Account_*, Tax_*, …); rows also via /v1/types/{table} and OData entity sets named after each table |
| ResponseCodes | Static HTTP / problem-code reference (not live Xero) |
| ReleaseNotes | Pointers to Xero / x0 docs (Accounting API has no tenant ReleaseNotes collection) |
Related
- Documentation index · Glossary
- data-schema.md — dashboard diagram + mermaid
- excel-desktop.md · excel-addin.md · excel-workbook.md
- data-freshness.md · date-transform.md
- gzip-responses.md · yaak.md · ai-agents.md