Field notes12 min read

The seven joins between HubSpot, Stripe and QuickBooks that nobody maintains

Each of these three systems links its own objects natively. None of them links to the other two. Here are the seven joins you own, and what breaks at each one.

If you run HubSpot, Stripe Billing and QuickBooks Online, you have three systems that each keep an internally consistent picture of your business and share nothing.

HubSpot knows a Deal is associated with a Company, Contacts and Line Items. Stripe knows a Subscription belongs to a Customer and produces Invoices. QuickBooks knows an Invoice has a CustomerRef and gets closed by a Payment through LinkedTxn. All of those links are native, free and maintained by the vendor.

The links between the three are not. There are seven of them, none of them exists in any of the three systems, and every one of them is something you or a tool you installed has to create and keep correct. This post walks all seven: what the join key is, what breaks when it's missing, and what decision has to be made before it can exist at all.


J1 — HubSpot Company ↔ Stripe Customer

Key: company domain, or contact email, or a custom field you plant.

Neither system has an opinion about the other. Most integrations match on email, and that is the single most common source of duplicate records in the whole chain — because the email on a HubSpot Contact is a person and the email on a Stripe Customer is a billing inbox. raj@acme.com closed the deal; ap@acme.com receives the invoice. Match on email and you get two customers for one company.

The more reliable key is company domain, with a custom field (cf_hubspot_company_id on the Stripe customer, or the reverse) written at creation time so the second sync never has to guess.

The decision behind it: when the two disagree about the customer's name, address or currency, which one wins? That is not a technical question, and no schema tells you the answer.


J2 — HubSpot Deal ↔ Stripe Subscription

Key: metadata.hubspot_deal_id on the subscription.

This is the join that makes or breaks the whole chain. If the deal id is on the subscription, every downstream question is answerable: which deal produced this revenue, did the rep's number match the billed number, was this upsell ever invoiced. If it isn't, you are inferring the link from customer plus dates, and that falls apart the moment a customer has two deals in the same month — which is exactly what happens with upsells.

What breaks without it: nothing, visibly. That is the problem. The sync runs, the subscription exists, and the connection between what sales sold and what billing charges simply does not exist anywhere. You discover this the first time someone asks why ARR in the CRM and MRR in Stripe differ by 4%.

The decision behind it: when is a deal actually "won"? Stage change to Closed Won, or an accepted Quote? Those are different moments, sometimes days apart, and reps move stages for pipeline hygiene reasons that have nothing to do with a signature.


J3 — HubSpot Line Item ↔ Stripe subscription_item

Key: product mapping plus price comparison.

A HubSpot Line Item has quantity and price — the price the rep actually quoted, which may differ from the catalog. A Stripe subscription item has an item price and, when overridden for this customer, its own unit amount. The join compares price × quantity on one side to unit_amount × quantity on the other.

This is where custom pricing lives, and it shows up three ways. A price override on the subscription item (standard plan, non-standard amount). A customer-specific price object created for one account. And one-off charges or coupons that never appear on the subscription at all, only on the invoice — which means the honest comparison is deal total against first invoice total, not plan against plan.

The decision behind it: do reps override prices? If yes, an override is expected and only a mismatch matters. If no, any override at all means someone in billing changed a number sales didn't sell.


J4 — HubSpot Product ↔ Stripe Price ↔ QuickBooks Item

Key: a three-way mapping table you build once.

This one is static, boring, and the reason half of all ledger postings are wrong. The HubSpot product "Growth annual" has to map to a Stripe price id and to a QuickBooks Item — and the QuickBooks Item is what carries IncomeAccountRef, the GL account the revenue lands in. Get this mapping wrong and the money posts, it just posts to the wrong account, which nobody notices until someone runs a P&L by product line.

What breaks: a new plan is added in Stripe, nobody adds it to the map, and invoices for it either fail to post or land in a default account.


J5 — Stripe Customer ↔ QuickBooks Customer

Key: same as J1, or QuickBooks DisplayName, or a stored id.

QuickBooks enforces uniqueness on DisplayName and nothing else. So an integration that creates a customer before searching for one will, on its second run, either fail with a duplicate-name error or succeed by appending something — and now you have "Acme Inc." and "Acme Inc. 2", each holding half the invoices.


J6 — Stripe Invoice ↔ QuickBooks Invoice

Key: DocNumber on the QuickBooks invoice, set to the Stripe invoice id.

QuickBooks has no field that means "the external system's id", so DocNumber is the conventional place to put it. It's also how you make posting idempotent: before creating, query for an invoice with that DocNumber; if it exists, this invoice is already posted.

The comparison is total against total after discounts and tax, then line by line. Where it goes wrong is tax: if Stripe calculated tax and QuickBooks also applies a default tax code, you post the amount twice-taxed or not taxed at all.

The decision behind it: post nightly, or at month-end? Accountants usually want a predictable rhythm and a single place to review, not a trickle of invoices arriving all day.


J7 — Stripe payment ↔ QuickBooks Payment

Key: QuickBooks Payment.Line[].LinkedTxn pointing at the invoice id.

An invoice marked paid in Stripe with no linked payment in QuickBooks means the revenue is recorded and the cash isn't. Your AR ages on something that was collected weeks ago.

The complication is Stripe fees and payouts. Stripe collects $1,000, takes $29, and deposits $971 in a batch with eleven other invoices. If the integration records a $1,000 payment against the invoice and the bank feed shows $971 in a lump, the bank reconciliation never clears. The correct shape is: payment of $1,000 against the invoice, an expense line for the fee, and a deposit that groups the invoices in that payout. Tools that skip this are the reason bookkeepers end up doing journal entries by hand every month.


What this list is actually telling you

Look at the pattern. Four of the seven joins can't exist until a business question is answered: what "won" means, who wins a conflict, whether reps can override price, when to post. None of those are field mappings. None of them can be inferred from a schema. And none of them are asked by the tools people usually install, which is why those tools work for three months and then quietly stop being right.

The other three are just careful engineering: plant the id at creation time, make writes idempotent on a key the destination system can enforce, handle fees and payouts as separate objects rather than folding them into the invoice.

That's the whole job. It isn't glamorous and it isn't hard in any individual place. It's hard because there are seven of them, they live in three systems none of which will tell you when they drift, and nobody owns the chain end to end.


TruelineHQ runs the HubSpot → Stripe Billing → QuickBooks Online connection — the sync, the business rules, and the fixes when an API changes. See how it works.

We run this chain so you don’t

TruelineHQ keeps HubSpot, Stripe Billing and QuickBooks Online in step — the joins, the rules, and the vendor changes. Start in shadow mode and see every write before it happens.

Connect your stackSee integrations