Accounting for Developers: The 2026 Guide

The accounting concepts developers actually need when building API integrations. Chart of accounts, invoices, reconciliation, tax, and multi-currency with code examples.

Bernard WillemsBernard Willems

Bernard Willems

19 min read
Accounting for Developers: The 2026 Guide

Last year, a developer on our team spent three days debugging why invoice totals in QuickBooks Online didn't match what our connector was sending. The amounts were correct. The tax rates were correct. The line items were correct. The problem? The chart of accounts mapping defaulted to an expense account instead of a revenue account. Every invoice landed on the wrong side of the books.

This is the kind of bug that accounting software integrations produce. Not a 500 error. Not a timeout. A silent, structurally valid write to the wrong place, discovered six weeks later during a month-end close.

If you're building software that connects to accounting systems, you need a working understanding of how accounting actually works. Not because you're going to become a CPA. Because the data model of every accounting API is shaped by accounting rules, and if you don't know those rules, you will build things that look right and are wrong.

This guide covers the accounting concepts that matter for integration developers. Not all of accounting. The parts that affect how you read, write, and sync financial data through APIs.

Double-entry is a constraint, not a concept

Every accounting system you'll integrate with enforces double-entry bookkeeping. This is the single most important thing to internalize because it shapes every API call you'll make.

The rule is simple: every financial transaction must have at least two entries that balance to zero. Money comes from somewhere and goes somewhere. If you debit one account for $500, you must credit another account (or combination of accounts) for $500. No exceptions. The system won't accept an unbalanced entry.

For developers used to working with CRMs or HRIS systems, this is a different world. In a CRM, you can create a contact with whatever fields you want and the system accepts it. In an accounting system, creating a transaction means satisfying a mathematical constraint. If your payload doesn't balance, the API rejects it.

This has practical consequences. When you're syncing invoices from your platform into a customer's accounting system, you're not just pushing a document. You're creating a set of balanced entries: a debit to accounts receivable (the customer owes money) and a credit to a revenue account (income was earned). If your integration doesn't specify the right accounts for both sides, the system either rejects the request or, worse, picks defaults that put the data somewhere unhelpful.

Modern Treasury published a solid primer on double-entry fundamentals in their "Accounting for Developers" series, covering the mechanics of debits and credits in detail. That's worth reading if you want the theoretical foundation. What follows here focuses on how those mechanics translate into integration code.

The chart of accounts: your most important mapping problem

The chart of accounts (COA) is a numbered list of every account in a company's books. It's the taxonomy that organizes all financial data, the schema of the accounting database. Every transaction references accounts from this list.

Accounts fall into five classifications:

Assets (what the company owns: cash, receivables, inventory), Liabilities (what the company owes: loans, payables, credit cards), Equity (ownership value: retained earnings, owner investment), Revenue (income earned from operations), and Expenses (costs incurred to operate).

The first three appear on the balance sheet. The last two appear on the income statement. This distinction matters for integrations because writing data to the wrong classification means the customer's financial statements are wrong.

Here's where it gets complicated for developers. Every company's chart of accounts is different. A construction company and a SaaS company both use QuickBooks, but their account structures look nothing alike. Your integration can't hardcode account IDs. You need to either let users map accounts during setup, or use the accounting platform's API to look up accounts by type and classification at runtime.

QuickBooks Online, for example, exposes account objects with a Classification field and an AccountType field that's more specific. A revenue account in QBO looks something like this:

{
  "Id": "1",
  "Name": "Sales of Product Income",
  "Classification": "Revenue",
  "AccountType": "Income",
  "AccountSubType": "SalesOfProductIncome",
  "CurrentBalance": 0,
  "Active": true
}

The same account in Xero comes back with a different structure:

{
  "AccountID": "ab72e8e0-7b21-4b92-a248-0cf4ee38c6f7",
  "Name": "Sales",
  "Type": "REVENUE",
  "Class": "REVENUE",
  "Status": "ACTIVE",
  "TaxType": "OUTPUT"
}

