RevOps HQ
← BACK TO BLOG
8/5/2026
Integrations

HubSpot and QuickBooks Online - How to Match Customer Records and Keep Them Matched

The identity layer under a HubSpot-QuickBooks sync - why DisplayName collisions reject writes, how to store the QuickBooks Id on the HubSpot company, and the reconciliation procedure to run before enabling continuous sync.

P

Paul Maxwell

AUTHOR

GET WEEKLY REVOPS INSIGHTS

No spam. Unsubscribe anytime.

A HubSpot–QuickBooks sync has to decide whether a company in HubSpot and a customer in QuickBooks are the same organisation. Every other part of the integration depends on that answer, and the platforms give you almost nothing to work with: no shared identifier, and two incompatible ideas of what a name is.

This covers the identity layer only, because invoice sync, payment status, item mapping and tax each depend on identity being settled first and are better treated separately once it is.

Terms

Entity — QuickBooks' word for a record type. Customer, Vendor and Employee are three entities that, importantly, share one name namespace.

DisplayName — the QuickBooks Customer field that appears on invoices and in the customer list. It is the name QuickBooks treats as the record's identity.

SyncToken — an integer on every QuickBooks entity, incremented on each modification. Any update must send the current value or the write is rejected. This is optimistic concurrency control, not an identifier.

Sparse update — a QuickBooks update that sends only the fields you intend to change. Requires Id and SyncToken. Fields omitted from the payload are left alone.

Internal name — the immutable API identifier of a HubSpot property, distinct from its human-readable label. It is set at creation and cannot be changed afterwards.

The constraint that governs everything

DisplayName must be unique across all Customer, Vendor and Employee records in a QuickBooks company file, which is a wider constraint than it first reads: the name is not checked only against other customers but against all three entity types sharing one namespace.

Submitting a name already in use returns a ValidationFault with code 6240, documented by Intuit as Duplicate Name Exists, and because the write does not partially succeed there is no record left behind to reconcile — the attempt simply produced nothing.

That behaviour inverts the failure most integrations are designed against. A HubSpot–QuickBooks sync does not quietly manufacture duplicate customers, because QuickBooks refuses them outright, and what accumulates instead is a queue of rejected writes. That queue is harder to notice than duplicates would have been: a duplicate customer appears in a list somebody looks at, whereas a rejection lives in an integration log that is only opened once the missing records are already being chased.

Two consequences follow directly.

A company that is also a supplier will collide for this reason: if you buy from Acme and sell to Acme, the Vendor record already holds the name, so the Customer write returns 6240 no matter how the customer record is constructed. Agencies, resellers and anyone with reciprocal trading arrangements hit this early, because the same organisation legitimately occupies both roles.

Ampersands do not normalise either, because QuickBooks treats & and the word and as different strings, which lets "Smith & Sons" and "Smith and Sons" coexist as distinct records with distinct transaction histories. Any deduplication routine that normalises punctuation before comparing will therefore merge two records the platform considers separate, and since the merge cannot be undone the histories are combined permanently.

What QuickBooks gives you to match on

The Customer entity exposes a small set of fields worth matching against.

Id is the immutable primary key assigned by QuickBooks, and because it is the only field that survives a rename unchanged it is what the join should ultimately be built on.

DisplayName is required in the sense that either it or one of Title, GivenName, MiddleName, FamilyName or Suffix must be present. It cannot be removed by a sparse update.

CompanyName is free text and falls outside the uniqueness constraint, which makes it a more stable human-readable match than DisplayName in files where staff have edited display names to control list sorting.

PrimaryEmailAddr must contain an @ and a ., a format QuickBooks enforces on write, and its domain tends to be the most stable non-key signal available because domains outlast the name changes and abbreviations that make DisplayName unreliable.

BillAddr is structured rather than free text, which makes it usable as a tiebreaker between two otherwise equal candidates, though not as a key in its own right since several customers may legitimately share an address.

The property to create in HubSpot

Store the QuickBooks Id on the HubSpot company and join on it from then on, and create the property before any sync runs, because backfilling it afterwards means repeating the entire reconciliation against records the sync has already touched.

text
POST /crm/v3/properties/companies
Content-Type: application/json

{
  "name": "quickbooks_customer_id",
  "label": "QuickBooks Customer ID",
  "groupName": "companyinformation",
  "type": "string",
  "fieldType": "text",
  "hasUniqueValue": true,
  "description": "QuickBooks Online Customer.Id. Written by the sync; do not edit."
}

name, label and groupName are the required body fields, and both type and fieldType must be supplied — type describes the data, fieldType describes the input control. The Properties API guide lists the permitted combinations, and the field types reference describes how each behaves in the UI.

Use string/text rather than number/number, because QuickBooks Id values are opaque identifiers rather than quantities, and typing them as numbers invites HubSpot to format, round or aggregate values whose digits carry no arithmetic meaning.

