# Xero — Accounting API: gotchas

> Canonical URL: https://developers.apideck.com/apis/accounting/xero/gotchas

## GET /accounting/tax-rates

Xero tax rates include `effective_tax_rate` for the combined percentage. Tax types are categorized as INPUT (purchases), OUTPUT (sales), or NONE. Use the appropriate tax type when applying rates to invoices vs bills.

## GET /accounting/bills

`filter[bill_number]` and `filter[reference]` are exact matches on Xero's `InvoiceNumber`
and `Reference` respectively. The comparison is case-insensitive (`filter[bill_number]=inv-01`
matches `INV-01`) but otherwise literal, so surrounding whitespace has to match too.

Xero does not enforce unique bill numbers, so `filter[bill_number]` can return more than one
bill. When using it to verify whether a create that timed out (`outcome: uncertain`) was
persisted, compare `supplier.id`, `total` or `reference` on the matches rather than assuming
a single hit. Combine with `filter[updated_since]` to narrow the window; the two are ANDed.

Without a filter or sort, Xero returns bills oldest-updated first, so a bill created moments
ago appears on the **last** page rather than the first. To find recent bills, use
`filter[bill_number]`/`filter[reference]`, or `sort[by]=updated_at&sort[direction]=desc`.

## POST /accounting/bills

When providing both supplier.id and supplier.display_name (or supplier.name), Xero will update the contact's name if the ContactID exists. If the ContactID doesn't exist, Xero will automatically create a new contact using the provided name. This allows for automatic contact creation but may update existing contact names if the display_name differs from the stored name.

`notes` is not written — Xero's ACCPAY invoice has no field it maps to, so the value is accepted and silently discarded. Use `line_items[].description` instead.

`reference` and `bill_number` do not collide (`Reference` and `InvoiceNumber` respectively), but Xero documents `InvoiceNumber` as the value that "will also display as Reference in the UI" on ACCPAY, and treats the `Reference` field as ACCREC-only. A value sent in `reference` therefore round-trips through the API without appearing in the Xero bill UI. See https://developer.xero.com/documentation/api/accounting/invoices

On read, `bill_number` and `po_number` are both mapped from `InvoiceNumber` and are always identical — Xero's Invoice object has no separate purchase-order-number field.

## PATCH /accounting/bills/{id}

Bills in xero can be tax inclusive, tax exclusive or no tax. The tax_inclusive field should be set to true, false or null if the bill is tax inclusive, tax exclusive or no tax.
## POST /accounting/bills/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

Records are written one at a time rather than in a single batched call, because this
resource runs a per-record step after the write (rebilling links for bills, allocations
for credit notes) and that step has to see its own record. A request of N records
therefore takes about N times as long as a single write and counts as N calls against
your Xero rate limit.

At most 20 records per request. That ceiling was measured for the single batched call
and has not been re-measured for the per-record path, so treat it as an upper bound
rather than a safe size: prefer smaller requests and set client timeouts from your own
observed per-record latency.

Each record succeeds or fails on its own; read the per-item `status` to see which were
written. If the request times out, the items it could not confirm come back with
`status: uncertain` rather than `failed` — some of them may well have been written, and
no ids are returned for those. Verify before retrying an `uncertain` item: list the
resource to find what exists, or a retry will duplicate it.

Results come back in the order the items were sent. Supply `_ref` on every item if you
need to tie an outcome back to your own record — it is echoed on every result, including
failures, and is never sent to the connector.

## GET /accounting/invoices

Invoices in Xero can be tax inclusive, tax exclusive, or no tax. The `tax_inclusive` field is set to `true` (tax inclusive), `false` (tax exclusive), or `null` (no tax). Line items include `tax_rate` references that link to the tax-rates resource.

## POST /accounting/invoices

When providing both customer.id and customer.display_name (or customer.name), Xero will update the contact's name if the ContactID exists. If the ContactID doesn't exist, Xero will automatically create a new contact using the provided name. This allows for automatic contact creation but may update existing contact names if the display_name differs from the stored name.