Notice the differences. QBO gives you AccountType and AccountSubType as separate fields. Xero attaches a TaxType directly to the account. A unified API like Apideck normalizes these into a single schema:

{
  "id": "1",
  "nominal_code": "4000",
  "name": "Sales",
  "type": "income",
  "classification": "revenue",
  "status": "active",
  "currency": "USD"
}

If you're building a multi-platform integration without a normalization layer, you're maintaining separate mapping logic for every platform. This is one reason unified accounting APIs exist.

One more thing about the COA: it's not static. Customers add accounts, rename them, deactivate them. Your integration needs to handle stale account references gracefully. A mapping that worked during onboarding might break three months later because the customer's bookkeeper reorganized their chart of accounts.

Invoices, bills, and the AR/AP lifecycle

Most accounting integrations start with invoices. If your platform generates charges, subscriptions, orders, or fees, the most common integration request is "sync this into my accounting system."

Two terms to keep straight:

Invoices vs bills

An invoice is a request for payment sent by a seller. It creates an accounts receivable entry: the customer owes you money. A bill is a request for payment received by a buyer. It creates an accounts payable entry: you owe someone money. In many accounting APIs, invoices and bills are the same object with a type flag. Merge's unified API, for instance, uses an Invoice object with a type field set to either accounts_receivable or accounts_payable.

Each invoice or bill contains line items. A line item connects the transaction to a specific account in the chart of accounts (usually a revenue or expense account) and includes quantity, unit price, tax, and description. The line items must reconcile to the invoice total, and the journal entry the system generates behind the scenes must balance.

Here's what creating an invoice looks like through the Apideck Accounting API:

POST /accounting/invoices

{
  "type": "accounts_receivable",
  "number": "INV-0042",
  "customer": {
    "id": "cust_8a3b1c"
  },
  "invoice_date": "2026-04-01",
  "due_date": "2026-05-01",
  "line_items": [
    {
      "description": "Monthly platform subscription",
      "quantity": 1,
      "unit_price": 299.00,
      "ledger_account": {
        "id": "acc_revenue_4000",
        "nominal_code": "4000"
      },
      "tax_rate": {
        "id": "tax_standard_10"
      }
    }
  ],
  "currency": "USD"
}

That ledger_account on the line item is where most integration bugs live. If it points to the wrong account, the invoice gets created successfully but the revenue shows up in the wrong place in the customer's financial statements. The API won't stop you because the payload is structurally valid.

The lifecycle matters for your integration. An invoice moves through states: draft, submitted, authorized, paid, voided. Not every accounting platform uses the same state names, but the pattern is consistent. Your integration needs to decide: at what state do I create the invoice in the accounting system? Do I update it when the status changes? What happens if the invoice is voided after it's been synced?

Syncing payments

Payment objects connect to invoices. When a customer pays an invoice, a payment record is created that references the invoice and records which bank or cash account received the funds. The accounting system then marks the invoice as paid and creates the corresponding journal entry (debit cash, credit accounts receivable).

A payment sync looks like this:

POST /accounting/payments

{
  "total_amount": 328.90,
  "transaction_date": "2026-04-15",
  "customer": { "id": "cust_8a3b1c" },
  "account": { "id": "acc_checking_1000" },
  "invoices": [
    {
      "id": "inv_0042",
      "amount": 328.90
    }
  ],
  "reference": "pmt_stripe_pi_3abc123"
}

The account field specifies which bank account the money landed in. The invoices array links this payment to one or more open invoices. That reference field (a Stripe payment intent ID in this case) is what makes reconciliation possible later. If your platform processes payments, syncing both the invoice and the payment is what gives the customer a complete picture. Syncing invoices without payments leaves open receivables that the customer's bookkeeper has to close manually.

Journal entries: the escape hatch