Set hasUniqueValue: true. HubSpot then rejects a second company carrying the same QuickBooks Id, converting a silent double-mapping into a visible write failure. It must be set at creation: attempting it later returns Cannot change hasUniqueValue from \false\ to \true\ on existing property, confirmed by HubSpot support. Contacts are an exception and reject it outright with subcategory Properties.CANNOT_SET_CONTACT_HAS_UNIQUE_VALUE, so this technique applies to companies and not to a contact-level equivalent.

The internal name is permanent, so quickbooks_customer_id will appear in every list, workflow, report and export built for the life of the portal — which is reason enough to settle the naming convention before creating it rather than after.

Reconciling before you enable sync

Run this against both systems while continuous sync is switched off, so that the join key is populated on every record that already exists and the first sync has no matching decisions left to make on its own.

  1. Export all QuickBooks customers with Id, DisplayName, CompanyName, PrimaryEmailAddr and BillAddr. Query the Customer entity through the API rather than exporting from the UI — in portals we have worked in the UI export does not include Id, which is the only field worth joining on. Confirm this against your own file before relying on it.
  2. Export all HubSpot companies with record ID, name, domain and quickbooks_customer_id.
  3. Match on email domain first, taking the domain from PrimaryEmailAddr and comparing it to the HubSpot domain property, since domains survive the rebrands and abbreviations that make name matching unreliable.
  4. Match the remainder on exact DisplayName against HubSpot name, then on CompanyName against name. Do not normalise punctuation at this stage — record near-matches as candidates rather than matches.
  5. Review every candidate by hand. A wrong match attaches one company's invoices to another and is normally discovered by the customer who receives them.
  6. Import the confirmed Id values into quickbooks_customer_id on the HubSpot companies.
  7. Verify by counting HubSpot companies where quickbooks_customer_id is populated and comparing that against the number of QuickBooks customers you expected to match, investigating any gap before continuing, because a gap at this point resurfaces later as a rejected write or an invoice raised against the wrong company.

Continuous sync should be enabled only once step 7 reconciles, since every unmatched record at that moment becomes a decision the integration makes without supervision.

Failure modes

Symptom: new customers stop appearing in QuickBooks, no error visible in HubSpot. Cause: ValidationFault 6240. The name already exists on a Customer, Vendor or Employee. Fix: query all three entities for the name before writing. On collision, either write to the existing record or apply a documented disambiguation suffix. Surface the rejection where a person will see it.

Symptom: an update returns a failure but the record looks fine. Cause: a stale SyncToken. Something modified the record between your read and your write, and QuickBooks rejects writes carrying an older token. Fix: re-read the entity, take the current SyncToken, re-apply your change. Do not cache tokens between runs.

Symptom: two HubSpot companies carry the same `quickbooks_customer_id`. Cause: the property was created without hasUniqueValue, and a matching rule fired twice. Fix: uniqueness cannot be added to an existing property. Deduplicate the companies, create a new property with hasUniqueValue: true set at creation, migrate the values, then deprecate the old one.

Symptom: "Smith & Sons" and "Smith and Sons" both exist and each has invoices. Cause: QuickBooks does not normalise ampersands, so both were accepted as distinct names. Fix: merge inside QuickBooks, not through the API. Decide which record survives before merging — the operation cannot be undone.

Symptom: a sparse update wipes fields you did not send. Cause: a full update was sent instead of a sparse one. A full update replaces the entity. Fix: set sparse on the request and include Id and SyncToken. Confirm on a test record that omitted fields survive.

What this does not solve

Matching contacts to QuickBooks is out of scope here — the entity model differs enough that company-level matching is the sensible first problem.

Id is stable within one QuickBooks company file and meaningless outside it. Migrating to a new file invalidates every stored value.

There is no supported way to make QuickBooks case-sensitive or ampersand-aware on names, and no API-side normalisation to configure. Names are matched as the platform stores them.

HubSpot does not publish a per-second rate limit for the Properties API on every tier. Measure against your own portal rather than relying on a figure.

Verification checklist

  • quickbooks_customer_id exists on companies with hasUniqueValue: true
  • Every HubSpot company expected to sync has a non-empty value
  • No two companies share a value
  • A deliberate 6240 collision has been triggered on a test record and appeared in whatever alerting you rely on
  • A sparse update on a test customer left omitted fields unchanged
  • Counts on both sides reconcile, and the difference is explained rather than tolerated

Integration scope and pricing for this work can be configured on our store.

Our HubSpot Services

From implementation to optimization, we handle every aspect of your HubSpot journey

WEEKLY PROGRAM

RevOps Office Hours

A recurring weekly RevOps operating program. Live support plus hands-on HubSpot implementation work.

$1,500/mo
Monthly Operating Program
  • 1 live Office Hours session per week
  • 4 hours of hands-on implementation work per month
  • We determine how hours are allocated based on priorities
  • Recurring monthly cadence