September 5, 2026 · 7 min read
The line item is a photograph of the product
A line item copies a product's price, name and SKU once, at creation, and the property that's supposed to link it back is a bare number with no relationship behind it. In one portal, 137 line items still cite a product that no longer exists.
The first piece counted fifteen line item records from one $9,995-a-month sale. Every one of them started somewhere, and the first of them, the deal's own line item, the record everything downstream eventually copies from, started the way every line item in HubSpot starts: as a snapshot of a product, taken once.
I think this is the single most useful thing to understand about the object, so I want to spend the piece on it. When you pick a product onto a quote or a deal, HubSpot doesn't connect the new line item to that product. It reads the product's values at that instant, writes them onto a brand-new record, and from then on the two go their separate ways.
What gets copied, and what doesn't
HubSpot's own property description says it better than I could. The field is hs_product_id, and its description reads: "ID of the product this was copied from." Copied from, past tense, not linked to.
At the moment of the copy a defined set of fields comes across: name, price, hs_sku, hs_url, hs_images, hs_product_type, the tax category, and the term and billing-frequency defaults. What doesn't come across is the product's folder, and, more to the point, anything that happens to the product afterward. Rename it, reprice it, retire it, and none of that touches a line item already created from it. That isn't an oversight. A sale has to record what was actually agreed, not whatever the catalog says today, and if you've ever had a customer dispute an invoice against a price list that moved, you'll want it this way. The line item is a photograph of the product, not a live window onto it.
The cost of that correctness is that the product library ends up telling you very little about what you actually sold. There's no report that reads "line items by product" the way the report builder reads a real association, because there isn't one to read.
The receipt isn't a relationship
hs_product_id is a plain number field. It's writable, so nothing stops a workflow or an API call from setting it after the fact. And it's hidden: true, so it isn't on a line item's record card and won't appear in a standard report's field picker unless you go looking.
The word "link" does a lot of unearned work in how people talk about this field, because there's no link behind it. Ask HubSpot's own association catalog for the relationship and it comes back empty in both directions:
| The call | What comes back |
|---|---|
| GET /crm/v4/associations/line_items/products/labels | {"results": []} |
| GET /crm/v4/associations/products/line_items/labels | {"results": []} |
I checked that the question was being asked correctly before believing the empty answer. The same call, asked about relationships that do exist, answers: line items to deals returns type 20, line items to quotes returns 68. It finds associations wherever HubSpot built one. It found none between line items and products, and a full pass across every line item in the portal agrees, zero product associations against 199 deal associations and 144 quote associations read off the same records. HubSpot built the connection out of a number rather than a foreign key, and nothing enforces that the number still points at anything.
When the number points at nothing
In the portal I traced, 450 of 632 line items carry a value in hs_product_id. The other 182 carry none at all, and I'll come back to those. The 450 reference 111 distinct product IDs. Only 69 of those 111 products still exist. The other 42 have been deleted or otherwise removed, and 137 line items, 30.4% of everything carrying a product reference, are still citing one of them.
Deleting a product doesn't warn you first. It doesn't cascade to the line items that point at it, and it doesn't clear their hs_product_id either. The dead reference just sits there and the line item reads back as a full, intact record, exactly as if nothing had happened. One concrete case: a line item carrying hs_product_id = 2251515249. A live read against that product returns HTTP 404, on a plain GET and again with ?archived=true. The identical call against a product that's still there, 2420931629, comes back 200 with a full property set. Same call shape, same portal, two outcomes, so the 404 isn't an artifact of how I asked. It's a dead ID on a live record.
There's a trap in how you'd go looking for these yourself, and I fell into it on the first pass. Ask HubSpot to batch-read a list of product IDs and it doesn't error when some are missing. It returns a normal, complete response, with the products it found in the results array and the misses in a separate errors field nobody thinks to check. A first pass that reads only the results array reports "resolved 69, missing 0" and calls the portal clean. The only way to catch the 42 is to diff the list of IDs you asked for against the list you got back.
The ones that still resolve aren't safe either
Subtract the 137 dead references from the 450 and 313 line items point at a product that's still there. Of those 313, 222 still match their product exactly on both price and name. The other 91 don't: 56 have a price that no longer matches the product's current price, 63 carry a name the product no longer goes by, and the two groups overlap.
One case I read live: three separate line items all priced at $200, and the product they were copied from now reads $195. Nobody re-priced the line items when the catalog changed, because nothing in HubSpot re-prices a line item when the catalog changes. That's the same snapshot behaviour from the top of this piece, just visible now instead of theoretical. A deal still open against one of those line items is forecasting $200 rather than $195, and nothing on the record flags the gap. You'd have to compare it to the product yourself, and you'd have to know to look.
Building without a product at all
None of this is mandatory, which surprised me a little the first time I noticed it. A line item doesn't need a product behind it. 182 of the 632 line items in this portal, 28.8%, have no hs_product_id at all. In a second portal I checked, one built around a third-party billing integration, that number is 100%. Roughly 95% of the line items there carry a SKU instead, and not one carries a product ID. The Product object is never touched.
HubSpot documents this as a first-class path rather than a workaround. "Create custom line item" is a named option in the line item editor, described in the knowledge base as creating "a new custom line item unique to this deal." So the product catalog is optional infrastructure, not a ledger of everything ever sold, and a portal that runs mostly on custom lines has a product library that's decorative whether anyone intended that or not. Anything reporting "top products" off the Product object is blind to every sale that never touched one.
What to check this week
hs_product_id isn't on your line item view by default, so add it before you can look for any of this. Two things worth doing once it's there:
- Pull the set of product IDs your open line items actually reference and batch-read them against the Product object. Don't stop at whether the call succeeds. Diff the list of IDs you sent against the list of records that came back. The ones missing from the second list are dangling, and the API won't tell you any other way.
- Pick a handful of line items whose product still resolves and compare price and name against the product directly. If your portal looks anything like this one, roughly three in ten won't match, and nothing will have told you before you looked.
The line item that was just born doesn't stay singular for long. The next hop it takes creates a second copy of itself, on purpose, in the same transaction, one for the deal and one for the quote, both holding the same sale. The next piece is about why HubSpot builds it that way deliberately, and why the duplication turns out to be the safety mechanism rather than the flaw.