How to Integrate a Cloud Accounting API into a SaaS Platform
A practitioner's guide to building a multi-tenant accounting integration that survives OAuth, webhooks, tax rules, and reconciliation without corrupting the ledger.
Most teams that set out to "add accounting" to their SaaS product start by wiring an HTTP client to a provider endpoint and posting an invoice. That works in a demo. It breaks the first time a customer connects two accounting organizations, the first time a webhook arrives twice, and the first time a refresh token rotates and gets lost mid-request. The durable version of this work is not a client. It is an integration layer.
That layer has to manage tenant connections, token storage and rotation, provider-specific accounting rules, record mapping, initial and incremental sync, webhook ingestion with retries and deduplication, rate limits, versioning, and an audit trail your support team can read at 2 a.m. This article walks through that architecture and the decisions that matter most.
A useful mental model:
SaaS product → accounting integration service → provider adapter (or unified API) → asynchronous sync and reconciliation
The accounting system stays the system of record for ledger objects: accounts, tax codes, posted transactions, journal entries. Your SaaS platform keeps stable cross-system identifiers, sync state, mapping metadata, and an operational audit log.
What "cloud accounting API" actually covers here
The phrase usually means the API of a hosted bookkeeping platform such as QuickBooks Online, Xero, Sage, Zoho Books, or a regional system. It can also mean a unified accounting API that normalizes several providers behind one interface.
It is worth being precise, because the implementation differs by intent. Reading invoices for reporting is not the same job as creating invoices from SaaS activity, posting bills, syncing customers and products, writing journal entries, or running true two-way sync. Decide which of these you are building before you touch an endpoint.
One more distinction matters for this piece. A payment API moves money. A banking API pulls account data. An invoicing API can create documents without exposing the general ledger. An embedded accounting ledger is different again: the SaaS company owns the accounting engine itself. This last approach is where Nordlet sits. Instead of connecting your product to a customer's external books, Nordlet gives platforms and marketplaces real double-entry books, an immutable audit-ready ledger, EU VAT handling, and payouts inside their own product, through an API-first design with typed SDKs and webhooks. If you are a marketplace deciding whether to integrate a customer's accounting system or run the books yourself, that is a genuine fork in the road, and we will come back to it.
Multi-tenancy: scope every connection twice
In a SaaS product, one application connects to many customer organizations. Every connection must be scoped by both the SaaS tenant and the provider's organization identifier, such as a Xero tenant ID or a QuickBooks realm ID.
Do not key connections on the OAuth user ID alone. One user can authorize several accounting organizations, and one organization can be reconnected by different users over time. If you get this wrong, you will eventually post one customer's invoice into another customer's books, and that is not a bug you recover from quietly.
The first architectural decision: direct connector, unified API, or embedded ledger
Direct provider integrations
A direct integration gives you the most control: provider-specific features, custom fields, error handling, sync frequency, data retention, and cost structure. The price is real. Each provider brings its own OAuth flow, schema, quirks, rate limits, webhook behavior, certification, and ongoing maintenance.
Unified accounting APIs
A unified API offers one interface across several accounting systems, normalizing entities like customers, invoices, suppliers, accounts, payments, and products. It can cut initial build time. It does not remove integration complexity, and treating it as if it does is a common mistake.
Before committing, ask: Which providers and operations are supported? Do writes work, or only reads? Are writes synchronous or queued? Are provider-specific fields available via passthrough? Is data cached, and how fresh is it? Who handles provider outages and API changes? Is pricing per call, per linked account, per record, or a platform fee? Can you export your mappings and disconnect cleanly? "Unified" rarely means "feature-equivalent across providers."
Embedded ledger
If your platform's core problem is running accounting for your users, not reflecting it into their existing books, an embedded ledger changes the calculation. A marketplace with thousands of sellers, or a SaaS product that needs audit-ready EU VAT and per-country compliance built in, is better served by owning the accounting engine than by wiring N external connectors. That is Nordlet's positioning: real books as an API, immutable double-entry, per-country VAT with VIES validation and i.SAF register generation, SEPA export, and reporting exports in XLSX, PDF, or JSON, all inside your product rather than a third-party checkout.
A decision rule I use:
| Situation | Best fit |
|---|---|
| One accounting provider dominates your customer base; provider-specific behavior is central | Direct connector |
| Broad provider coverage and speed to market matter more than control | Unified API |
| You are a platform or marketplace that needs to run and own the books, with EU VAT and audit trails | Embedded ledger (Nordlet) |
| Mixed reality | Hybrid: unified for common flows, direct connectors for strategic providers |
Recommended integration architecture
Isolate the integration from core business logic
Do not scatter provider calls across billing, orders, subscriptions, and reporting. Build a dedicated integration service holding provider adapters, OAuth connection management, canonical models, ID mappings, sync jobs and queues, webhook endpoints, retry and dead-letter handling, audit logs, and reconciliation tools. This keeps a Xero or QuickBooks field name from leaking into your domain model.
Define a narrow canonical model
Model only the workflows you support. Typical objects: customer, product or service item, invoice, invoice line, payment, credit note, supplier, account, tax rate, currency, journal entry.
Preserve distinctions that affect accounting. Do not flatten draft versus approved versus posted, tax-inclusive versus tax-exclusive amounts, credit notes versus negative invoices, payment allocation versus payment existence, or a customer-facing invoice number versus a provider object ID. Each record should carry SaaS ID, provider ID, provider name and API version, source-of-truth indicator, status, timestamps, last sync time, external reference, and error/reconciliation status.
Maintain explicit mapping tables
Map SaaS tenant to accounting organization, SaaS customer to provider contact, SaaS product to provider item or account, SaaS invoice to provider invoice, SaaS payment to provider payment, tax category to tax code, and revenue category to ledger account.
Never use names or emails as permanent identifiers. Names change, duplicates exist, two customers can share an email. Store the provider's immutable ID and keep the original SaaS ID.
Add state machines
Give a connection explicit states: not connected, authorization pending, connected, initial sync pending, syncing, healthy, rate limited, reauthorization required, provider unavailable, disconnected, permanently failed. Give records their own: pending, sent, confirmed, failed-retryable, failed-needs-user-action, reconciled, conflict detected. A binary "connected/not connected" flag tells your support team nothing useful.
OAuth and connection onboarding
Use the authorization code flow. The customer clicks Connect, your server creates a short-lived authorization transaction, the customer authenticates at the provider and grants scopes, you exchange the code for tokens, discover the authorized organization, store the connection against the correct tenant, and queue an initial sync.
Validate the callback carefully: state against a server-side transaction, exact redirect URI, the provider issuer, and the tenant context. RFC 9700, OAuth 2.0 Security Best Current Practice, formalizes the current guidance: use PKCE with authorization-code flows, avoid implicit and password grants, and use exact redirect-URI matching.
Request least-privilege scopes. Use read-only where you do not need to write. Scope models change, so do not copy old examples. Xero's OAuth documentation notes that broad accounting scopes are being replaced by granular ones, with new apps assigned granular scopes since March 2026 and broad scopes scheduled to remain available until September 2027.
Store tokens server-side only. Never expose them to browser JavaScript, mobile clients, frontend logs, analytics, or customer-facing error messages. Encrypt at rest, redact from logs, and keep per-connection metadata: encrypted access and refresh tokens, expiry, provider org ID, granted scopes, issue time, last successful refresh, revocation status.
Refresh-token rotation is where production breaks
A refresh response may return a new refresh token. You must persist the newest value atomically, or you lock the customer out. Per Xero's developer FAQ, each refresh produces a new refresh token, refresh tokens last up to 60 days, and there is a 30-minute grace period to retry with the existing token if a response is lost. QuickBooks Online tells developers to store the latest refresh token from the most recent response: refresh tokens last up to 100 days, and the value can rotate as often as every 24 hours.
In practice: use a per-connection refresh lock, replace the refresh token atomically, handle transient token-endpoint failures with retries, expose a clear reauthorization state, and never refresh on every API call while a valid access token remains.
Initial synchronization
Run initial sync as a background job. Do not block the OAuth callback or the UI on it.
A robust sequence: fetch organization metadata, retrieve accounts, tax rates, currencies, contacts, and items, store provider IDs and mappings, import only the historical window the product needs, queue dependent records in dependency order, record a checkpoint, and mark the connection healthy only after validation.
Dependency order matters. An invoice may need a contact, an item, a revenue account, a tax code, and a currency to exist first. Do not blindly create duplicates. Match on a stored external ID, a customer-supplied reference, a defined matching rule, or an explicit user-selection step for ambiguous cases.
Incremental synchronization
Treat webhooks as signals, not authoritative records
A webhook usually says something changed. Verify the signature, record the raw event, return the required success response quickly, deduplicate, queue a follow-up fetch, retrieve the current object from the provider, apply the update transactionally, update the checkpoint, and route failures to retry or reconciliation. This handles incomplete payloads and out-of-order events.
Provider rules differ, so do not promise one universal behavior. Xero webhooks carry an HMAC-SHA256 signature in the x-xero-signature header, require HTTPS, retry with decreasing frequency for up to 24 hours, can be disabled after repeated failure, and can save events for up to 31 days for replay after recovery. QuickBooks Online verifies with HMAC-SHA256 and a verifier token, expects HTTP 200 within three seconds, and retries on a fixed schedule that eventually settles into six-hour intervals, with Intuit recommending asynchronous processing.
Deduplication and idempotency
At-least-once delivery is normal. Expect duplicate webhooks, retries after a timeout where the write actually succeeded, reordered events, and duplicate user clicks. Use a provider event ID where available, a unique event key in durable storage, an operation ID generated before each write, database uniqueness constraints, upsert semantics, and a reconciliation query after ambiguous timeouts.
Do not assume every accounting API offers native idempotency keys. If the provider does not, store an internal operation key and check whether the expected remote object already exists before retrying. Patterns from Stripe's idempotent requests and general webhook idempotency guidance transfer well here.
Polling and change detection
Pair webhooks with periodic polling. Webhooks get disabled, delayed, lost to misconfiguration, or limited to certain objects. Xero supports the If-Modified-Since header for changes after a UTC timestamp, but its documentation warns that not every change updates UpdatedDateUTC, so some partially paid transactions and contact fields will not appear via that filter. QuickBooks Online's Change Data Capture tracks changes from the previous 30 days, caps responses at 1,000 objects, returns deleted entities with a Deleted status, and works best with shorter polling windows.
Store a per-provider, per-tenant checkpoint and use overlap windows. Request changes from slightly before the last checkpoint, then deduplicate by provider ID and modification version. This protects against clock skew and partial failures.
Accounting-specific data integrity
This is the part that separates a working integration from a trustworthy one.
Tax behavior is not interchangeable. Xero's documentation notes that invoices, credit notes, and purchase orders default to tax-exclusive when LineAmountTypes is unset, receipts and bank transactions default to tax-inclusive, and manual journals default to no tax. Use the TaxType field, not the tax-rate name. Never assume "20% VAT" uniquely identifies a tax code; it can vary by country, transaction type, effective date, and organization config. This is where EU multi-country VAT gets expensive to get right by hand, and where an embedded ledger with per-country compliance built in earns its place.
Use decimal or fixed-precision arithmetic for money, never binary floating point. Persist currency code and precision, unit amount, quantity, discount, tax amount, net, gross, and exchange rate. Decide clearly whether you send line-level amounts and let the provider compute totals, or send a precomputed total. Mixing the two creates rounding discrepancies your accountant will find.
Reference ledger accounts by provider ID or code, not display name. Before a write, validate that accounts, tax codes, customers, items, and currency are active and supported. A failed invoice write is usually a mapping or configuration problem, not a transient API error, so the UI should explain the fix rather than show "sync failed."
Do not assume object equivalence. A "payment" can mean a receipt, an allocation, a deposit, a bank transaction, or a journal entry. A SaaS "subscription" may not map cleanly to an accounting "invoice." Define the accounting event you want to represent before you pick an API object. See Xero's payments documentation for how one provider models this.
Rate limits and versioning
Limits are usually scoped by tenant, organization, application, or a combination. Per Xero's limits FAQ: five concurrent requests per tenant, 60 per minute per tenant, 5,000 per day per tenant, and 10,000 per minute across the app, with remaining-limit headers and Retry-After on 429s. QuickBooks Online documents 500 requests per minute and 10 per second per realm ID, with a batch endpoint supporting up to 30 operations per request.
Build per-tenant rate limiting, global throttling, exponential backoff with jitter, respect for Retry-After, bounded concurrency, pagination, and metrics on 429s and remaining quota. Do not use unlimited parallelism for imports; one large customer can starve every other job on that tenant.
Pin or explicitly manage API versions. QuickBooks Online defaults to minor version 75 when none is specified. Centralize versions in config, run contract tests against sandboxes, monitor release notes, and treat unknown response fields as forward-compatible. Sandboxes will not reproduce every production condition, so test disconnect, reconnect, revoked consent, expired tokens, rate limits, and provider-side edits before launch.
What I would build first
If I were starting this tomorrow, in order:
- Define the exact accounting workflow: which events and objects you read or write.
- Choose the provider strategy: direct, unified, embedded, or hybrid.
- Build a tenant-scoped connection model with provider org IDs, scopes, status, and token metadata.
- Implement OAuth with authorization code flow, PKCE, state validation, least-privilege scopes, and server-side token storage.
- Build one provider adapter behind a stable internal interface.
- Create ID mappings on immutable provider IDs.
- Implement reads first; import reference data before any write.
- Add an initial sync job with pagination, rate limiting, checkpoints, and dependency ordering.
- Implement one narrow write (create an invoice) only after customer, item, tax, and account mappings validate.
- Make writes retry-safe with internal operation IDs, uniqueness constraints, and remote-state checks.
- Add signed webhooks that acknowledge fast and process asynchronously.
- Add polling and reconciliation so webhooks are acceleration, not the sole source of truth.
- Test failure modes deliberately.
- Add support and audit tooling that makes failures explainable and repairable.
- Expand object coverage gradually.
FAQ
Should I use a unified API or build direct connectors?
Use a direct connector when one provider dominates your customers or provider-specific behavior is central. Consider a unified API when broad coverage and speed matter more than control. If your real need is running the books yourself rather than syncing to a customer's system, neither answers the question; look at an embedded ledger like Nordlet instead.
Are webhooks enough for staying in sync?
No. Webhooks get disabled, delayed, or lost, and payloads can be incomplete or out of order. Treat them as change signals, fetch the current object afterward, and back them with periodic polling and reconciliation.
How do I stop refresh-token failures from locking customers out?
Persist the newest refresh token atomically on every refresh, use a per-connection lock, retry transient token-endpoint failures, and expose a clear reauthorization state. Xero's 30-minute grace period helps, but the atomic persist is what saves you.
Can I match records by customer name or email?
Not as a permanent identifier. Names change, duplicates happen, and two customers can share an email. Store the provider's immutable ID and keep your own SaaS ID alongside it.