---
name: revenue-hub-contract-migration
description: >-
  Move real agreements into HubSpot's native Contract object without breaking
  how customers get billed. Covers the decisions that cannot be taken back, the
  model that survives multi-year and ramped agreements, the stored-payment-method
  seam that most plans miss, and the failures that report success. Use when a
  Revenue Hub contract migration is being planned, built, or has already gone
  quiet in a way nobody can explain.
---

# Revenue Hub contract migration

Written by Ryan Ginsberg (Unified Support Solutions) from production contract
migrations — built first against the beta Contracts API, then through the
import, because only one of those can carry a stored payment method. Every
behaviour below was observed on a live portal rather than read in a doc. Free to
use, copy and change.

**What this is for.** Getting agreements into the Contract object with the
billing relationship intact. It assumes somebody has already decided Revenue Hub
is the right move — if that is still open, run the readiness skill first, or the
same nine questions at <https://ryanginsberg.com/revenue-hub-audit>.

**What this is not.** It is not a configuration guide, and it will not tell you
this is easy. Two of the sections below exist because a migration reports success
while doing something wrong.

**How to use it.** Work through it in order with the portal open. Where a check
can be answered by reading the portal rather than asking someone, read the
portal — a recollection of how billing works is not evidence of how it works.

## Start with what cannot be undone

Establish these before anyone opens a spreadsheet. Each is fixed at the moment a
contract is created:

- **Billing enabled.** True makes HubSpot the billing system of record.
  HubSpot's own documentation states the migration cannot be undone.
- **Collection process.** Automatic or manual, per contract, permanently. Most
  books contain both, so this is not one decision — it is one per agreement,
  read from how that customer actually pays today rather than from a default.
- **Payment enabled.** Whether invoices carry a pay-now link.
- **Currency and effective date.**

And the one teams meet late: **a billing-enabled contract cannot be backdated.**
The effective date clamps to today, so an agreement that began years ago will
say it began at cutover. Whether that matters depends on who reads the record
afterwards — but it is a real loss of history, it costs nothing to decide now,
and nobody will raise it unless you do.

## The model that survives real data

**One contract per agreement, not per record.** Multi-year agreements are often
stored as a chain of annual subscriptions. Map one contract per subscription and
a three-year agreement becomes three near-identical contracts, one of which ends
before it starts. Collapse the installments inside the agreement's term window
into a single contract; a subscription that post-dates the term is the next
renewal and earns its own.

**Term comes from the recurring line item, not a field on the deal.** However
people say it out loud — "it's a three-year deal" — the system derives term from
the number of payments on the recurring line. A text field saying three years
drives nothing, enforces nothing, and will disagree with the line within a
quarter.

**A ramped line is one line over time, not two lines.** A price that steps in a
later year is the same line at two prices, expressed as phases that share a ramp
key, with contiguous non-overlapping dates and a term covering only that phase.
Get this wrong and every downstream total double-counts, because the phases are
read as though they bill concurrently. Any customer-facing surface built on that
data inherits the error.

**A concession is a discount, not a lower price.** Carry it as a percentage
against list, so the record shows list and reduction as separate numbers. A price
somebody quietly lowered cannot be explained a year later, and there is nothing
to roll off at renewal because nothing recorded what the price would otherwise
have been.

**Not every line should ramp.** A flat line sitting beside a ramped one is easy
to give a term it should not have — see the term trap below, which is the most
expensive mistake in this document.

## The seam most plans miss

Ask this before agreeing a cutover date: **how many customers have a stored
payment method, and what happens to it?**

Where a business auto-charges, that arrangement lives with the subscription, not
the contract. If migrating means asking those customers to re-enter payment
details, that is not a migration anyone should sign off — it puts a live revenue
book at risk in order to change an internal data model. Establish the number
early, because it decides whether the project is safe at all.

Two things follow. The external payment method reference can only be set **as the
contract is created**, so it is not a follow-up task: a contract created without
it cannot be repaired, only rebuilt. And it is required for automatic collection
and must be blank for manual — which is why collection process has to be read per
customer rather than assumed for the book.

Expect the source data to be untidy, and expect the untidiness to be structural
rather than random. Common shapes worth testing for:

- one person holding many stored methods across several customer records,
  usually from re-entering a card each renewal rather than reusing one;
- subscriptions flagged to collect automatically that have no stored method at
  all — often a cohort created through an invoice-first flow, where the flag
  describes an intention rather than a reality;
- stored methods whose last four digits disagree with what the subscription
  bills, which means somebody has to say which one is current.

None of these are modelling problems. They are questions for the business, and
they take days to answer, so surface them in week one.

## The failures that report success

This is the section worth reading twice. In a migration, the dangerous failures
are not the ones that stop — they are the ones that finish and say so.

- **A row can import its properties while silently dropping its associations.**
  You get a contract with no line items and a success message. One cause: a line
  item may have at most one parent object type per row, so a row offering it two
  drops the associations rather than failing. Make the second association a
  separate step.
- **A column naming a property that does not exist gets mapped to one that
  does.** If the substitute is read-only you find out immediately. If it is
  writable, your data lands somewhere plausible and wrong, and nothing says so.
- **Association labels import unlabelled when the label name does not match.**
  No error. The association exists, so it looks complete — it simply is not the
  billing contact any more.
- **A term on a line that should not have one ends it early.** Give every line a
  fixed twelve-month term and any line meant to renew indefinitely stops at the
  anniversary: billing quietly ceases while related lines carry on, and nothing
  on screen announces it. Leave the period blank on lines that should renew
  indefinitely, and on the final phase of a ramp; use a term only on phases that
  hand off to the next.
- **Writing to a property that does not exist on that object does nothing, and
  keeps doing nothing.** This can run for weeks across a whole book before
  anyone reads a record back and finds the field empty.
- **A staging screen is not a result.** Rows listed before migration are rows,
  not records. Reading them as records is an easy and expensive mistake.

## Two setup details that cost a day each

**The grouping key must require unique values.** The property that ties a
contract's rows together only groups them if it carries that rule. Without it
every row becomes its own contract — with no error. And a deleted property name
is reserved permanently along with its original settings, so a name that once
existed without the rule can never be recreated with it. Choose a name the
business can live with indefinitely.

**Keys belong on the object whose rows they vary across.** A key that groups the
phases of a line must live on the line item. On the contract it can hold only one
value for the whole contract, so it cannot express "these two rows ramp together
and that one does not."

## Verify by reading back, not by reading the result screen

`DONE` means the file was processed. It does not mean the records are right.
Pull each contract back through the API and check:

- the payment method is attached, and to the contract you intended;
- ramp phases share one key, and the earlier phase ends exactly when the next
  begins;
- lines that should be evergreen have **no end date** — this is the check that
  silently protects revenue;
- the annual total reflects the current period only. If it looks like the sum of
  every phase, the phases were read as concurrent lines;
- associations exist **and carry their labels**;
- the fields the finance system depends on are populated, not merely mapped.

Do this on a single record before any batch, and again on a sample afterwards.

## How to close

Report what you verified live and what you took on trust, and keep those two
things separate. Then name the decisions that are now fixed — billing enabled,
collection process, effective dates — because the business has just bought them
permanently and may not know it.

If you migrated a subset, say plainly which agreements did not go and why. An
exception list with a reason per row is worth more than a completion percentage,
and it is the only honest way to hand the remainder back.
