Almost every operational app a small business builds ends up needing the accounting system. It needs the customer list, the item list, the open invoices, or it needs to create an invoice at the end of a job. This is the point where a straightforward project acquires the ability to do real damage, because a duplicate customer in an operations tool is an annoyance and a duplicate invoice in the books is a phone call from your accountant.
The specifics below use QuickBooks vocabulary because that is what most US small businesses run, but the rules apply to Sage, Xero, or whatever your bookkeeper has been using since 2011.
The one rule
Almost every integration disaster traces back to blurring that line — an app that maintains its own idea of customer balances, an operations tool that decides on its own to void something, a sync that treats a closed period as writable. Keep the boundary sharp and the rest is mechanical.
Decide the direction for every object
Bidirectional sync sounds appealing and is where most of the pain lives. For each object, pick one owner and make the other side read-only. Write it down and keep it somewhere both your team and whoever maintains the integration can see.
| Object | Owner | Flow |
|---|---|---|
| Chart of accounts, items, tax codes | Accounting | Read into the app, never written |
| Customers | Accounting, usually | Read in; new ones created via a reviewed path |
| Jobs, schedules, crews, field data | Custom app | Never enters the books as an object |
| Estimates and change orders | Custom app | Optional push, often not worth it |
| Invoices | Accounting | Created by the app through a queue, then read back |
| Payments and deposits | Accounting | Read into the app for status only |
| Time entries | Custom app | Pushed to payroll on an explicit approval |
The row people argue about is customers. Letting an operational app create customers freely is how a business ends up with "Smith, John", "John Smith", and "J Smith (new house)" and a receivables report nobody trusts. The workable middle is that the app searches the accounting customer list first, and creating a new one is an explicit action that a person confirms.
How writes should work
01
Build the proposal, do not write directly
When a job is ready to bill, the app produces an invoice proposal — lines, quantities, tax treatment, customer, dates — as an object someone can look at. Nothing has touched the books yet.
02
Validate against the books' own rules
Every item must exist in the item list, the customer must exist, the tax code must be valid, the date must not fall in a closed period. Catching this before the write turns an accounting error into a form error.
03
Queue it as an idempotent operation
Each write carries a key derived from the job, so a retry after a timeout cannot create a second invoice. This one detail prevents the most common and most embarrassing failure in accounting integrations.
04
Apply, log, and store the returned identifier
The accounting system's own ID comes back and gets stored on the job. From that moment the two records point at each other and nothing has to be matched by name or amount ever again.
05
Read back to confirm it landed
Never trust a successful HTTP response as proof. Read the invoice back and compare the total. Connections drop after the write and before the response more often than you would like.
06
Surface failures to a person, immediately
A failed write becomes a visible task with the reason, not a log line. Silent failures are how a business discovers in March that nothing has synced since January.
The things that bite
- Closed periods. Your bookkeeper closes a month; your app tries to write an invoice dated inside it. Check the closing date before writing and route anything blocked to a human rather than silently re-dating it.
- Sales tax. Tax is jurisdictional, rate-changing, and occasionally wrong in ways that are legally yours. Let the accounting system or a dedicated tax service compute it. Do not implement tax logic in an operations app.
- Rate limits and sandbox drift. Accounting APIs throttle, and the sandbox never behaves exactly like the production company file. Design the sync to run in batches with backoff, and test against a copy of the real file before go-live.
- Multi-entity setups. Two companies, two files, one operations team is common in the trades and it doubles the mapping work. Find out on day one; discovering it mid-build changes the design.
- Deletions and voids. These are accounting decisions with consequences. The app should be able to request a void through the same reviewed path, never perform one automatically.
- The bookkeeper's manual habits. Someone has been renaming customers, using the memo field as a status code, or entering journal adjustments by hand. Learn these before you sync, because your integration will otherwise fight them every month.
Talk to whoever closes the books before you write a line of integration code. They know a dozen conventions that exist nowhere in the schema.
Reconciliation is a feature, not an afterthought
Every long-running integration drifts. Someone edits an invoice by hand, a sync fails during a maintenance window, a record gets merged. The question is not whether drift happens but whether you find it in days or at year end.
A nightly reconciliation job that compares what the app believes against what the books actually contain — invoice totals, statuses, customer identifiers — and reports a short list of discrepancies to a person is the cheapest insurance in the whole project. It is a day of work and it repays itself the first time it catches something.
Two more habits worth adopting. Keep a permanent record of every write with its payload and response, so any question about "where did this invoice come from" has an answer. And make the first two weeks read-only: sync data in, produce proposals, let a person create the invoices by hand while you compare. The write path turns on once the proposals have been right for a while.
That staged approach — read first, write later, reconcile always — is the same pattern that applies to any system you do not control, and it is described more generally in connecting a legacy system without replacing it.
Frequently asked questions
Should our custom app create invoices in QuickBooks automatically?
Eventually, yes, but not on day one. Start by generating invoice proposals that a person reviews and enters, and compare the proposals against what gets created. Once they have been consistently right, turn on the write path with idempotency keys and read-back confirmation so a retry can never produce a duplicate invoice.
Which system should own the customer list?
The accounting system, in almost every case, because that is where receivables live and duplicates are most expensive. The operational app should search the accounting list before creating anything, and new customers should be an explicit, confirmed action rather than an automatic side effect of taking a job.
What breaks most often in accounting integrations?
Duplicate writes after a timeout, invoices dated into a closed period, items or tax codes that do not exist in the books, and silent sync failures nobody notices for weeks. Idempotency keys, a closing-date check, pre-write validation, and a nightly reconciliation report handle all four.
Should the app calculate sales tax?
No. Tax is jurisdictional, changes frequently, and errors are legally yours. Let the accounting system or a dedicated tax service compute it, and have the operations app carry the tax code rather than the logic.