Using HubSpot property history to fix historical deal attribution

26 March 2026

Tom Mitchell
Tom Mitchell Owner, TJM Digital Ltd

Most HubSpot portals have the same reporting gap hiding in plain sight.

Deals exist. Revenue is tracked. Pipeline reports look fine. But when someone asks “what channel drove this deal?” the answer is either missing, wrong, or whatever the contact’s source happens to be today – not what it was when the deal was actually created.

That matters. Attribution that reflects the present rather than the moment of creation is not attribution. It is a guess dressed up as a data point.


The problem with HubSpot deal attribution today

A common approach to deal-level attribution is to pull hs_analytics_source from the associated contact and stamp it onto the deal.

For new deals going forward, this works well. You trigger a workflow on deal creation, look up the contact, read the source, write it to a custom deal property. Snapshot taken.

The problem is historical deals.

If you are backfilling attribution onto deals that were created months or years ago, the contact’s current hs_analytics_source may no longer reflect what it was at the point the deal was created. Contacts revisit your site. They come back through different channels. HubSpot updates the source value. By the time you run the backfill, you are capturing today’s value and stamping it onto yesterday’s deal.

For contacts whose source has never changed, this is fine. For the rest, your historical attribution is silently wrong.


HubSpot property history changes the game

HubSpot records a timestamped history of property changes.

Most people know this exists in the UI – you can click “View property history” on any contact record and see the full changelog. Fewer people realise this data is available via the API and that it is genuinely useful for solving real operational problems.

The v3 CRM API supports a propertiesWithHistory parameter on its read endpoints. When you include it, HubSpot returns not just the current value of a property, but every previous value alongside the timestamp of when each change occurred.

The API call itself is simple:

GET /crm/v3/objects/contacts/{contactId}?propertiesWithHistory=hs_analytics_source

A typical response looks something like this:

Contact 12345 - hs_analytics_source history:

2024-01-15  ORGANIC_SEARCH
2024-06-03  PAID_SOCIAL
2024-09-22  DIRECT_TRAFFIC

Each entry tells you what the value was and when it changed to that value.


Reconstructing point-in-time HubSpot attribution

Once you have the property history, the logic for a proper historical backfill becomes straightforward.

For each deal:

  1. Get the deal’s createdate
  2. Find the associated contact
  3. Request the contact’s hs_analytics_source with propertiesWithHistory
  4. Walk through the history entries and find the value that was active at or just before the deal creation date
  5. Write that value to your custom deal property

If a deal was created on 2024-07-10 and the contact’s source history shows ORGANIC_SEARCH from January and PAID_SOCIAL from June, the correct attribution for that deal is PAID_SOCIAL – because that was the value at the time the deal was created.

If you had simply pulled the current value, you would get DIRECT_TRAFFIC from September – which had nothing to do with the deal at all.


Beyond attribution – other uses for HubSpot property history

Once you understand the principle – reconstructing what a HubSpot property value was at a specific point in time – the applications extend well beyond traffic source.

Lifecycle stage at the point of deal creation. Lead status at the time of a meeting being booked. Owner at the moment a task was completed. Any property where the current value may have drifted from the value that was relevant at the time of a specific event.

Anywhere you need a historical snapshot rather than a live reading, property history gives you the raw material to build one.


Practical considerations for using propertiesWithHistory

There are a few things worth knowing before you build this.

History is returned in reverse chronological order. The most recent change comes first. When walking through entries to find the value at a specific date, you are looking for the first entry whose timestamp is equal to or earlier than your target date.

The drill-down fields matter too. hs_analytics_source on its own gives you the channel category – ORGANIC_SEARCH, PAID_SOCIAL, DIRECT_TRAFFIC, and so on. For more useful attribution, you probably also want hs_analytics_source_data_1 and hs_analytics_source_data_2, which carry the detail. A source of PAID_SOCIAL with a drill-down of “LinkedIn” is far more actionable than PAID_SOCIAL alone. Request history for all three properties in the same call.

Rate limits apply. If you are backfilling thousands of deals, you will be making a lot of API calls – one per contact at minimum. Batch your requests, paginate sensibly, and respect the rate limits. A well-written Python script can process a few thousand deals in under an hour without hitting any walls.

Edge cases exist. Some contacts will have no history for the source property – either it was never set or the history has been truncated. Some deals may not have an associated contact at all. Your script should handle both gracefully, flagging records it could not enrich rather than silently skipping them.

The v1 API uses a different parameter. If you are working with the legacy contacts API, the equivalent parameter is propertyMode=value_and_history. The v3 approach using propertiesWithHistory is cleaner and should be preferred for new work.


Why this matters for HubSpot reporting trust

Attribution reporting is one of the areas where HubSpot portals quietly lose credibility.

When a CMO asks which channels are driving revenue and the answer is based on whatever the contact’s source happens to be today, the numbers feel plausible but are not reliable. Nobody notices the drift until someone digs in and finds that a deal from eighteen months ago is attributed to a campaign that launched six months ago.

A proper historical backfill using property history does not make attribution perfect. But it makes it defensible. It captures what was true at the time the deal was created rather than what happens to be true now. That distinction is the difference between a report people reference and a report people politely ignore.


Where to start

If you have a custom deal property for traffic source and you are populating it going forward via a workflow, the backfill is the missing piece.

Write a script that loops through your historical deals, looks up the associated contact, retrieves the source property history, finds the value at deal creation, and writes it back.

It is not a complex build. The HubSpot API does the heavy lifting. The logic is a timestamp comparison. The output is a deal property that now means something for every deal in your portal, not just the ones created after you set up the workflow.

The property history API sits in the background of every HubSpot portal, quietly recording everything, waiting for someone to ask the right question.

Most people never ask.

← Previous Why derivative fields are the most reliable source of truth in HubSpot – when used selectively

Need help with your HubSpot?

If anything in this post resonated — or if you're dealing with something similar — I'm happy to talk it through.

Let's talk →