Skip to content

A QuickBooks Integration That Does Not Erase Fields

QuickBooks Online replaces the whole object on an update unless you ask it not to, and every record carries a version you have to send back. Both of those land on the form, not on the API client.

5 min read

A QuickBooks Online integration goes live. A week later a customer notices that a supplier record has lost its billing address, its payment terms and its tax code. Nobody deleted them. Somebody edited the supplier's email address through a form in your application.

That is the first thing to know about this particular API, and it is the one that reaches furthest into the front end.

An update is a replacement

Most APIs treat a request body as the fields you want changed. QuickBooks Online, by default, treats it as the object you want the record to become. Anything you leave out is not unchanged - it is empty.

A form in a Next.js application is exactly the thing that walks into this. The edit screen knows about three fields because three fields are what the user needs to change. The Server Action receives three fields and sends three fields. The accounting system now has a supplier with three fields.

There are two defences and it is worth having both. The request can state that it is a partial update, which is a parameter on the call rather than something clever. And the handler can read the current record, apply the change to it, and send the whole thing back - so the payload is complete regardless of how the API chooses to interpret it.

The second one costs a round trip. It also means the behaviour does not depend on remembering a flag, which is the kind of trade worth making in code that touches somebody's books.

The version belongs on the form

Every record in QuickBooks Online carries a version. An update has to send the version it was based on, and if the record has changed since, the update is rejected.

This is correct and it is the behaviour you want. It is also a requirement that lands on the form rather than on the API client, which is where teams get it wrong.

The page loads a record at version 7 and renders a form. The user spends four minutes typing. Meanwhile somebody in the finance team edits the same record in QuickBooks and it becomes version 8. The form submits with version 7 and is refused.

The wrong response is to catch that, fetch the current version and resend - because that silently discards the finance team's change. The right response is to tell the person what happened and show them what changed. That means the version has to be part of the form's state, carried through the Server Action, and the conflict has to be a visible outcome rather than an exception in a log.

Which, usefully, is the same optimistic concurrency pattern any multi-user form needs. The accounting API just makes it compulsory.

Where each piece of the integration actually lives

The architecture question is the same as for any accounting integration, and the answers are worth restating for this one because QuickBooks adds a wrinkle.

The authorisation callback is a Route Handler. Along with the authorisation code, QuickBooks returns the identifier of the company that was authorised, and both have to be captured in that one request - the code is useless without knowing which company it belongs to. The state parameter has to be verified rather than read, because that is what stops somebody else's authorisation being attached to your user's account.

The tokens go in a table keyed by that company identifier. Not on the user, not in a cookie, not in anything a client component can reach. One person can authorise several companies, which makes the connection the record and the user a reference on it.

Routine reads and writes belong in Server Actions and server components, where the credentials exist and the browser never sees them.

The long work does not. A Server Action is a request with a timeout, and a first import, a reconciliation run or a backfill is not a request. Those go to a queue: the Server Action creates a job record, returns immediately, and the page shows progress. This is the single most common structural mistake in these integrations and it is invisible in development, where the test company has eleven invoices.

Webhooks tell you the identifier and nothing else

A QuickBooks webhook arrives at a Route Handler, is signed, and says that an entity of a given type and identifier changed. It does not say what changed or what the record now contains.

So the handler verifies the signature, writes the notification down, returns quickly, and a job fetches the record afterwards. Doing the fetch inline means the webhook endpoint's response time is now a function of the accounting API's response time, and a slow API becomes failed deliveries.

The notification can also arrive twice, and it can arrive out of order relative to the change you just made yourself. Both are ordinary. The handler being safe to run twice - and ignoring a notification about a version it already holds - is what makes the difference between a sync that converges and one that oscillates.

Matching the customer is the project

The technical work above is a fortnight. The rest of the time goes somewhere that does not look like engineering.

QuickBooks requires the display name of a customer to be unique within a company, which means your application's "Acme Ltd" and the accounting system's "Acme Limited" are two different customers and will stay that way until somebody decides which is which. Multiply that by two thousand records and a decade of inconsistent data entry.

That matching exercise - which side owns a name, what happens to the near-duplicates, which field is the key - is a conversation with the finance team, not an engineering estimate. Teams that treat it as a data import problem discover it three weeks before launch.

When not to build this

If the requirement is that invoices appear in QuickBooks and nothing flows back, and the volume is tens per month, a managed connector will do it this afternoon for a subscription fee. Building a two-way integration is worth it when the mapping is specific to your business, when the volume makes manual correction expensive, or when the data has to be in your own system to be useful.

It is not worth it to avoid a small monthly cost. The reconciliation rules are where the time goes, and they do not get cheaper because you wrote the API client yourself.

Related questions

Why did editing a customer's email clear their address?
Because QuickBooks Online treats an update as a replacement of the whole object unless the request says otherwise. A form that knows three fields sends three fields, and everything absent is now absent in the accounting system. The fix is to send the update as a partial one explicitly, and the safer habit on top of that is to read the current record, apply the change to it and send the result, so the request is complete whichever way the API interprets it.
What is the SyncToken for?
It is the record's version, and an update has to send the one it was based on. If somebody changed the record in QuickBooks since your page loaded it, your token is stale and the update is rejected - which is correct behaviour and exactly what you want. The implication for the front end is that an edit form has to carry the version it loaded and show a real conflict message, rather than refetching the token and overwriting whatever the other person did.
Can a Server Action do the whole sync?
Only if it finishes inside a request. A Server Action is a request, so anything measured in minutes - a first import, a full reconciliation, a backfill - belongs in a queue with the Server Action starting it and returning. The pattern that works is a job record created immediately, a page that shows its progress, and the long work happening somewhere a timeout cannot kill it halfway.
Where do the tokens live?
Server-side only, in a table keyed by the company they authorise, never in a cookie the browser can read and never in anything a client component touches. One login can authorise more than one company, so the connection rather than the user is the thing you store and look up by. That is the same shape as any accounting integration and the reason it is a table rather than two columns on a user.

← Back to all articles

Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page