CRM, MarTech and RevOps

Marketing events now reach Salesforce, and the funnel reports closed-loop attribution.

A B2B SaaS company ran demand gen in HubSpot and sold in Salesforce, but the events never crossed the seam. We built a custom HubSpot app that syncs marketing events into Salesforce, backfilled the history, and rebuilt ROI reporting without touching closed-won records.

94%
of closed-won deals matched to a marketing source after backfill, up from 41%
612,000
historical marketing events backfilled into Salesforce campaign membership
6 hrs to 35 min
monthly source-of-pipeline reporting, from manual export to a live dashboard
0
closed-won or renewal records altered during the migration

The challenge.

  • Our HubSpot events never made it into Salesforce, so the seam between marketing and sales was a black hole. A webinar registration, a content download, an event check-in, none of it landed on the Salesforce contact or the opportunity.
  • When a deal closed, we could not say which campaign touched it. Roughly 41% of closed-won had any marketing source attached, and even that we did not trust.
  • The native HubSpot-to-Salesforce connector synced contacts and a few fields, but it would not mirror marketing events into Salesforce campaigns, and the field mapping kept losing the original timestamp.
  • Source-of-pipeline reporting was a monthly export-and-stitch job in a spreadsheet. It took most of a day, two people disagreed on the numbers, and the board got a version we had to caveat.
  • Every fix we considered scared us, because our closed-won and renewal history lived in Salesforce and one bad sync could rewrite years of revenue records.

What we did.

  1. 1

    Mapped the event taxonomy and the broken seam first.

    Before writing code, we inventoried every HubSpot marketing event type (form submissions, email engagement, webinar and event attendance, list membership) and traced where each one died on the way to Salesforce. We defined a canonical event model, decided which events become Salesforce campaign members versus activity records, and agreed on the matching keys (email, then HubSpot contact ID to Salesforce contact and lead ID) so nothing double-counts.

  2. 2

    Built a custom HubSpot app, not another brittle workflow.

    We shipped a private HubSpot app backed by a Vercel-hosted microservice. It listens to HubSpot event webhooks, normalizes each event against the canonical model, and writes it into Salesforce as campaign membership with the original event timestamp preserved. The logic is observable: every sync writes a structured log, failures retry with backoff, and an idempotency key means a replayed webhook never creates a duplicate. Native tier and workflow limits stop being the ceiling because the business logic lives in code we own.

  3. 3

    Backfilled the history without touching closed-won.

    The hard part is the past. We backfilled 612,000 historical marketing events into Salesforce campaign membership in batches against a sandbox first, with a dry-run mode that reported what it would write before it wrote anything. The job only ever created campaign-member and activity records keyed to the original event date. It never updated opportunity amounts, stages, or close dates, so closed-won and renewal history stayed exactly as it was. We reconciled record counts before and after every batch.

  4. 4

    Rebuilt ROI reporting on the connected data.

    With events finally landing on contacts and opportunities, we rebuilt source-of-pipeline and influenced-revenue reporting as a live dashboard. First-touch, last-touch, and multi-touch views all read from the same synced campaign membership, so marketing and sales argue from one number. The monthly board export became a dashboard the RevOps lead opens in 35 minutes instead of rebuilding for six hours.

The outcome.

  • 94% of closed-won deals now carry a matched marketing source, up from 41% before the backfill, so attribution holds from first touch to closed deal.
  • 612,000 historical marketing events were backfilled into Salesforce campaign membership with the original timestamps intact, so the pipeline of the last several years reports the same way the new pipeline does.
  • Monthly source-of-pipeline reporting dropped from roughly six hours of manual export-and-stitch to a 35-minute review of a live dashboard.
  • Zero closed-won or renewal records were altered during the migration, confirmed by before-and-after record reconciliation on every batch.
  • The sync runs as an owned, observable HubSpot app with structured logging and retry logic, so RevOps can see exactly what synced and when, instead of trusting a black-box connector.
We had argued about source-of-pipeline for two years because the events never reached Salesforce. The custom app fixed the seam, the backfill gave us the history, and the board number stopped needing an asterisk. Nothing in our closed-won history moved, which is the part I cared about most.
Head of Revenue Operations, B2B SaaS company

Questions about this work.

Can you sync HubSpot marketing events into Salesforce without breaking our closed-won history?

Yes. We built a custom HubSpot app that writes marketing events into Salesforce as campaign membership and activity records keyed to the original event date. The backfill only creates those records. It never updates opportunity amounts, stages, or close dates, so closed-won and renewal history stays exactly as it was, and we reconcile record counts before and after every batch.

Why a custom HubSpot app instead of the native connector?

The native HubSpot-to-Salesforce connector syncs contacts and a defined set of fields, but it will not mirror the full range of marketing events into Salesforce campaigns, and it loses the original event timestamp. A private app backed by a microservice lets us define the event model, preserve timestamps, retry failures, and keep the logic observable, instead of working around native tier and workflow limits.

How do you backfill years of marketing events safely?

We run the backfill in batches against a sandbox first, with a dry-run mode that reports what it would write before writing anything. The job is idempotent, so a replay never duplicates a record, and it only ever creates campaign-member and activity records, never edits to revenue fields. We reconcile counts after each batch.

What does attribution look like once the events sync?

With events landing on contacts and opportunities, we rebuild source-of-pipeline and influenced-revenue reporting on the synced campaign membership. First-touch, last-touch, and multi-touch views read from the same data, so marketing and sales report one number instead of two spreadsheets that disagree.

Want a result like this? See where your systems are leaking pipeline.