When the transaction you need to record doesn't fit neatly into invoices, bills, or payments, journal entries are the fallback. A journal entry is the most primitive write operation in accounting: a set of lines, each referencing an account, each marked as debit or credit, summing to zero.

Here's a journal entry that records a $500 year-end accrual (recognizing revenue that was earned but not yet invoiced):

POST /accounting/journal-entries

{
  "title": "Revenue accrual - December 2025",
  "journal_date": "2025-12-31",
  "line_items": [
    {
      "description": "Accrued consulting revenue",
      "type": "debit",
      "amount": 500.00,
      "ledger_account": { "id": "acc_1200" }
    },
    {
      "description": "Accrued consulting revenue",
      "type": "credit",
      "amount": 500.00,
      "ledger_account": { "id": "acc_4000" }
    }
  ]
}

The debit to account 1200 (accrued receivables, an asset) and the credit to account 4000 (revenue) must sum to zero. If you remove one line or change an amount so they don't balance, the API rejects the entry.

Journal entries are useful for adjustments (correcting a prior entry), accruals (recording revenue or expense before cash moves), reclassifications (moving an amount from one account to another), and any complex transaction your platform needs to represent that doesn't map cleanly to the accounting system's higher-level objects.

The tradeoff is that journal entries are powerful but opaque. An invoice carries semantic meaning: who the customer is, what was sold, payment terms, due date. A journal entry is just numbers and account codes. If your integration relies heavily on journal entries instead of using the appropriate transaction objects, customers will struggle to understand what the entries represent when they review their books.

Use the highest-level object that fits your use case. Fall back to journal entries when you have to.

Authentication: OAuth is the gatekeeper

Before your integration can read or write anything, it needs to authenticate with the customer's accounting system. Nearly every major accounting platform uses OAuth 2.0 for third-party access, and the implementation details vary enough to cause problems.

The OAuth flow for accounting APIs

The general pattern: your application redirects the user to the accounting platform's authorization page, the user grants permission, the platform returns an authorization code, and your application exchanges that code for an access token and a refresh token. The access token is what you attach to every API call. The refresh token is what you use to get a new access token when the current one expires.

QBO access tokens expire after one hour. Xero tokens expire after 30 minutes. Sage Intacct uses session-based auth with a different pattern entirely. If your integration doesn't handle token refresh correctly, it will work during testing and break in production the first time a token expires mid-sync.

Scopes and permissions

Most platforms let users control what your integration can access. Xero moved to granular OAuth scopes in 2024, meaning you now request specific permissions like accounting.transactions.read or accounting.contacts.write instead of getting access to everything. QBO uses similar scoping. Your onboarding flow needs to request only the scopes you need, and your integration needs to handle the case where a user grants partial access.

A unified API like Apideck handles the OAuth flow and token refresh for you across all connected platforms. But understanding what happens underneath matters for debugging.

Webhooks and sync strategies

Once authenticated, you need to decide how your integration keeps data in sync between your platform and the accounting system.

Push vs poll

There are two approaches. Polling means your integration periodically calls the accounting API to check for changes (new invoices, updated payments, modified accounts). Webhook-based sync means the accounting platform notifies your integration when something changes.

QBO supports webhooks that notify you of entity changes. Xero has webhooks for key events. But not every platform supports webhooks, and those that do often have limitations on what events they cover. Many integrations use a hybrid: webhooks for real-time awareness, with periodic polling as a fallback to catch anything the webhooks missed.

Idempotency and deduplication

Whichever approach you use, your sync logic must be idempotent. Webhooks can fire more than once for the same event. Polling can encounter the same record on consecutive runs. If your integration creates a duplicate invoice because it processed the same event twice, the customer now has a reconciliation problem. Store the external ID of every synced object and check for it before creating new records.

Reconciliation: where integrations actually fail

Reconciliation is the process of comparing two sets of records to ensure they agree. In practice, it means comparing what your platform says happened with what the accounting system shows.

This is the hard problem. Not conceptually hard. Operationally hard.