## PATCH /accounting/invoices/{id}

For updating invoices, there are some limitations. Check out [here](https://developers.apideck.com/connectors/xero/docs/application_owner+oauth_credentials#1-limitations-and-rules-for-updating-xero-invoices)
## PATCH /accounting/invoices/{id}

Invoices in Xero can be tax inclusive, tax exclusive, or no tax. Set `tax_inclusive` to `true` (amounts include tax), `false` (amounts exclude tax), or `null` (no tax applied). When updating, changing `tax_inclusive` recalculates all line item amounts.

## POST /accounting/invoices/batch

The whole request is one call to the provider, so it counts as **one request**
against your plan however many records it carries.

At most 20 invoices per request. A request carrying more is rejected before any
invoice is written.

How much of your Xero rate limit a request uses depends on how you reference ledger
accounts on line items. Sending `line_items[].ledger_account.code` costs one Xero
call for the whole request. Sending `line_items[].ledger_account.id` instead costs
more than two calls per invoice, because each account has to be looked up — prefer
`code` when you have it, and set client timeouts accordingly.

A request that times out may still have created some or all of its invoices. When
that happens no ids are returned for them, so treat a timed-out batch as uncertain
rather than failed: list invoices to find what was written before retrying, or a
retry will duplicate them.

Results are returned in the order the items were sent. Supply `_ref` on every item
if you need to tie outcomes back to your own records.

## GET /accounting/ledger-accounts

`current_balance` is sourced from Xero's Trial Balance report (as at the current date, UTC),
adding one extra downstream request per call — Xero's Accounts API itself returns no balance.
Balances are signed debit-positive: asset and expense balances are positive; liability, equity
and revenue balances are negative. Accounts with a zero balance return 0.

Populating the field requires the connection's granted scopes to include
`accounting.reports.trialbalance.read` (or the legacy `accounting.reports.read`). Connections
authorized before this scope entered the default request list (2026-08-18) must be
re-authorized to grant it; until then — or when the authorizing Xero user lacks the "reports"
role — accounts are returned without `current_balance` instead of the request failing.

Fixed assets: the `fixed_asset` object comes from Xero's fixed-asset register, for an account
that holds asset cost: the registered assets whose asset type uses it as the fixed-asset
account (disposed and draft assets are not counted). `fixed_asset.cost` and
`fixed_asset.accumulated_depreciation` are totals over those assets; they come from the
register, not the ledger, so they can differ from `current_balance`. An account usually holds
several assets, so `acquisition_date`, `depreciation_method`, `downstream_depreciation_method`
and `useful_life_months` are set only when every one of its assets has the same value.
Methods are compared as Xero names them, so `DiminishingValue150` and `DiminishingValue200`
differ; a rate-based asset has no useful life. The object needs the `assets.read` scope:
connections authorized before it entered the default request list must be re-authorized, and
until then it is not set. It is computed only when `fields` names `fixed_asset` or one of its
sub-fields (for example `fields=id,name,current_balance,fixed_asset`), because it costs one
request for the asset types plus one per 200 registered assets; otherwise no extra request is
made. With Data Scopes, name the consented sub-fields (`fixed_asset.cost`, and so on). With
more than 2,000 registered assets, or when reading the register takes past 20 seconds of the
request, it is not set. Accumulated-depreciation accounts and other accounts have no
`fixed_asset`.

## GET /accounting/ledger-accounts

Xero limits to 10 attachments per resource (invoices/bills) of 3.5MB each.
## GET /accounting/ledger-accounts/{id}

`current_balance` is sourced from Xero's Trial Balance report (as at the current date, UTC),
adding one extra downstream request per call — Xero's Accounts API itself returns no balance.
Balances are signed debit-positive. Accounts with a zero balance return 0.

Populating the field requires the connection's granted scopes to include
`accounting.reports.trialbalance.read` (or the legacy `accounting.reports.read`). Connections
authorized before this scope entered the default request list (2026-08-18) must be
re-authorized to grant it; until then — or when the authorizing Xero user lacks the "reports"
role — the account is returned without `current_balance` instead of the request failing.

The `fixed_asset` object comes from the fixed-asset register, as described on the list:
computed only when `fields` names `fixed_asset` or one of its sub-fields, with the
`assets.read` scope, at the cost of extra requests.

## POST /accounting/ledger-accounts/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

Xero creates ledger accounts one at a time, with bank accounts created differently
from other account types, so each record in the request is written separately. A
request of N records therefore takes about N times as long as a single write and
counts as N calls against your Xero rate limit.

At most 20 records per request. That ceiling has not been measured for records
written one at a time on this resource, so treat it as an upper
bound rather than a safe size: prefer smaller requests and set client timeouts from
your own observed per-record latency.

Each record succeeds or fails on its own; read the per-item `status` to see which
were written. If the request times out, the items it could not confirm come back
with `status: uncertain` rather than `failed` — some of them may well have been
written, and no ids are returned for those. Verify before retrying an `uncertain`
item: list the resource to find what exists, or a retry will duplicate it.

Results come back in the order the items were sent. Supply `_ref` on every item if
you need to tie an outcome back to your own record — it is echoed on every result,
including failures, and is never sent to the connector.

## GET /accounting/invoice-items

Xero limits to 10 attachments per resource (invoices/bills) of 3.5MB each.
## POST /accounting/credit-notes

- `type` is required. Without it Xero returns a 400 ("Please ensure the invoice type is set") and nothing is created.
- Allocations are applied after the credit note is created. The credit note and the invoice must both be `authorised`, with the same contact and currency, and the allocation amount can't be more than the invoice balance.
- If an allocation fails, the request returns a 400 and the credit note is rolled back: an `authorised` credit note is voided (after removing the allocations that already succeeded), a `draft` one is deleted. If the rollback itself fails, the error says the credit note is still in Xero and gives its id; void or delete it before retrying.

## PATCH /accounting/credit-notes/{id}

- Authorised Credit Note can only be updated to voided status and cannot be deleted once authorised.
- Any invoice allocation and payment must be deleted before updating the authorised Credit Note to status voided.
- To delete payments, use the payment ID from the raw data and delete it using the Delete Payment endpoint.
- For adding and removing invoice allocations, use the proxy API. [Know more](https://developers.apideck.com/connectors/xero/docs/application_owner+oauth_credentials#2-curl-requests-for-adding-and-removing-allocations-from-xero-credit-note)

## PATCH /accounting/credit-notes/{id}

Credit notes in xero can be tax inclusive, tax exclusive or no tax. The tax_inclusive field should be set to true, false or null if the credit note is tax inclusive, tax exclusive or no tax.
## DELETE /accounting/credit-notes/{id}

Xero will only allow deleting a Credit Note if it is in draft status.
Credit notes with "authorised" status cannot be deleted directly. Instead:
- If the credit note is empty (i.e., has no invoice allocations and payments), its status can be updated to "voided" by update Credit Note endpoint.
- If the credit note has invoice allocations and payments, these must be removed first. Once all invoice allocations and payments are deleted, the credit note's status can be updated to "Voided" by update Credit Note endpoint.
  [Know more](https://developers.apideck.com/connectors/xero/docs/application_owner+oauth_credentials#2-curl-requests-for-adding-and-removing-allocations-from-xero-credit-note)

## POST /accounting/credit-notes/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

Records are written one at a time rather than in a single batched call, because
this resource runs a per-record step after the write (an allocations step) and
that step has to see its own record. A request of N records therefore takes
about N times as long as a single write and counts as N calls against your
Xero rate limit.

At most 20 records per request. That ceiling has not been measured for records
written one at a time on this resource, so treat it as an upper
bound rather than a safe size: prefer smaller requests and set client timeouts from
your own observed per-record latency.

A credit note sent with `allocations` is allocated to those invoices right after it is
created. If an allocation fails, the item comes back with an `error` even when its
`status` is `created`: Xero may have kept the credit note without its allocations (an
authorised credit note cannot be removed) or removed it. List credit notes before
retrying that item, or a retry may duplicate it.

Each record succeeds or fails on its own; read the per-item `status` to see which
were written. If the request times out, the items it could not confirm come back
with `status: uncertain` rather than `failed` — some of them may well have been
written, and no ids are returned for those. Verify before retrying an `uncertain`
item: list the resource to find what exists, or a retry will duplicate it.

Results come back in the order the items were sent. Supply `_ref` on every item if
you need to tie an outcome back to your own record — it is echoed on every result,
including failures, and is never sent to the connector.

## POST /accounting/bill-credit-notes

Bill credit notes are Xero credit notes of type `ACCPAYCREDIT`; the list returns only supplier (AP) credits, while the separate `credit-notes` resource returns customer (AR) credits. `remaining_credit` reports the unapplied amount.
A credit note can only be deleted while it is in `draft`. An `authorised` credit note cannot be deleted — set `status` to `voided` to reverse it instead.

## POST /accounting/bill-credit-notes/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

Records are written one at a time rather than in a single batched call, because this
resource runs a per-record step after the write (rebilling links for bills, allocations
for credit notes) and that step has to see its own record. A request of N records
therefore takes about N times as long as a single write and counts as N calls against
your Xero rate limit.

At most 20 records per request. That ceiling was measured for the single batched call
and has not been re-measured for the per-record path, so treat it as an upper bound
rather than a safe size: prefer smaller requests and set client timeouts from your own
observed per-record latency.

A credit note sent with `allocations` is allocated to those invoices right after it is
created. If an allocation fails, the item comes back with an `error` even when its
`status` is `created`: Xero may have kept the credit note without its allocations (an
authorised credit note cannot be removed) or removed it. List credit notes before
retrying that item, or a retry may duplicate it.

Each record succeeds or fails on its own; read the per-item `status` to see which were
written. If the request times out, the items it could not confirm come back with
`status: uncertain` rather than `failed` — some of them may well have been written, and
no ids are returned for those. Verify before retrying an `uncertain` item: list the
resource to find what exists, or a retry will duplicate it.

Results come back in the order the items were sent. Supply `_ref` on every item if you
need to tie an outcome back to your own record — it is echoed on every result, including
failures, and is never sent to the connector.

## GET /accounting/customers

In Xero, contacts are not strictly separated into customers or suppliers until linked to a transaction. Apideck's approach is to return customers as `IsCustomer=true||IsSupplier=false` and suppliers as `IsSupplier=true||IsCustomer=false`. If you need other cases, use pass-through, e.g. `?pass_through[where]=IsCustomer=false AND IsSupplier=false` for uncategorized contacts. See the [Pass-through guide](https://developers.apideck.com/guides/pass-through) for more details.

## POST /accounting/customers/batch

The whole request is one call to the provider, so it counts as **one request**
against your plan however many records it carries.

At most 20 customers per request. A request carrying more is rejected before any
customer is written.

A request that times out may still have created some or all of its customers.
When that happens no ids are returned for them, so treat a timed-out batch as
uncertain rather than failed: list customers to find what was written before
retrying, or a retry will duplicate them.

Results are returned in the order the items were sent. Supply `_ref` on every
item if you need to tie outcomes back to your own records.

## GET /accounting/suppliers

In Xero, contacts are not strictly separated into customers or suppliers until linked to a transaction. Apideck's approach is to return suppliers as `IsSupplier=true||IsCustomer=false` and customers as `IsCustomer=true||IsSupplier=false`. If you need other cases, use pass-through, e.g. `?pass_through[where]=IsCustomer=false AND IsSupplier=false` for uncategorized contacts. See the [Pass-through guide](https://developers.apideck.com/guides/pass-through) for more details.

## POST /accounting/suppliers/batch

The whole request is one call to the provider, so it counts as **one request**
against your plan however many records it carries.

At most 20 suppliers per request. A request carrying more is rejected before any
supplier is written.

A request that times out may still have created some or all of its suppliers.
When that happens no ids are returned for them, so treat a timed-out batch as
uncertain rather than failed: list suppliers to find what was written before
retrying, or a retry will duplicate them.

Results are returned in the order the items were sent. Supply `_ref` on every
item if you need to tie outcomes back to your own records.

## POST /accounting/payments

Xero supports creating payments against multiple allocation types in a single request. However, the response will provide the ID of the first payment only. To access all payment IDs, inspect the raw data.

Xero rejects payments where the organisation base currency, the invoice currency, and the bank
account currency are all different (a "3-currency transaction"):

`"The invoice, payment and organisation base currencies are all different. Xero cannot currently handle a 3-currency transaction."`

Pay from a bank account in either the base or the invoice currency (two currencies) to avoid the error.

Refunding a credit note (for example when the invoice is already paid):
- Send an allocation with `type: credit_memo` and the credit note id. Xero records it as a credit note refund (`ARCREDITPAYMENT`). The credit note becomes `paid` when the refund settles its remaining credit in full.
- The `account` must be a bank account, or have "Enable payments to this account" ticked in Xero. Otherwise Xero returns "Account type is invalid for making a payment to/from".
- Bank accounts often have no code in Xero. Send `account.id` instead of `account.code`.
- The amount can't be more than the credit note's remaining credit ("Payment amount exceeds the amount outstanding on this document").

## PATCH /accounting/payments/{id}

Xero doesn't support updating payments. To update a payment, you need to delete the existing payment and create a new one.

## POST /accounting/payments/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

A payment allocated across several invoices is recorded by Xero as several
payments, so a single item you send can turn into more than one provider record.
Because of that, records are written one at a time rather than in a single
batched call, and each result returns the id of the first payment Xero creates
for that item. A request of N records therefore takes about N times as long as
a single write and counts as N calls against your Xero rate limit.

At most 20 records per request. That ceiling has not been measured for records
written one at a time on this resource, so treat it as an upper
bound rather than a safe size: prefer smaller requests and set client timeouts from
your own observed per-record latency.

Each record succeeds or fails on its own; read the per-item `status` to see which
were written. If the request times out, the items it could not confirm come back
with `status: uncertain` rather than `failed` — some of them may well have been
written, and no ids are returned for those. Verify before retrying an `uncertain`
item: list the resource to find what exists, or a retry will duplicate it.

Results come back in the order the items were sent. Supply `_ref` on every item if
you need to tie an outcome back to your own record — it is echoed on every result,
including failures, and is never sent to the connector.

## GET /accounting/companies

Xero does not expose a total record count in list responses. `meta.total_count` will not appear.

## GET /accounting/journal-entries

Xero has two resources: `Journals` (postings from any source — invoices, bills, payments, manual journals, etc.) and `ManualJournals` (manual entries only). The default behaviour of `journal-entries` reads from `Journals`. From 2026-03-02 Xero moves the `Journals` endpoint to its Advanced tier; for the full general ledger view use `accounting/general-ledger-transactions` instead.

Use `?filter[scope]=manual` to read exclusively from `ManualJournals` (free in every Xero tier, target behaviour after the upcoming default flip). Use `?filter[scope]=system` or omit the filter to keep the legacy default. The `?filter[status]=draft` shortcut continues to read drafts from `ManualJournals`.

The exposed id today depends on the journal entry status: for drafts it is the `ManualJournalID`; once posted, it is the Xero `JournalID` (different from the draft id). To track the underlying manual journal across the lifecycle, use the new `manual_journal_id` field returned in write responses (and use the pattern `manual_journal_id ?? id` for stable storage). On a draft → posted transition the `id` changes but the `manual_journal_id` remains stable.

Xero returns 100 items per call, so the `limit` parameter is ignored.

## POST /accounting/journal-entries

Writes go to Xero `ManualJournals` (free in every Xero tier).

When the resulting status is `draft`, the response `id` is the `ManualJournalID`. When the status is `posted` (either created posted directly, or transitioned from draft), the response `id` is the Xero `JournalID` for backwards compatibility — note that this is **not** the same value as the underlying `ManualJournalID`.

The response also returns `manual_journal_id` — the underlying `ManualJournalID`, which is stable across the entire lifecycle (draft → posted → voided). When persisting an id from this response, use the pattern `manual_journal_id ?? id` so your stored reference does not change on a draft → posted transition.

Xero only supports `draft` and `posted` for the status field. For ledger accounts in journal lines, you can use either `ledger_account.id` (UUID) or `ledger_account.code`; if only the ID is provided, the code will be resolved automatically.

Journal entries in xero can be tax inclusive, tax exclusive or no tax. The `tax_inclusive` field should be set to true, false or null if the journal entry is tax inclusive, tax exclusive or no tax.

## PATCH /accounting/journal-entries/{id}

Xero has two resources: journals (entries from transactions like invoices, payments, or manual journals) and manual journals (user-created entries).
Draft entries are manual journals. When posted, they become journals and the ID changes. Updating a posted entry creates a reverse journal and a new one, so the ID also changes.
Xero only supports `draft` and `posted` for the status field.
For ledger accounts in journal lines, you can use either `ledger_account.id` (UUID) or `ledger_account.code`. If only the ID is provided, the code will be resolved automatically.

## POST /accounting/journal-entries/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

Records are written one at a time rather than in a single batched call, because
this resource runs a per-record step after the write and that step has to see
its own record. A journal created with `status: posted` takes two calls to Xero
(create, then look up the posted journal's id); a draft takes one. A request of N
posted journals therefore counts as about 2N calls against your Xero rate limit.

At most 20 records per request. A full request of 20 posted journals takes about
18 seconds and makes about 40 Xero calls, two thirds of Xero's 60-calls-per-minute
limit, so space consecutive full requests at least a minute apart or expect `429`
responses on the later ones. Set client timeouts accordingly.

Each record succeeds or fails on its own; read the per-item `status` to see which
were written. If the request times out, the items it could not confirm come back
with `status: uncertain` rather than `failed` — some of them may well have been
written, and no ids are returned for those. Verify before retrying an `uncertain`
item: list the resource to find what exists, or a retry will duplicate it.

Results come back in the order the items were sent. Supply `_ref` on every item if
you need to tie an outcome back to your own record — it is echoed on every result,
including failures, and is never sent to the connector.

## GET /accounting/general-ledger-transactions

Backed by Xero's `Journals` API, which moves to the Advanced app tier on 2026-03-02. Each posting (invoice, bill, payment, payroll, manual journal, etc.) appears here with its origin discriminated by `source_type` and `source_id`.

Filtering by `source_type=journal_entry` returns no results because Xero's underlying `where=SourceType=="MANJOURNAL"` filter does not match any rows on the `Journals` endpoint, even when manual-journal postings clearly exist in the unfiltered list response. To list manual-journal postings, use `accounting/journal-entries?filter[scope]=manual` or `accounting/journal-entries` directly after the manual-only redefinition.

`currency` and `currency_rate` are returned as `null`. Xero's `Journals` endpoint does not expose currency information; fetch the originating document via `source_type` + `source_id` to retrieve currency.

The `Reference` field on Xero `Journals` is mapped to `reference`. For manual-journal postings, this is the `Narration` of the originating manual journal.

## GET /accounting/general-ledger-transactions/{id}

Backed by Xero's `Journals/{id}` endpoint. Two limitations of that endpoint surface here:

`source_type` and `source_id` are returned as `null` for every record retrieved through this single endpoint — invoices, bills, payments, manual journals, all of them. Xero's `GET /api.xro/2.0/Journals/{id}` strips those fields regardless of journal type. To recover them, query the list endpoint (`GET /accounting/general-ledger-transactions`) where these fields are populated. Note that `filter[source_id]` takes the **originating document id** (e.g., the invoice's id), not this resource's transaction id — use it when you already know the source document and want to find every GL posting it produced. To find a specific transaction by its own id, narrow the list with `filter[updated_since]` close to the transaction's `posted_at` and iterate.

`currency` and `currency_rate` are returned as `null`. Xero's `Journals` endpoint does not expose currency information; fetch the originating document via `source_type` + `source_id` (after recovering them through the list endpoint) to obtain currency.

## POST /accounting/purchase-orders

Xero accepts a shipping address as a string instead of separate fields. Ie: `23 Main Street
Central City
Marineville
1234
`
## PATCH /accounting/purchase-orders/{id}

Xero accepts a shipping address as a string instead of separate fields. Ie: `23 Main Street
Central City
Marineville
1234
`
## PATCH /accounting/purchase-orders/{id}

Purchase orders in xero can be tax inclusive, tax exclusive or no tax. The tax_inclusive field should be set to true, false or null if the purchase order is tax inclusive, tax exclusive or no tax.
## GET /accounting/locations

Xero limits to 10 attachments per resource (invoices/bills) of 3.5MB each.
## POST /accounting/attachments/{reference_type}/{reference_id}

Xero limits to 10 attachments per resource (invoices/bills) of 3.5MB each.
## GET /accounting/fiscal-periods

Xero has no fiscal-year list. Years are generated from the organisation's financial year-end setting: the current fiscal year and the 9 before it, listed newest first, never a year that ended before the organisation was created in Xero. Xero does not return the conversion date, so history converted from an earlier system, or dated before the organisation was created, is not listed. If the company ever changed its fiscal year-end, earlier years are reported on the current year-end, not the one in force at the time. Every record has `type: year`; `id` is the year's start date (`YYYY-MM-DD`) and `name` and `fiscal_year` are the year in which it ends. `status` comes from the lock dates: `closed` when the year ends on or before the end-of-year lock date (it stops every user, advisers included), `locked` when it ends on or before the period lock date (it stops every user except advisers), otherwise `open`; a lock date inside a year leaves it `open`. `locked_through` is the later of the two lock dates capped at the year's end, when it is on or after the year's start, otherwise null.

## GET /accounting/fiscal-periods/{id}

An id outside the generated 10-year window answers 404.

## GET /accounting/trial-balance

Built from Xero's `GET /Reports/TrialBalance`, using its YTD Debit and YTD Credit columns: balance sheet accounts are cumulative to `end_date`, income and expense accounts run from the start of the financial year that contains `end_date`. Xero's separate Debit and Credit columns (that month's movement only) are not used. `filter[end_date]` maps to Xero's `date` (today in UTC when left out, since Xero's own default is not today) and `filter[accounting_method]=cash` to `paymentsOnly=true`. Accounts whose YTD debit and credit net to zero are left out. `account_id` is Xero's AccountID, the id `GET /accounting/ledger-accounts/{id}` takes; a balance row without an AccountID has `line_type: unmapped` and a null `account_id`. `code` and the full account `name` come from Xero's accounts list (one extra request), because the report's "Name (Code)" label cannot tell a code from a name that ends in parentheses; `code` is absent for accounts that have none. `currency` and `start_date` come from the organisation's base currency and financial year end (one extra request), and are null if that lookup fails. Both extra requests are made for the organisation named by `x-apideck-company-id`, like the report itself. A report whose debits and credits differ by half a cent or more returns a `502`.
## GET /accounting/tracking-categories

Returns tracking category options only. IDs and names are concatenated as "CategoryName - OptionName". The provided ID can be used to link to the correct tracking category to invoices, bills, etc.
## POST /accounting/tracking-categories/batch

Creating tracking categories is not supported on Xero.

## POST /accounting/bill-payments

Xero rejects bill payments where the organisation base currency, the bill currency, and the
bank account currency are all different (a "3-currency transaction"):

`"The invoice, payment and organisation base currencies are all different. Xero cannot currently handle a 3-currency transaction."`

Pay from a bank account in either the base or the bill currency (two currencies) to avoid the error.

## POST /accounting/bill-payments/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

A payment allocated across several invoices is recorded by Xero as several
payments, so a single item you send can turn into more than one provider record.
Because of that, records are written one at a time rather than in a single
batched call, and each result returns the id of the first payment Xero creates
for that item. A request of N records therefore takes about N times as long as
a single write and counts as N calls against your Xero rate limit.

At most 20 records per request. That ceiling has not been measured for records
written one at a time on this resource, so treat it as an upper
bound rather than a safe size: prefer smaller requests and set client timeouts from
your own observed per-record latency.

Each record succeeds or fails on its own; read the per-item `status` to see which
were written. If the request times out, the items it could not confirm come back
with `status: uncertain` rather than `failed` — some of them may well have been
written, and no ids are returned for those. Verify before retrying an `uncertain`
item: list the resource to find what exists, or a retry will duplicate it.

Results come back in the order the items were sent. Supply `_ref` on every item if
you need to tie an outcome back to your own record — it is echoed on every result,
including failures, and is never sent to the connector.

## GET /accounting/expenses

In Xero, both suppliers (also used for employees) and customers are identified using the same contact ID, which maps to both `supplier_id` and `customer_id`. To accurately determine the contact type, use the suppliers and customers endpoints.
## POST /accounting/expenses

Xero expenses map to Spend Money bank transactions and are created with status AUTHORISED — there is no draft state. Both a bank/credit-card account (`account.id`) and a supplier (`supplier.id`) are required, even though they are optional in the unified schema; omitting either returns a Xero validation error ("A Contact must be specified" / "The BankAccount must be specified"). The `account.id` must correspond to a 'bank' or 'credit_card' account type — other account types may result in errors or unexpected behavior. Xero's Expenses (expense-claims) module, where contact-less drafts can be saved in the UI, has no public write API. For a draft, payable-style expense without a bank account, use the Bills resource (Xero ACCPAY invoices) with `status: "draft"` instead.
`reference` and `memo` both map to the single Xero `Reference` field on the underlying bank transaction, and `reference` wins — when both are sent, `memo` is silently discarded. On read both are populated from `Reference` and are always identical, so a caller cannot tell which field a value came from. Send only one of the two.

## GET /accounting/expenses/{id}

In Xero, both suppliers (also used for employees) and customers are identified using the same contact ID, which maps to both `supplier_id` and `customer_id`. To accurately determine the contact type, use the suppliers and customers endpoints.
## GET /accounting/aged-creditors

Reading this report needs the `accounting.reports.aged.read` scope (or the legacy
`accounting.reports.read`) on the connection's Xero grant. When the grant lacks both, the
request is refused with a `403` that names the missing scope, before anything is sent to
Xero. Re-authorize the connection with `accounting.reports.aged.read` to use this endpoint.

## GET /accounting/aged-debtors

Reading this report needs the `accounting.reports.aged.read` scope (or the legacy
`accounting.reports.read`) on the connection's Xero grant. When the grant lacks both, the
request is refused with a `403` that names the missing scope, before anything is sent to
Xero. Re-authorize the connection with `accounting.reports.aged.read` to use this endpoint.

## GET /accounting/bank-feed-accounts

Bank Feed Accounts in Xero represent bank accounts connected to Xero's Bank Feeds feature, which automatically imports bank transactions into Xero on a daily basis.
For more information on setting up and using Bank Feeds with Xero, see the [Bank Feeds for Xero guide](https://developers.apideck.com/guides/bank-feeds-xero).

## POST /accounting/bank-feed-accounts

Bank Feed Accounts in Xero represent bank accounts connected to Xero's Bank Feeds feature, which automatically imports bank transactions into Xero on a daily basis.
For more information on setting up and using Bank Feeds with Xero, see the [Bank Feeds for Xero guide](https://developers.apideck.com/guides/bank-feeds-xero).

## POST /accounting/bank-feed-statements

In Xero, if a transaction ID has been previously used, the API will reject the entire bank feed statement.
However, if the statement includes multiple transactions and at least one transaction ID is unique,
the API will return a success response but will not process the transactions with duplicate IDs.

## GET /accounting/categories

Xero limits to 10 attachments per resource (invoices/bills) of 3.5MB each.

[API reference](https://developers.apideck.com/apis/accounting/reference.md) · [Coverage matrix](https://developers.apideck.com/apis/accounting/coverage)
