CRM, MarTech and RevOps
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.