Here's why. Your platform and the accounting system will drift. Timing differences are the most common cause: your system records a transaction on March 31 but the accounting system receives it on April 1, which might put it in a different reporting period. Rounding differences in tax calculations produce one-cent discrepancies that accumulate. Partial payments create mismatches between expected and actual amounts. Voided or refunded transactions that aren't synced back create orphaned entries.

Bank reconciliation adds another layer. The customer's bank statement shows actual cash movement. The accounting system shows recorded transactions. Matching these two is something accountants do monthly (or daily, for larger companies). If your integration introduces data that doesn't match bank records, you've created a reconciliation problem for the customer.

The practical takeaway: your integration needs to be idempotent, handle retries without creating duplicates, timestamp everything, and provide a way for customers (or their accountants) to trace any entry in the accounting system back to the source event in your platform. Unique external IDs on every synced object are not optional.

Multi-entity and multi-currency

In 2026, SaaS platforms building accounting integrations increasingly serve customers who operate across multiple legal entities and currencies. A property management company with LLCs in three states. A marketplace with sellers across a dozen countries. A travel tech platform processing bookings in local currencies across dozens of markets.

Multi-entity accounting means the same parent company maintains separate books for each subsidiary or legal entity, plus a consolidated view. Sage Intacct is built for this natively. QuickBooks Online handles it through separate company files or through the Advanced tier's multi-entity features. Xero uses a multi-org structure. Each has different API implications for how you scope your integration.

The key question for your integration: does your data need to land in one entity or be split across several? If a single transaction in your platform spans multiple entities (a transfer between subsidiaries, an intercompany charge), you may need to create entries in multiple company books and matching elimination entries for consolidation.

Multi-currency adds its own complexity. Accounting systems require a base currency (also called functional currency or home currency). Foreign currency transactions must be recorded at the exchange rate on the transaction date, and the system tracks unrealized gains and losses as exchange rates fluctuate. Your integration needs to pass currency codes and exchange rates, not just amounts. If you send an amount without specifying the currency, most systems will assume the base currency, and if that's wrong, the books are wrong.

Compare these two invoice payloads. The first is what most developers send initially:

{
  "line_items": [
    { "unit_price": 250.00, "quantity": 1 }
  ]
}

The system assumes the company's base currency (say, USD). If the actual transaction was in EUR, the books are now wrong by whatever the exchange rate difference is. The correct version includes currency and rate:

{
  "currency": "EUR",
  "currency_rate": 1.08,
  "line_items": [
    { "unit_price": 250.00, "quantity": 1 }
  ]
}

That currency_rate of 1.08 means 1 EUR = 1.08 USD. The accounting system uses this to compute the base currency equivalent ($270.00) and records both amounts. When the exchange rate changes before the invoice is paid, the system records the difference as a foreign exchange gain or loss automatically.

Tax: the field that breaks everything

Tax handling varies so dramatically across accounting platforms, jurisdictions, and business types that it deserves its own warning.

At minimum, your integration needs to handle: tax rates that differ by product type and customer location, tax-exempt customers, tax-inclusive versus tax-exclusive pricing, and the accounts to which tax amounts are posted. A line item total of $100 with 10% tax-exclusive means $110 hits the accounting system ($100 to revenue, $10 to a tax liability account). The same $100 with tax-inclusive pricing means $90.91 to revenue and $9.09 to tax. If your integration doesn't distinguish between these two models, the revenue figures and tax liabilities will both be wrong.

Tax-inclusive vs tax-exclusive

In API terms, the difference looks like this. Tax-exclusive (common in the US):

{
  "line_items": [
    {
      "unit_price": 100.00,
      "tax_rate": { "id": "tax_10_percent" },
      "tax_amount": 10.00,
      "total_amount": 110.00
    }
  ],
  "total": 110.00
}

Tax-inclusive (common in the EU, UK, Australia):

