Revenue Recognition on Milestone Contracts: From a HubSpot Deal to Sage Intacct
Automating revenue recognition for milestone contracts from HubSpot into Sage Intacct: what crosses the boundary, what stays behind, and where the schedule has to live.
Paul Maxwell
AUTHOR
GET WEEKLY REVOPS INSIGHTS
No spam. Unsubscribe anytime.
A milestone contract is signed in January for $480,000 across four delivery stages, and the first question anyone asks is what it does to the quarter. Sales answers $480,000, because that is the deal they closed. Finance answers something between nothing and $120,000, depending on whether stage one has been accepted. Both are working from the same signature and neither can see the other's arithmetic, so the number reported to the board is whichever system the person building the deck happened to open.
This article describes how to automate revenue recognition for milestone contracts that originate as HubSpot deals and are recognised in Sage Intacct. It starts with what a milestone contract is in each system, then sets the boundary between them, then describes the deal structure that survives the handoff, the schedule that has to live in the finance system, and the identity key that makes reconciliation possible. It closes with what breaks under contract amendment, the business case for building this, and the checks to run before the first month-end close depends on it.
Three terms are used precisely throughout. A milestone is a contractual delivery stage with an acceptance condition attached to it. A recognition schedule is the set of dated amounts that move from deferred revenue to recognised revenue as those conditions are met. Billing is a separate matter entirely: an invoice can be raised before, after, or nowhere near the point at which the associated revenue is recognised, and conflating the two is the error this whole arrangement exists to prevent.
The Milestone Contract in Each System
In HubSpot a contract is a deal with an amount, a close date, a pipeline stage, and line items describing what was sold. Line items carry price and quantity and associate to the deal (HubSpot line items API). The deal object has no native concept of a delivery stage that has been accepted, and no field whose meaning is "revenue that may now be recognised" (HubSpot deals API).
In Sage Intacct the same contract is a set of accounting entries with dates and accounts attached, posted against a customer and a project. The general ledger is the system of record for what has been recognised, and the journal entry is the artefact that records it (Sage Intacct journal entries).
The asymmetry matters more than the terminology. HubSpot holds the commercial fact — this customer agreed to this scope at this price — and Intacct holds the accounting consequence. Neither is a subset of the other, and an integration that treats one as a mirror of the other produces a finance system carrying sales assumptions or a CRM carrying accounting entries.
The Boundary and the Three Fields That Cross It
Only three things need to cross from HubSpot into Intacct at the point a deal closes: the customer identity, the contract value broken down by milestone, and the expected date of each milestone. Everything else the finance system needs it already has or will generate itself.
Nothing needs to cross in the other direction except one value, and that value is not money. It is the reconciliation key described below, written back onto the deal so that a person looking at the CRM can find the contract in the finance system. Recognised revenue does not belong on the deal record. A salesperson who can see recognised revenue on a deal will report it, and the number they report will be stale between the moment it is read and the moment the period closes.
This is a decision about permissions as much as about architecture. A CRM field containing an accounting figure will be used as an accounting figure by someone with no obligation to reconcile it.
The Deal Structure That Survives the Handoff
A milestone contract cannot be expressed as a single deal amount, because a single amount has no internal structure for a schedule to be derived from. Two structures carry that internal detail successfully, and a third, which is the one most often reached for, does not.
The structure that works at low volume is one line item per milestone, each carrying the milestone value and a date property. Line items already associate to the deal and already carry the amounts, so no new object is required (HubSpot associations).
The structure that works at higher volume is a custom object, one record per milestone, associated to the deal. This costs more to build and buys two things the line item cannot: a milestone can carry its own status, and it can be edited after the deal closes without altering the commercial record of what was sold.
The structure that does not work is a set of deal properties named milestone_1_amount through milestone_4_amount. It fails the first time a contract has five milestones, and every subsequent report has to union four columns that should have been four rows (HubSpot properties).
| One line item per milestone | Custom object per milestone | Numbered deal properties | |
|---|---|---|---|
| Milestone count | One line item per milestoneAny | Custom object per milestoneAny | Numbered deal propertiesFixed at build time |
| Status per milestone | One line item per milestoneNo | Custom object per milestoneYes | Numbered deal propertiesOnly as more properties |
| Editable post-close | One line item per milestoneAlters the sold record | Custom object per milestoneYes, independently | Numbered deal propertiesYes, silently |
| Build cost | One line item per milestoneLowest | Custom object per milestoneModerate | Numbered deal propertiesLow, then rising |
| Reporting shape | One line item per milestoneRows | Custom object per milestoneRows | Numbered deal propertiesColumns needing a union |
Ownership of the Recognition Schedule
The recognition schedule belongs in Sage Intacct, and the argument for that is not a preference about tooling.
A schedule is an accounting artefact governed by a revenue standard, audited, and subject to restatement. It has to survive the departure of the person who built it and produce the same answer when an auditor recalculates it in two years. Intacct's platform holds the schedule against the contract and posts against it (Sage Intacct platform services).
HubSpot's role stops at supplying the inputs. What crosses the boundary at close is a proposed schedule — amounts and dates derived from the milestones — and what Intacct does with it is subject to review by someone who is accountable for the general ledger. The proposal is data. The schedule is a record.
Teams that invert this, computing recognition in HubSpot workflows and posting the results, end up with revenue policy encoded in a marketing automation tool that nobody in finance can read, version, or defend.
Identity and the Reconciliation Key
No two commercial systems share an identifier space, and this pairing is no exception. The HubSpot deal has a numeric record ID and the Intacct contract has its own, so neither system can look the other up without a key stored deliberately on both sides.
The arrangement that holds is a single field on the deal carrying the Intacct contract identifier, written back by the integration once the contract exists, and a customer-defined field on the Intacct contract carrying the HubSpot deal ID. Both are set once and never edited by a person. The customer record needs the same treatment, because a company in HubSpot and a customer in Intacct are matched on name by default, and names diverge the first time a legal entity changes.
Matching on company name is the failure that produces revenue posted against the wrong customer, and it is invisible until someone reconciles a statement.
Triggering and What Fires the Write
The trigger is the acceptance of a milestone rather than the closing of the deal. A deal closing creates the contract and the proposed schedule. A milestone being accepted is what permits recognition against it, and acceptance is a delivery event rather than a sales event.
Two mechanisms carry it. A webhook subscription fires on the property change that records acceptance and calls the integration (HubSpot webhooks). A custom workflow action does the same inside a workflow, which is easier for an administrator to see and to change (custom workflow actions).
Either way the write into Intacct goes through its web services layer, which is the documented interface for posting transactions (Sage Intacct web services).
The write must be idempotent, keyed on the milestone identifier, because a webhook can be delivered more than once. An integration that posts on every delivery will double-recognise revenue, and the error surfaces at close rather than at the point it is made.
Failure Under Amendment
Contracts change, and amendment is where this architecture is tested against something other than the happy path.
A scope increase adds a milestone to an agreement that has already closed. If milestones are line items, adding one after close alters the record of what was sold, and that pressure is what moves higher-volume implementations to a custom object. If milestones are a custom object, the new record is created, a new schedule line is proposed, and the sold record stays intact.
A date slip moves a milestone without changing its value, and this is the common case. It changes which period the revenue lands in, which is exactly the thing finance needs to know early, so the date has to flow through rather than be corrected by hand in the finance system. A date corrected only in Intacct leaves the CRM asserting a delivery date the business no longer expects.
A cancellation after partial delivery is the case that cannot be automated, and it should not be. Reversing recognised revenue is a judgement with audit consequences, so the integration's correct behaviour is to stop and raise the case for a person to settle rather than to post a reversal.
The Business Case
What this buys is a single close date that does not require reconciliation by hand, and a forecast that finance can use without rebuilding it. The recurring saving is the days each month spent matching a spreadsheet of signed contracts against the ledger, and the one-off saving is the class of argument in which two departments present different revenue figures for the same quarter.
What it costs is a build against two APIs, a reconciliation key on four object types, and a standing obligation: every change to the milestone model in HubSpot is now a change to something finance depends on. That is a governance cost rather than a licence cost, and it is the one teams underestimate.
The case is strongest where contracts are large, milestone-based, and few enough that each error is material — professional services, construction, capital equipment, implementation-heavy software. It is weakest where revenue is subscription-based and ratable, because a subscription schedule is generated from a start date and a term, and none of the structure described here is warranted to produce it.
Verification Before the First Close Depends On It
Six checks, each derived from a property of the arrangement above rather than from a build this practice has run.
Post a contract with four milestones and confirm the proposed schedule sums to the deal amount, to the cent. Rounding distributed across milestones is where a few pence goes missing, and a close will not balance because of it.
Deliver the same webhook twice and confirm one journal entry, not two.
Amend a milestone date and confirm the schedule moves period without changing total value.
Change a customer's legal name in HubSpot and confirm the integration still finds the right Intacct customer, which it will only do if matching is on the key rather than the name.
Close a deal with no milestones and confirm the integration declines it rather than posting the full amount to the close date.
Reconcile one full month end to end, comparing the ledger against a hand-built schedule, before anyone is permitted to rely on the automated figure.
Boundaries of This Approach
This describes the mechanics of moving a milestone structure across a system boundary. It does not state which revenue standard applies to a given contract, nor when a performance obligation is satisfied, and those are determinations for the firm's accountants rather than for an integration.
It assumes throughout that milestones are discrete and separately acceptable. Contracts recognised on percentage of completion need a different input — measured progress rather than a binary acceptance — and the schedule is then derived from a cost or effort measure that HubSpot does not hold at all.
It also assumes that one contract maps to exactly one deal. Master agreements with statements of work beneath them are a different object model, and flattening them to a single deal loses the level at which the commercial relationship is actually governed.
In Summary
A milestone contract has structure, and a single deal amount does not carry it. Express the milestones as rows — line items at low volume, a custom object once they need their own status — and let the deal hold the commercial fact rather than the accounting one.
Move three things across the boundary at close: customer identity, milestone values, milestone dates. Keep the recognition schedule in Sage Intacct, where it can be audited and restated. Write one reconciliation key in each direction and never match on a name.
Trigger recognition on milestone acceptance rather than on deal close, make the write idempotent, and refuse to automate a cancellation that reverses revenue already recognised.
Recognised revenue on the deal record is the cheapest thing to remove and the one worth removing first. While it sits there, any salesperson can report a finance figure without reconciling it, and at some point one of them will.