ERP Accounting APIs — Research Report
Purpose: Ground the design of AIR's Backend interface (capabilities() / compile() / post() / reverse()) in the reality of the ERP APIs we intend to target.
Research method: 9 targeted web searches/fetches, official developer documentation prioritized.
Consultation date: 2026-08-05.
1. QuickBooks Online (QBO) — AIR's first real backend
1.1 API model
- REST + JSON, base URL
https://quickbooks.api.intuit.com/v3/company/{realmId}/...(sandbox:https://sandbox-quickbooks.api.intuit.com/...). TherealmIdis the company ID, obtained during the OAuth flow. - Minor versions: since August 1, 2025, minor versions 1–74 are deprecated; minor version 75 is the base/default and any
minorversionparameter below 75 is ignored. Recommendation: pin requests explicitly (?minorversion=75) and validate schema compatibility against mv75. - Rate limits: ~500 requests/minute per company (realm ID), max ~10 concurrent requests, and ~120 requests/minute on the Batch endpoint. Throttling returns HTTP 429 → backend must implement backoff + retry.
1.2 Authentication
- OAuth 2.0 Authorization Code flow via Intuit's identity platform. Access tokens expire after 1 hour; refresh tokens must be used (and rotated/persisted). Scope:
com.intuit.quickbooks.accounting. - Free sandbox companies are available in the developer dashboard for full end-to-end testing (Phase 3 target).
1.3 JournalEntry entity
Reference: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/journalentry
- A
JournalEntryis a header + array ofLineobjects. - Each line:
Description,Amount(always positive),DetailType: "JournalEntryLineDetail", and aJournalEntryLineDetailobject containing:PostingType:"Debit"or"Credit"— the debit/credit convention is an explicit enum per line, with an unsigned amount.AccountRef(value= account ID,nameoptional) — reference to the Account.- Optional
Entity(Customer/Vendor/Employee reference — required for lines on AR/AP accounts), class/department refs,TaxCodeRef/TaxApplicableOnfor tax lines.
- Header:
TxnDate,DocNumber,PrivateNote,CurrencyRef+ExchangeRate(multicurrency companies). - Operations: create (POST), read, full update, sparse update, delete (
?operation=deletewith Id + SyncToken). QBO uses optimistic concurrency viaSyncToken— every update/delete must supply the current SyncToken or fails. - No native "reverse" operation on JournalEntry: reversal = either delete the entry, or post a mirror-image journal entry (swap Debit/Credit) — AIR should generate an explicit reversal entry to preserve the audit trail rather than deleting.
1.4 Invoice and Payment entities
Invoice: header (CustomerRef, TxnDate, DueDate, CurrencyRef, DocNumber) +Line[]withDetailType: "SalesItemLineDetail"(ItemRef,Qty,UnitPrice,TaxCodeRef). Tax computed viaTxnTaxDetail(and Automated Sales Tax in US companies).Payment: separate entity linked to invoices viaLinkedTxn(TxnId,TxnType: "Invoice").LinkedTxnon the Invoice side is read-only; linkage is written from the Payment side.- This matters for AIR: a
Saleeconomic event may compile to an Invoice + Payment pair rather than a raw JournalEntry — the backend'scapabilities()should advertise which target document types it can emit.
1.5 Idempotency
- QBO does not support modern
Idempotency-Keyheaders. Two mechanisms:requestidquery parameter (a UUID unique per company): if the service receives a duplicaterequestid, it returns the original response instead of re-executing. Intuit strongly recommends sending it on every mutating request — this is our primary idempotency mechanism.- Fallback: enable
CustomTxnNumbersand rely onDocNumberuniqueness (duplicate → error 6140), usable as a duplicate detector.
Sources (consulted 2026-08-05):
- https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/journalentry
- https://developer.intuit.com/app/developer/qbo/docs/workflows/manage-linked-transactions
- https://blogs.intuit.com/2015/04/06/15346/ (Request ID / idempotency)
- https://medium.com/intuitdev/changes-to-our-accounting-api-that-may-impact-your-application-c330bd1a06f5 (minor version 75)
- https://docs.codat.io/updates/250219-qbo-minor-versions-update/
- https://satvasolutions.com/blog/quickbooks-online-api-guide (rate limits)
- https://truto.one/blog/how-to-integrate-with-the-quickbooks-online-api-2026-guide
2. Xero Accounting API
2.1 API model & auth
- REST + JSON, base
https://api.xero.com/api.xro/2.0/.... OAuth 2.0 only (OAuth 1.0a fully retired). - Tenant model: one OAuth connection can grant access to multiple Xero organisations; every request must carry the
Xero-Tenant-Idheader. The backend must persist the (token, tenantId) pair per organisation. - Rate limits: 60 calls/min per tenant, 5,000 calls/day per tenant, 10,000 calls/min app-wide. Daily per-tenant cap is the binding constraint for batch posting — favor batch endpoints (up to multiple elements per PUT/POST call).
2.2 ManualJournals
Reference: https://developer.xero.com/documentation/api/accounting/manualjournals
- Fields:
Narration(required),Date,Status,JournalLines[]. - Each
JournalLine:LineAmount,AccountCode,Description,TaxType,Tracking(analytic categories). - Debit/credit convention: signed amount — positive = debit, negative = credit. No PostingType enum. At least one debit and one credit line; the journal must balance.
Status:DRAFTorPOSTEDon creation; a posted manual journal is removed by setting status toVOIDED(drafts →DELETED). Voiding is the sanctioned "reversal" path, but AIR may still prefer an explicit contra-journal for traceability.- Invoices:
Invoicesendpoint (Type ACCREC/ACCPAY, LineItems with LineAmountTypes Exclusive/Inclusive/NoTax), separatePaymentsendpoint.
2.3 Idempotency
- Native support:
Idempotency-KeyHTTP header (UUID) on POST/PUT — retries do not create duplicates. This is the cleanest idempotency model of all surveyed APIs.
Sources (consulted 2026-08-05):
- https://developer.xero.com/documentation/api/accounting/manualjournals
- https://developer.xero.com/documentation/guides/oauth2/limits/
- https://developer.xero.com/faq/limits
- https://xeroapi.github.io/xero-python/v1/accounting/index.html (idempotency_key parameter on ManualJournals operations)
- https://cdn.cdata.com/help/DXF/jdbc/pg_accountingtable-manualjournals.htm (sign convention, balance requirement)
3. Odoo
3.1 API model & auth
- External API via XML-RPC (
/xmlrpc/2/common,/xmlrpc/2/object) and JSON-RPC (/jsonrpc); a REST-ish surface exists since Odoo 17. All protocols hit the same ORM (execute_kw(db, uid, password, model, method, args)). - Deprecation:
/xmlrpc,/xmlrpc/2and/jsonrpcare scheduled for removal in Odoo 22 (fall 2028) / Online 21.1 (winter 2027), replaced by the External JSON-2 API. AIR's Odoo backend should abstract transport so the RPC layer can be swapped. - Auth: database + login + API key (used as password in RPC). External API access requires a Custom pricing plan (not available on One App Free / Standard) — a real deployment constraint.
3.2 Journal entries: account.move
- One model for everything: journal entries, customer invoices, vendor bills, credit notes — discriminated by
move_type(entry,out_invoice,in_invoice,out_refund, ...). - Lines in
line_ids(account.move.line), each with separatedebitandcreditdecimal fields (one of the two is 0.0),account_id,partner_id,name. Invoice-style lines go throughinvoice_line_ids. - Lifecycle via
state:draft→posted→cancel. Post withaction_post(execute_kw(..., 'account.move', 'action_post', [[move_id]])); back to draft withbutton_draft. - Balance is validated at post time (debits = credits); to build temporarily unbalanced drafts pass context
{'check_move_validity': False}. - Reversal: first-class —
_reverse_moves/ reversal wizard creates a contra move withreversed_entry_idlinking back to the original (good fit for AIR's provenance graph).
3.3 Idempotency
- None built in. RPC create calls are not idempotent; the backend must implement its own dedupe (e.g., store the AIR event ID in
ref/a custom field and search before create).
Sources (consulted 2026-08-05):
- https://www.odoo.com/documentation/19.0/developer/reference/external_rpc_api.html
- https://www.odoo.com/documentation/19.0/developer/reference/external_api.html (External JSON-2)
- https://github.com/odoo/odoo/blob/14.0/addons/account/models/account_move.py
- https://www.odoo.com/forum/help-1/creating-journal-entries-via-external-api-xml-rpc-130818
- https://www.getknit.dev/blog/odoo-api-integration-guide-in-depth
4. SAP S/4HANA (brief survey — defer details, complexity high)
- Modern path: SOAP/OData APIs under the Finance – Posting Integration (SAP_COM_0002) communication scenario: Journal Entry – Post (synchronous and asynchronous) and Journal Entry – Change, plus OData read APIs (e.g., Operational Journal Entry Item – Read). Journal entries are complex documents: header (company code, document type, posting date) + typed item categories (G/L items, debtor items, creditor items, product tax items, withholding tax items). Debit/credit is expressed via signed amounts / debit-credit codes depending on the API.
- Reversal: a dedicated Journal Entry – Reverse service; requires
ReversalReasonandReversalReferenceDocument, with no line items — reversal is a header-level operation referencing the original document. Sync and async variants exist. - Legacy path:
BAPI_ACC_DOCUMENT_POST(post) andBAPI_ACC_DOCUMENT_CHECK(validate-only — interesting analogue of AIR'sValidate()), plus IDoc-based batch interfaces. On-premise landscapes still rely heavily on these. - Complexity drivers: communication arrangements setup, document splitting, parallel ledgers, tax determination inside SAP, strict field controls per document type. Decision: defer SAP backend; requires a dedicated deep research pass (OData vs SOAP vs BAPI, S/4HANA Cloud vs on-prem).
Sources (consulted 2026-08-05):
- https://community.sap.com/t5/technology-blog-posts-by-sap/apis-for-journal-entries-the-collection-updated-july-2025/ba-p/13565258
- https://community.sap.com/t5/enterprise-resource-planning-blog-posts-by-sap/guidelines-for-api-journal-entry-post/ba-p/13421397
- https://help.sap.com/docs/SAP_S4HANA_CLOUD/b978f98fc5884ff2aeb10c8fdeb8a43b/57b40036b71f4825adad70a0a5b91573.html (Journal Entry – Reverse)
- https://community.sap.com/t5/enterprise-resource-planning-blog-posts-by-sap/user-guide-for-journal-entry-post-api-on-s-4-hana-cloud/ba-p/13424193
5. NetSuite SuiteTalk & Sage Intacct (brief capability survey)
5.1 NetSuite (SuiteTalk REST)
- SuiteTalk REST Web Services: JSON, standard verbs (GET/POST/PATCH/DELETE) on records including
journalEntry(/services/rest/record/v1/journalEntry). Legacy SOAP SuiteTalk still exists. - Journal entry = header (subsidiary, currency, trandate) +
linesublist; each line has account reference and separatedebit/creditfields (Odoo-like convention). - Auth: Token-Based Authentication (OAuth 1.0-signed, via an Integration Record with consumer key/secret + token) or OAuth 2.0 (auth code and client-credentials machine-to-machine flows). Setup ceremony is significant (integration record, roles, permissions).
- Reversal: native support via reversal date fields on the journal entry record (creates a linked reversing entry). No native idempotency-key header; use
externalIdon records for dedupe/upsert.
Sources (consulted 2026-08-05):
- https://www.moderntreasury.com/journal/how-to-authenticate-to-netsuites-suitetalk-rest-web-services-api
- https://yepcode.io/recipes/rest-api-to-oracle-netsuite-journal-entries/
- https://www.brokenrubik.com/blog/netsuite-rest-api-guide
5.2 Sage Intacct
- Two API surfaces: legacy XML API (envelope:
<request><control><operation><content>, two-level authentication: sender ID/password + company/user credentials) and a newer REST API with OAuth 2.0 — all new objects/features ship on REST only. - Journal entries: XML API
GLBATCHobject (journal entry is an owned element of a GL transaction/batch — header journal +GLENTRYlines withTR_TYPE/amount or debit-credit indicators); REST API exposesgeneral-ledger/journal-entriescreate endpoint on the developer portal. - Dimensions (department, location, class, project) are pervasive on lines — the AIR line model must carry analytic dimensions generically.
Sources (consulted 2026-08-05):
- https://developer.intacct.com/api/general-ledger/journal-entries/
- https://developer.sage.com/intacct/apis/intacct/1/intacct-openapi/groups/general-ledger/groups/journal-entries/tags/general_ledger_journal-entries/paths/create-general-ledger-journal-entry
- https://www.getknit.dev/blog/sage-intacct-api-integration-guide-in-depth
6. Comparison matrix
| Aspect | QBO | Xero | Odoo | SAP S/4HANA | NetSuite | Intacct |
|---|---|---|---|---|---|---|
| Protocol | REST/JSON | REST/JSON | XML-RPC/JSON-RPC (→ JSON-2) | OData/SOAP (+BAPI/IDoc) | REST/JSON (+SOAP) | XML + REST |
| Auth | OAuth 2.0 (1h tokens) | OAuth 2.0 + Xero-Tenant-Id | API key via RPC login | Comm. arrangement / OAuth | TBA (OAuth1) or OAuth 2.0 | 2-level XML creds / OAuth 2.0 |
| D/C convention | PostingType enum + unsigned amount |
signed LineAmount (+debit / −credit) | separate debit/credit fields |
item categories + D/C codes | separate debit/credit fields |
GLENTRY type/amount |
| Idempotency | requestid query param |
Idempotency-Key header |
none (manual dedupe) | async msg IDs / none simple | externalId upsert |
none simple |
| Reversal | delete or contra entry (manual) | VOID status | _reverse_moves (linked contra) |
dedicated Reverse API (reason + ref) | native reversal date | contra entry |
| Rate limits | 500/min/realm | 60/min + 5k/day/tenant | server-dependent | gateway-dependent | account-tier concurrency | account-tier |
| Sandbox | free sandbox company | demo company | local/docker instance | trial/CAL complex | dev account (paid) | dev account |
7. Implications for AIR backend interface
The common Backend interface (capabilities() / compile() / post() / reverse()) must accommodate:
- Debit/credit representation is backend-specific. AIR's
CompiledJournalshould use one canonical internal convention (explicitside: Debit|Credit+ unsignedDecimalamount is the safest, lossless superset); each backend'scompile()maps it to signed amounts (Xero), PostingType enums (QBO), or paired debit/credit fields (Odoo, NetSuite). capabilities()must be rich, declaring at minimum: supported document types (raw journal entry vs invoice/payment documents), multicurrency support, tax-line handling (does the target recompute tax — QBO AST, SAP — or accept our tax lines — Xero, Odoo), analytic dimensions (Xero Tracking, Intacct dimensions, Odoo analytic), draft-then-post vs direct post, max lines/batch sizes, and native idempotency mechanism.- Idempotency is an adapter concern with a common contract. The interface should require:
post(payload, idempotency_key)where the key is derived from the AIR event ID + policy version. Adapters map it toIdempotency-Key(Xero),requestid(QBO),externalId(NetSuite), or a search-before-create fallback (Odoo, Intacct). Every backend MUST persist the (AIR event → target document ID) mapping in thePostingReceipt. reverse()must support three strategies, selected via capabilities: (a) native reversal API (SAP, NetSuite, Odoo_reverse_moves), (b) status change/void (Xero VOIDED), (c) synthesized contra entry (QBO, Intacct, generic CSV). Regardless of mechanism, AIR always records a reversal event in the provenance graph; deletion of posted entries is forbidden even where the API allows it (QBO delete).- Concurrency/version tokens: receipts must carry target-side version tokens (QBO
SyncToken) so later reverse/update operations don't fail on optimistic locking. - Rate limiting and retries belong in a shared posting runtime (token bucket per realm/tenant, exponential backoff on 429/503, batch endpoints where available: QBO Batch ≤ ~30 ops, Xero batched PUT/POST) — not reimplemented per backend.
- Multi-tenancy is first-class: a backend connection = (credentials, tenant/realm/db identifier, environment sandbox|prod). QBO realmId, Xero tenantId, Odoo db, SAP company code must be part of the connection config, never of the compiled journal.
- API versioning must be pinned per backend (QBO
minorversion=75, Odoo transport migration to JSON-2 by 2028, Intacct XML→REST) and surfaced incapabilities()so golden tests can be version-scoped. - Validate-before-post is natively supported by some targets (SAP
BAPI_ACC_DOCUMENT_CHECK, Odoo draft state, Xero DRAFT status) — the interface should expose an optionaldry_run/draft mode used by AIR's Approval pass before final posting. - Ordering of backend development confirmed: generic CSV → QBO (best docs, free sandbox, simple entity model) → Xero (cleanest idempotency) → Odoo (open source, self-hostable test instance) → SAP last (deep dedicated research required).