{
  "line_items": [
    {
      "unit_price": 100.00,
      "tax_rate": { "id": "vat_20_percent" },
      "tax_amount": 16.67,
      "total_amount": 100.00
    }
  ],
  "total": 100.00
}

Notice the tax-inclusive line item has a total_amount equal to the unit_price because the tax is already inside that number. The accounting system needs to unbundle it: $83.33 to revenue, $16.67 to a VAT liability account. If your integration sends tax-inclusive amounts as if they were tax-exclusive, the customer's revenue is overstated and their tax liability is understated. Accountants catch this. Usually not quickly.

QuickBooks Online has its own automated sales tax engine for US transactions. Xero handles tax through tax rates attached to line items with region-specific behavior. International platforms must contend with VAT reverse charges, withholding tax, and GST/HST rules that change which party records the tax liability. If your platform operates across both cash-based and accrual accounting regimes, the tax timing gets even more complex.

The safest pattern is to pass tax amounts explicitly as computed by your platform, rather than relying on the accounting system's tax engine to recalculate. This avoids discrepancies between what your platform charged the customer and what the accounting system records. But some accounting platforms prefer to own tax calculation. Understand your target platforms and offer configuration options.

Building with accounting APIs in 2026

If you're building accounting integrations in 2026, here's the compressed version of what to get right. (For a deeper operational checklist, see our best practices for accounting integrations in vertical SaaS.)

Map to the chart of accounts dynamically. Never hardcode account IDs. Use the highest-level transaction object available (invoices over journal entries) for readability. Sync payments alongside invoices to close open receivables. Assign unique external IDs to every object so you can reconcile and deduplicate. Handle tax explicitly, including the distinction between tax-inclusive and tax-exclusive amounts. Support multi-currency by always passing currency codes and exchange rates. Build for multi-entity customers even if most of your current users are single-entity.

QuickBooks still commands over 60% of the US small business accounting market, but your integration will encounter dozens of other platforms. Each has its own data model, its own API idioms, its own rules about required fields. A unified accounting API (like Apideck's, which normalizes across these platforms through a single integration point) reduces the surface area of this problem, but understanding the accounting concepts underneath is what prevents the subtle, silent bugs that erode trust. If you're weighing whether to build or buy your accounting integrations, the concepts in this guide are exactly what you'd need to maintain yourself.

The best accounting integrations are the ones the customer's accountant never has to think about. That only happens when the developer who built them understood what the accountant expects to see.

If you want to see where AI is reshaping the accounting stack on top of these same integration patterns, that's the next layer of the story.

Frequently asked questions

What is the difference between an invoice and a bill in accounting APIs?

An invoice and a bill represent the same transaction from two different perspectives. An invoice is created by the seller and records accounts receivable (money owed to you). A bill is created by the buyer and records accounts payable (money you owe someone). In most accounting APIs, both use the same object with a type field set to accounts_receivable or accounts_payable.

Do all accounting APIs enforce double-entry bookkeeping?

Yes. Every major accounting platform (QuickBooks, Xero, Sage, NetSuite, FreshBooks) enforces double-entry at the API level. If you submit a journal entry where debits and credits don't balance to zero, the API will reject it. Higher-level objects like invoices handle the balancing for you by generating the underlying journal entries automatically.

How do I handle multiple accounting platforms in one integration?

You can either build and maintain separate integrations for each platform, or use a unified accounting API that normalizes data across platforms into a single schema. Building individually gives you full control but requires maintaining separate authentication, data mapping, and sync logic for every platform. A unified API trades some flexibility for speed and lower maintenance cost.

Ready to get started?

Scale your integration strategy and deliver the integrations your customers need in record time.

Ready to get started?
Talk to an expert

Trusted by fast-moving product & engineering teams

JobNimbus
Blue Zinc
Drata
Octa
Nmbrs
Apideck Blog

Insights, guides, and updates from Apideck

Discover company news, API insights, and expert blog posts. Explore practical integration guides and tech articles to make the most of Apideck's platform.