SPB Git

spb/air Public MIT

AIR — The Language of Accounting.

Python 100%
17.5 KB

# 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/...). The realmId is 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 minorversion parameter 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 JournalEntry is a header + array of Line objects.
  • Each line: Description, Amount (always positive), DetailType: "JournalEntryLineDetail", and a JournalEntryLineDetail object containing:
    • PostingType: "Debit" or "Credit" — the debit/credit convention is an explicit enum per line, with an unsigned amount.
    • AccountRef (value = account ID, name optional) — reference to the Account.
    • Optional Entity (Customer/Vendor/Employee reference — required for lines on AR/AP accounts), class/department refs, TaxCodeRef/TaxApplicableOn for tax lines.
  • Header: TxnDate, DocNumber, PrivateNote, CurrencyRef + ExchangeRate (multicurrency companies).
  • Operations: create (POST), read, full update, sparse update, delete (?operation=delete with Id + SyncToken). QBO uses optimistic concurrency via SyncToken — 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[] with DetailType: "SalesItemLineDetail" (ItemRef, Qty, UnitPrice, TaxCodeRef). Tax computed via TxnTaxDetail (and Automated Sales Tax in US companies).
  • Payment: separate entity linked to invoices via LinkedTxn (TxnId, TxnType: "Invoice"). LinkedTxn on the Invoice side is read-only; linkage is written from the Payment side.
  • This matters for AIR: a Sale economic event may compile to an Invoice + Payment pair rather than a raw JournalEntry — the backend's capabilities() should advertise which target document types it can emit.

# 1.5 Idempotency

  • QBO does not support modern Idempotency-Key headers. Two mechanisms:
    1. requestid query parameter (a UUID unique per company): if the service receives a duplicate requestid, it returns the original response instead of re-executing. Intuit strongly recommends sending it on every mutating request — this is our primary idempotency mechanism.
    2. Fallback: enable CustomTxnNumbers and rely on DocNumber uniqueness (duplicate → error 6140), usable as a duplicate detector.

Sources (consulted 2026-08-05):


# 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-Id header. 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 amountpositive = debit, negative = credit. No PostingType enum. At least one debit and one credit line; the journal must balance.
  • Status: DRAFT or POSTED on creation; a posted manual journal is removed by setting status to VOIDED (drafts → DELETED). Voiding is the sanctioned "reversal" path, but AIR may still prefer an explicit contra-journal for traceability.
  • Invoices: Invoices endpoint (Type ACCREC/ACCPAY, LineItems with LineAmountTypes Exclusive/Inclusive/NoTax), separate Payments endpoint.

# 2.3 Idempotency

  • Native support: Idempotency-Key HTTP 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):


# 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/2 and /jsonrpc are 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 separate debit and credit decimal fields (one of the two is 0.0), account_id, partner_id, name. Invoice-style lines go through invoice_line_ids.
  • Lifecycle via state: draftpostedcancel. Post with action_post (execute_kw(..., 'account.move', 'action_post', [[move_id]])); back to draft with button_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 with reversed_entry_id linking 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):


# 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 ReversalReason and ReversalReferenceDocument, 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) and BAPI_ACC_DOCUMENT_CHECK (validate-only — interesting analogue of AIR's Validate()), 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):


# 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) + line sublist; each line has account reference and separate debit / credit fields (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 externalId on records for dedupe/upsert.

Sources (consulted 2026-08-05):

# 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 GLBATCH object (journal entry is an owned element of a GL transaction/batch — header journal + GLENTRY lines with TR_TYPE/amount or debit-credit indicators); REST API exposes general-ledger/journal-entries create 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):


# 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:

  1. Debit/credit representation is backend-specific. AIR's CompiledJournal should use one canonical internal convention (explicit side: Debit|Credit + unsigned Decimal amount is the safest, lossless superset); each backend's compile() maps it to signed amounts (Xero), PostingType enums (QBO), or paired debit/credit fields (Odoo, NetSuite).
  2. 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.
  3. 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 to Idempotency-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 the PostingReceipt.
  4. 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).
  5. Concurrency/version tokens: receipts must carry target-side version tokens (QBO SyncToken) so later reverse/update operations don't fail on optimistic locking.
  6. 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.
  7. 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.
  8. API versioning must be pinned per backend (QBO minorversion=75, Odoo transport migration to JSON-2 by 2028, Intacct XML→REST) and surfaced in capabilities() so golden tests can be version-scoped.
  9. 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 optional dry_run/draft mode used by AIR's Approval pass before final posting.
  10. 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).