Home Blog Contact
Home/Blog/Tracing Every Deal Back to the Call That Star…
ArticleRevOpsHubSpotRingCentralData Engineering

Tracing Every Deal Back to the Call That Started It

6 min readBy Miloš Mitrović

A call-driven sales team could not reliably connect an inbound call to the deal it became, and no piece of contact data carried a record of where it came from. I built a pipeline that made every deal traceable back to the call and the source that started it. Here is how the engagement went.

At a glance

  • Client: A US-based, call-driven sales operation running HubSpot as its CRM and RingCentral for inbound calls, with demand arriving from ads and the website.
  • Challenge: Calls, callers, and HubSpot deals sat in separate silos, so no one could say which call or source produced a given deal, and contact data had no record of its origin.
  • Approach: A Python and Postgres pipeline that resolves every caller to one canonical person, records the origin of every field, matches callers to HubSpot contacts and deals, attaches recordings, and writes an auditable trail back to HubSpot.
  • Result: Any deal can now be traced to the call and source that started it, on a canonical dataset of about 28,000 people built from roughly 88,000 identifiers, with the origin of every field recorded.

The context

The client runs a call-driven sales operation. Demand arrives from ads and the website, prospects phone in through RingCentral, and the team works opportunities as deals in HubSpot. I was brought in to build the connective tissue between a phone call and the deal it eventually became. My role was to design and build the enrichment and attribution pipeline end to end.

The problem and its constraints

The three systems did not talk to each other in any trustworthy way. A person might call from two numbers on two days and land as two separate contacts. A recorded call sat in RingCentral with no link to the HubSpot deal it belonged to. And when a number finally matched a deal, nobody could say where the surrounding contact data had come from, so attribution was guesswork.

Several hard constraints shaped the build. HubSpot was the source of truth for deals and could not be overwritten, so the pipeline had to reconcile against it. Every step had to be idempotent, safe to re-run without creating duplicates, because data arrived continuously and runs overlapped. And it had to hold up at real volume, tens of thousands of people and calls, not a demo dataset.

What was in place before, and why it fell short

Matching calls to deals had been a manual spreadsheet exercise, the kind that produces a one-off report and is stale the moment it is saved. Contacts were duplicated across numbers, calls were orphaned from their deals, and with no audit trail a figure in a report could not be defended if someone asked how it was derived. It did not scale, and more to the point it could not be trusted.

What I built

I built a canonical data store in PostgreSQL 16, running in Docker, with a Python pipeline on top of it. A scheduled run sweeps RingCentral for new calls and recordings, sweeps the sales inbox, and mirrors HubSpot deals and contacts so the local picture stays current.

The core is identity resolution. Rather than trust any single phone number, the pipeline resolves each caller to one canonical person, collapsing the roughly 88,000 raw identifiers it has seen into about 28,000 real people, with a merge audit so any consolidation can be reviewed.

On top of that sits the part I care about most: field-level provenance. Every value on a canonical record carries a record of where it came from, which source, which run, which raw row. That is roughly 181,000 origin records today, and it is what turns attribution from an assertion into something you can audit down to the field.

An idempotent orchestrator ties it together. Given a call and a recording, it resolves the person, finds or creates the matching HubSpot contact and deal, assigns an owner by round robin, and attaches the recording to the right record, and it does all of that safely if it runs twice. Canonical data is written back through the HubSpot CRM API, calls and recordings come from the RingCentral API, and every action is written to an audit log that now holds around 64,000 entries.

What was tricky, and the calls I made

  • Idempotency was the whole game. Because runs overlap and re-run, every write had to be safe to repeat, so the orchestrator is built on find-or-create and upsert semantics, and a replayed call never spawns a duplicate contact, deal, or recording.
  • Identity resolution has a long tail. Most callers resolve cleanly, but the edge cases, shared numbers, reassigned numbers, and one person spread across several identifiers, are where a naive match corrupts data, so merges are recorded in a separate audit rather than done silently.
  • HubSpot had to stay the source of truth for deals. I made the sync reconcile against HubSpot rather than push over it, so the pipeline enriches and attaches without ever fighting the CRM for ownership of a deal.
  • The API pushed back. Real rate limits and permission errors meant the writers needed backoff and careful handling rather than optimistic bulk calls, and recordings had their own lifecycle to manage when uploads had to be rebuilt.

The outcome

The client went from a manual, untrustworthy spreadsheet match to a running pipeline where any deal can be traced back to the call that started it and the source that drove it, on canonical data with the origin of every field on record. The figures here, about 28,000 canonical people, 181,000 field origins, and 64,000 audit entries, are the live state of the system rather than a projection. I did not run a formal revenue-attribution study on this engagement, so I will not put a percentage on it. The structural change is the point: attribution stopped being a guess and became something the team can audit.

Sources

M
Miloš Mitrović
Email Marketing for Ecommerce

Have a question or a project?

Whether it is about this post or a system you want built, I'm happy to talk.

Get in touch

404

Post not found. It may have been moved or the link is incorrect.

← Back to the blog
Summarize with AI
ChatGPT, Perplexity, and Grok open with the prompt ready to run. Claude, Gemini, and Copilot open a chat with the prompt copied; press Ctrl+V (Cmd+V on Mac) to paste. The full text is included, so it works even without web access.