Skip to content

QuickBooks Online and the Update That Deletes What It Omits

Eloquent sends the attributes that changed. QuickBooks Online wants the object it should become. Between those two sentences is a supplier record with its address gone.

6 min read

A QuickBooks Online integration ships. Three weeks later a supplier record has no billing address, no payment terms and no tax code, and the audit log says the last person to touch it only changed an email address.

Nothing is broken. The integration did exactly what it was told, and what it was told was the default behaviour of two systems that disagree about what an update means.

Eloquent sends changes, the API wants the object

A model's save() sends the attributes that are dirty. That is the right behaviour for a database and the wrong payload for this API, which treats the body of an update as the record it should become. Fields you omit are not left alone - they are cleared.

So the mapping layer cannot be a thin translation of a model's changed attributes. It has to produce a complete object, which in practice means reading the current record from QuickBooks, applying the change to it, and sending the result.

The API does support a partial update, and it is worth using. It is not worth relying on alone, because it is a flag somebody can forget on the one call that matters. Reading before writing costs a request and removes a class of silent data loss, and in code that edits somebody's books that is a trade worth making without discussion.

The structural conclusion is that this integration needs its own mapper rather than a method on the model. The object sent to the accounting system is not the model, it is a document assembled from it, and treating those as the same thing is where this failure comes from.

The version makes a blind retry wrong

Every record carries a version, and an update has to send the version it was based on. If the record moved in the meantime, the update is rejected.

That is good behaviour and it breaks the default retry. A queued job that holds a finished payload and resends it after a failure is sending a version that is already stale, so it fails again, and again, until the attempts are exhausted - and the change is lost in a failed-jobs table rather than applied.

The job therefore has to store the intention rather than the payload. What it holds is "set this customer's email to this value", not a serialised request body. On each attempt it reads the current record, applies the intention, and sends the result with the version it just read.

That makes the job genuinely safe to run twice, which matters more than usual here because Laravel's queue will retry whether or not anybody designed for it.

The second half of the version question is what to do when the conflict is real - when somebody in the finance team changed the same field. Overwriting is a decision, not a default. For most fields the honest answer is that the accounting system wins and your application records that its change was refused, because a silent overwrite of a bookkeeper's correction is the kind of behaviour that ends an integration's credibility in one meeting.

The connection is a company, and it is not the user

Authorisation is granted for a specific company file, and one login can grant several. So the thing you store is a connection - the company identifier, the tokens, when they were refreshed, and whether it is currently healthy - in its own table.

Not columns on a user. Not a single row in a settings table. The company identifier is what you look up by, because every subsequent request is on behalf of one company and the application has to know which.

The token refresh has the same hazard as every other accounting API, which is two workers refreshing at the same moment and invalidating each other. That failure and its remedy are set out in the Xero piece, and the mechanics are close enough that repeating them here would waste your time. The durations differ - check the current ones in Intuit's documentation rather than hard-coding what a blog post said - and the shape of the problem does not.

One QuickBooks-specific addition is worth knowing: the refresh token expires through disuse as well as through age. An integration for a customer who stopped using your application for a few months comes back disconnected, with nothing having gone wrong. A disconnected state has to be a real state in your schema, with something in the interface that explains it, rather than an exception that surprises somebody.

Matching customers is a table, not a lookup

QuickBooks requires a customer's display name to be unique within a company. That single constraint is responsible for most of the pain in these projects.

Your database has "Acme Ltd". Theirs has "Acme Limited", and also "ACME LTD - DO NOT USE" from 2019, and also "Acme" with a trailing space. Matching on a name means creating a duplicate the first time somebody types the suffix differently, and duplicates in an accounting system are a cleanup job for a bookkeeper rather than a bug you can fix.

So the pairing lives in a mapping table - your identifier, their identifier, the company it belongs to - and it is populated by a matching exercise somebody reviews once rather than by a string comparison running forever.

That exercise is not engineering work and it is most of the elapsed time. It also surfaces the question nobody asked: which system owns a customer's name, and what happens when it changes on the other side.

Decide who numbers the invoices

The last decision is commercial and has to be made before anything is written.

If QuickBooks assigns invoice numbers, your application cannot display a number until the push has succeeded, which changes what a customer sees and what happens when the accounting system is unavailable.

If your application assigns them, somebody has to turn off automatic numbering in QuickBooks, and gaps in the sequence become your responsibility to explain.

Both are workable. What is not workable is both systems numbering, which produces two invoices for one sale and a reconciliation nobody can finish. It is a five-minute conversation in week one and a fortnight of unpicking in month four.

When not to build this

If invoices need to appear in QuickBooks, nothing needs to come back, and the volume is tens per month, a managed connector is the right answer and it works this afternoon.

A built integration earns its place when the mapping is specific to how your business works, when the volume makes manual correction expensive, or when the accounting data has to be inside your own system to be queried. It does not earn its place as a way to avoid a subscription, because the reconciliation rules are where the cost is and they are the same either way.

Related questions

Can we map our Customer model straight onto QuickBooks customers?
Not by name, which is the mapping everybody reaches for first. QuickBooks requires the display name to be unique inside a company, so your records and theirs will differ in punctuation, legal suffixes and a decade of inconsistent typing - and matching on a string means creating duplicates every time somebody writes Ltd instead of Limited. What works is a mapping table holding your identifier, their identifier and the company the pairing belongs to, populated once by a human-reviewed matching exercise.
What happens when a queued job retries with a stale version?
It fails, repeatedly, until the attempts run out - because the version it is sending was already superseded. A retry that resends the same payload is wrong by construction here. The job has to refetch the current record, re-apply the intended change and send that, which means the job stores the change rather than the finished payload. That is the difference between an integration that recovers and one that fills a failed-jobs table.
Who owns the invoice number?
One system, decided before anything is built, and it is a commercial question rather than a technical one. If QuickBooks numbers the invoices then your application cannot show a number until the push succeeds, which affects what the customer sees. If your application numbers them then somebody has to turn off automatic numbering in QuickBooks and accept that gaps are your responsibility. Both work. Having both number things is the version that produces two invoices for one sale.
How long does this take to build?
The authorisation, the first successful invoice and the webhook handler are about a fortnight. The rest is the mapping - which customer is which, what happens to a credit note, how a part payment is applied, which side wins when both changed something. That part is a series of decisions with the finance team, and it is the part that determines whether anybody trusts the result.

← Back to all articles

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