---
name: revenue-hub-object-model
description: >-
  The HubSpot Revenue Hub object model, written for an agent that has to work in
  a portal rather than describe one. Covers what each container is authoritative
  for, how line items copy rather than link, where a dollar figure is derived and
  where somebody typed it, which properties decide behaviour, and which look
  writable but are not. Load this before quoting, contracts, billing, revenue
  reporting or any migration work.
---

# Revenue Hub object model

Written by Ryan Ginsberg (Unified Support Solutions) from production work in
these objects. Everything below was read off live portals rather than from
documentation, and where the two disagree the portal wins. Free to use, copy and
change.

**What this is for.** Getting an agent — or a person — up to speed fast enough to
be useful and slow enough to be safe. It is the terrain map. The migration skill
is the route; this is what the ground is made of.

**How to use it.** Read the model, then verify the parts you are about to act on
against the actual portal. Every claim here is checkable in a few API calls, and
portals differ: properties get added, features get enabled, one company's
"contract" is another's "subscription".

## The objects and their ids

| Object | Type id | What it is |
|---|---|---|
| Contact | `0-1` | a person |
| Company | `0-2` | an organisation |
| Deal | `0-3` | the sales process, not the revenue |
| Product | `0-7` | the catalog entry |
| Line item | `0-8` | an instance of a product on one container |
| Quote | `0-14` | a proposal with terms |
| Invoice | `0-53` | a request for payment |
| Subscription | `0-69` | a recurring billing relationship |
| Payment | `0-101` | money that arrived |
| Order | `0-123` | the commercial commitment |
| Contract | `0-721` | the agreement, as a record that behaves like one |

Deals track **process**. Orders and contracts track **commitment**. Invoices and
payments track **money**. Teams routinely report revenue off deals and then
cannot reconcile it, because a deal is a forecast that closed, not a thing anyone
owes.

## The single most important mechanic: a line item is a photograph

When a product is put on a quote or a deal, HubSpot does not connect the line
item to that product. It reads the product's values at that instant and writes
them onto a brand-new record. The field that looks like a foreign key,
`hs_product_id`, is described by HubSpot itself as *"ID of the product this was
copied from"* — copied from, past tense, not linked to.

Consequences an agent must hold:

- **Editing a product does not change anything already sold.** Yesterday's quote
  keeps yesterday's price, name and SKU.
- **The catalog is therefore the least reversible thing in the system**, because
  every downstream document is a copy taken at a moment.
- **A line item's `name` is its own**, not the product's. Get it wrong at
  creation and it is wrong on the customer's invoice.

## Every hop copies — except one

Follow a single sale through the chain and each container gets its **own** line
items, freshly created:

- deal → quote: a separate, complete set
- deal/quote → subscription: another set
- each billing cycle → an invoice line item **and** a payment line item

Those last two are disjoint populations: whatever an invoice's line item is, the
payment for that same invoice does not reuse it. So one modest recurring sale can
easily produce a dozen-plus line item records, and counting line items tells you
nothing about how much was sold.

**Deal → order is the exception.** HubSpot does not copy: it takes the deal's
existing line item and gives it a second association pointing at the order. One
record, two containers. Any logic that assumes "one line item, one parent" breaks
here — and any cleanup that deletes "the order's line items" is deleting the
deal's.

## Where a total is real and where somebody typed it

This is the reporting rule, and it is the difference between a number that
reconciles and one that argues.

Walk each container and ask: **does its headline figure derive from the line items
underneath it, or is it a value in a field that merely looks like a total?**

- A **deal's** headline amount is typed by a human. Several other deal revenue
  properties *are* exact sums of the line items — those reconcile; the headline
  need not.
- **Subscriptions** carry the richest recurring vocabulary, and can hold two
  states at once — a current price and an upcoming one — so reading "the price"
  without asking which one is a common error.
- **Orders, invoices, products and carts carry no ACV/ARR/MRR/TCV at all.** That
  vocabulary lives on line items, deals, quotes and subscriptions. Asking an
  invoice for MRR returns nothing, not zero-because-zero.
- **Invoice line items are worse than absent**: `amount` is fully populated while
  `hs_mrr` and `hs_arr` read $0.00. Sum recurring revenue across all line items in
  a portal and the invoice copies quietly drag the answer down.

The safe instruction: **report from the container, not the line item**, and know
which of the container's numbers are derived.

## The contract behaves unlike everything else

Contracts are modelled on how a commitment behaves rather than how a record does:

- **Line items lock at creation.** There is no edit. A re-price is
  terminate-and-recreate.
- **Contracts cannot be deleted**, only terminated. The record stays.
- **Amendments are their own objects**, dated and attached, rather than edits.
- **Several fields are fixed at creation** — whether HubSpot does the billing,
  automatic versus manual collection, whether invoices carry a pay-now link, the
  currency and the effective date.
- **A billing-enabled contract cannot be backdated.** The effective date clamps
  to today, so a migrated agreement will say it began at cutover.
- **Term is derived from the recurring line item** — the number of payments — not
  from any field on the deal that says "3 years".
- **A ramped line is one line across several phases**, phases sharing a grouping
  key, each with its own start date, term and price. Read them as separate
  concurrent lines and every total double-counts.

## Properties that look writable and are not

Check before you write, because the failure is usually silent:

- **Calculated properties** report as writable in the schema and reject values at
  write time. Renewal dates and rollup totals are commonly calculated.
- **Some line item recurring-term properties are read-only in an import** even
  though the API accepts them, so behaviour differs by the path you take.
- **Writing to a property that does not exist on that object does nothing at
  all** — no error, indefinitely. Read one record back after any new write.
- **A deleted property's name is reserved permanently**, along with its original
  settings. A name that once existed without a rule cannot be recreated with one.

## The API is not one substrate

Different read paths have different guarantees, and generalising from one to
another causes data bugs rather than slow queries:

- **Search is an index and lags writes.** Never use it as a uniqueness check, an
  existence check, or any read that gates a write.
- **Direct GET by id, association reads and GraphQL are strongly consistent** and
  are the safe choices for a guard.
- **Batch reads with property history cap lower than plain batch reads.**
- **Product ids, pipeline ids and association type ids are portal-specific.** An
  id carried from another portal fails in ways that read like a permissions
  problem.

Measure the paths you intend to depend on, in the portal you intend to use. Do
not inherit these numbers as facts.

## Associations carry meaning, and unlabelled is a different thing

Most pairs have both an unlabelled association and one or more labelled ones —
billing contact, primary company. An unlabelled association still exists and
still shows in the UI, so "it's associated" is not the same as "it's the billing
contact". When creating associations, name the label and then read it back.

## How to check any of this yourself

Four calls settle almost any question:

1. `GET /crm/v3/properties/{objectType}` — what exists, what type, what is
   calculated or read-only, and the option values for enumerations.
2. `GET /crm/v3/objects/{objectType}/{id}?properties=...` — what a real record
   actually holds, which beats any description of what it should hold.
3. `GET /crm/v4/objects/{type}/{id}/associations/{toType}` — what is attached and
   with which labels.
4. `GET /crm/v4/associations/{a}/{b}/labels` — the labels available for a pair,
   with their type ids.

State plainly which of your claims came from those and which came from memory.
In this object model, the difference is usually the whole answer.
