Skip to main content
GUIDE · HUBSPOT9 min read

HubSpot + TrackLayer: server-side CRM events for B2B attribution

A production guide for sending HubSpot lifecycle, deal, and merge events into TrackLayer so B2B teams can optimize media against qualified pipeline, account movement, and real revenue instead of raw form fills.

Context

Why B2B needs CRM events

B2B attribution usually fails after the browser session, not during it. The ad click, landing page, and form submit are still important, but they do not tell the whole story once an account enters a real sales process. Buyers research across devices, get routed through SDR qualification, move between lifecycle stages, and often convert into revenue weeks after the first session. When only the browser event is measured, campaigns are optimized toward the easiest lead to capture rather than the lead that becomes a qualified opportunity. HubSpot is where that later truth becomes visible, because the CRM sees lifecycle state, ownership, deal creation, stage progression, and closed-won outcomes.

Feeding those CRM moments into TrackLayer closes the gap between demand generation and revenue operations. TrackLayer can preserve the click IDs, hashed identifiers, domains, and account metadata from the acquisition side, then join them to HubSpot stage changes and deal events from the CRM side. That gives paid media teams cleaner feedback loops for LinkedIn and Meta, gives ABM programs a better account-level view, and gives finance a clearer way to tie media spend to pipeline and revenue instead of vanity lead counts.

Requirements

Prerequisites

01

HubSpot Marketing Hub or Sales Hub Professional or higher so workflows, custom properties, and programmable automation are available.

02

A HubSpot private app token with scopes for contacts, companies, deals, and workflow-triggered webhook actions.

03

A TrackLayer Growth plan or higher with access to server-side ingestion, event mappings, and downstream destination routing.

04

LinkedIn Ads API access for offline or CRM conversion uploads, plus the Meta Conversions API destination if paid social optimization will use the same CRM events.

Build

Setup

Build the bridge in layers. First prove the HubSpot webhook can reach TrackLayer. Then normalize identifiers and event IDs. Then map lifecycle stages into canonical events. Then attach deal value and stage changes. That sequence makes debugging much easier because each replay answers one clear integration question.

Step 1

Create the HubSpot workflow webhook into TrackLayer

Start with one narrow workflow that fires when a contact becomes marketing qualified or when a deal enters a sales-owned stage. HubSpot should send the CRM object IDs, lifecycle state, and timestamps to your backend or directly to TrackLayer. The point is to treat HubSpot as the source of lifecycle truth while TrackLayer becomes the normalization layer that preserves attribution context and forwards only the canonical event downstream.

HubSpot Workflow → Send a webhook

POST https://edge.tracklayer.io/v1/ingest/crm
Authorization: Bearer tl_live_xxxxxxxxx
Content-Type: application/json

{
  "source": "hubspot",
  "event": "hubspot.lifecycle_changed",
  "occurred_at": "{{ date_now }}",
  "contact": {
    "id": "{{ contact.hs_object_id }}",
    "email": "{{ contact.email }}",
    "lifecycle_stage": "{{ contact.lifecyclestage }}",
    "lead_status": "{{ contact.hs_lead_status }}"
  },
  "company": {
    "id": "{{ company.hs_object_id }}",
    "domain": "{{ company.domain }}",
    "name": "{{ company.name }}"
  }
}
Step 2

Normalize and sign the HubSpot payload before forwarding

Many teams put a small middleware service between HubSpot and TrackLayer. That gives you one place to verify the private app token, suppress internal test traffic, normalize emails and domains, attach campaign metadata, and mint a stable event ID. Stable IDs matter because the same MQL, SQL, or opportunity event may be replayed by a workflow rerun or a delayed API retry. Deduplication only works if the event identity is deterministic.

import crypto from "node:crypto";

function normalizeEmail(value: string) {
  return value.trim().toLowerCase();
}

function buildEventId(contactId: string, stage: string, changedAt: string) {
  return crypto
    .createHash("sha256")
    .update([contactId, stage, changedAt].join(":"))
    .digest("hex");
}

export async function forwardHubSpotEvent(payload: {
  contact: { id: string; email: string; lifecycle_stage: string };
  company?: { domain?: string };
  occurred_at: string;
}) {
  const eventId = buildEventId(
    payload.contact.id,
    payload.contact.lifecycle_stage,
    payload.occurred_at
  );

  await fetch("https://edge.tracklayer.io/v1/events", {
    method: "POST",
    headers: {
      Authorization: "Bearer tl_live_xxxxxxxxx",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      event_id: eventId,
      event: "lead_stage_changed",
      source: "hubspot",
      occurred_at: payload.occurred_at,
      user_data: {
        email: normalizeEmail(payload.contact.email),
      },
      account_data: {
        domain: payload.company?.domain ?? null,
      },
      custom_data: {
        hubspot_contact_id: payload.contact.id,
        hubspot_lifecycle_stage: payload.contact.lifecycle_stage,
      },
    }),
  });
}
Step 3

Map HubSpot lifecycle stages to TrackLayer canonical events

Do not pass raw HubSpot stage names straight through to each destination. Keep the HubSpot labels for auditability, but map them once into a canonical event vocabulary that TrackLayer owns. That avoids brittle per-platform logic and lets revenue operations change internal stage naming without breaking LinkedIn or Meta event routing. The mapping should reflect business intent, not CRM field labels.

const lifecycleToCanonicalEvent: Record<string, string> = {
  subscriber: "lead_captured",
  lead: "lead_captured",
  marketingqualifiedlead: "qualified_lead",
  salesqualifiedlead: "sales_qualified_lead",
  opportunity: "opportunity_created",
  customer: "customer_won",
  evangelist: "customer_expanded",
};

function toCanonicalEvent(stage: string) {
  return lifecycleToCanonicalEvent[stage.toLowerCase()] ?? "crm_stage_changed";
}
Step 4

Emit deal.stage_changed with value and ownership context

Lifecycle stage alone is not enough for serious B2B attribution. The deal object carries the pipeline meaning: amount, close date, owner, pipeline ID, and the exact stage transition that tells you whether the account moved from discovery to demo, proposal, negotiation, or closed won. TrackLayer should receive the deal-stage event with both the new state and the commercial value so ad platforms and the warehouse can separate top-of-funnel efficiency from pipeline creation and revenue creation.

{
  "event": "deal.stage_changed",
  "source": "hubspot",
  "event_id": "deal_84721_contractsent_2026-04-23T11:04:19Z",
  "occurred_at": "2026-04-23T11:04:19Z",
  "user_data": {
    "email": "buyer@acme.io"
  },
  "account_data": {
    "domain": "acme.io",
    "company_name": "Acme",
    "hubspot_company_id": "3029911"
  },
  "custom_data": {
    "hubspot_contact_id": "102948",
    "hubspot_deal_id": "84721",
    "hubspot_pipeline": "sales_pipeline",
    "hubspot_deal_stage": "contractsent",
    "tracklayer_stage": "proposal_sent",
    "owner_id": "55192",
    "currency": "USD",
    "value": 36000,
    "billing_period": "annual"
  }
}
Step 5

Validate downstream mapping and replay safely

Once TrackLayer receives the CRM payloads, verify three things: the canonical event is correct, the event ID deduplicates retries, and the destination mapping matches the platform’s optimization object. LinkedIn may want an opportunity-created or qualified-lead conversion rule, while Meta may use a custom event or a standard Lead signal with value attached. Keep replay tooling behind a controlled endpoint so operations can resend a missed event without inventing a new event ID.

curl -X POST https://edge.tracklayer.io/v1/replay \
  -H "Authorization: Bearer tl_live_xxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "event_id": "deal_84721_contractsent_2026-04-23T11:04:19Z",
    "destinations": ["linkedin", "meta"],
    "reason": "destination_timeout"
  }'

Expected result:
{
  "ok": true,
  "deduplicated": false,
  "canonical_event": "proposal_sent",
  "replayed_destinations": 2
}
Schema

Pipeline stage event mapping

The mapping table is where CRM semantics become destination semantics. HubSpot can keep its own lifecycle vocabulary, but the canonical event name should remain stable so TrackLayer can route the same CRM event into ad APIs, reporting exports, and internal analytics without rewriting the meaning every time a team updates pipeline labels.

HubSpot lifecycleTrackLayer canonicalLinkedIn/Meta CAPI event
Leadlead_capturedMeta: Lead · LinkedIn: Lead Gen / Prospect
Marketing Qualified Leadqualified_leadMeta: QualifiedLead · LinkedIn: MQL conversion
Sales Qualified Leadsales_qualified_leadMeta: Custom SQL event · LinkedIn: SQL conversion
Opportunityopportunity_createdMeta: Opportunity custom event · LinkedIn: Opportunity conversion
Customercustomer_wonMeta: Purchase / Subscribe · LinkedIn: Closed Won conversion
Evangelist or expansion stagecustomer_expandedMeta: Purchase with expansion value · LinkedIn: Expansion / Renewal conversion
ABM

Account-based attribution

Account-based marketing needs more than person-level conversion counting. One stakeholder may click the ad, another may become the named contact in HubSpot, and a third may be attached to the deal that eventually closes. If those records stay isolated, the campaign looks weaker than it really is. By carrying company domain, HubSpot company ID, account owner, target-account status, and deal associations into TrackLayer, the event stream can show that paid media is influencing movement inside the account even when the individual contact changes. That is the practical ABM win: better visibility into account progression, not just more individual leads.

Revenue

Revenue attribution

Revenue attribution needs explicit logic, because one HubSpot deal amount can represent very different economics. For SaaS, MRR is often the cleanest optimization signal for near-term efficiency, especially when contracts start monthly and expansions happen later. ARR is more useful when the sales motion is annual and the buying decision is made on total contract value rather than the monthly equivalent. Contract value can be the right reporting metric for services or multi-year deals, but it should be handled carefully if revenue is recognized over time or if churn and downgrades are common.

The operational rule is to send the value that matches the optimization objective and keep the alternate values in the event envelope. A team might optimize paid social on first-year ARR, monitor MRR payback internally, and reserve full contract value for finance views. TrackLayer can store all three values while exposing only the destination-safe one to Meta or LinkedIn. That keeps bidding aligned with the business outcome you actually care about instead of whichever number was easiest to pull from the deal record.

Identity

Contact merge events

HubSpot contact merges matter because deduplication changes the identity truth that attribution depends on. A buyer may appear first as a webinar lead with a personal email, later as a demo request with a work email, and eventually as the primary contact on a deal. When HubSpot merges those records, TrackLayer should ingest the merge event as an identity graph update rather than as a new conversion. The old contact IDs become aliases, the surviving record becomes the durable node, and historical events can resolve to the unified identity without double-counting the person across campaigns or destinations.

This is especially important for sales-assisted pipelines where enrichment tools, list uploads, and manual CRM cleanup are common. If merge history is ignored, matched audiences fragment, SQL and opportunity counts drift, and the same account can look like multiple net-new wins. When merge history flows into TrackLayer, identity stitching gets stronger over time instead of noisier. The result is a cleaner person-and-account graph that keeps ABM, lifecycle attribution, and warehouse reporting anchored to the same reconciled record.

FAQ

Common questions

Should HubSpot replace browser tracking?

No. HubSpot CRM events should complement browser and server web events, not replace them. Browser events capture the click and session context, while HubSpot confirms whether the lead became qualified pipeline or revenue later in the sales cycle.

Do I need to send every HubSpot property to TrackLayer?

No. Send the fields required for identity, stage mapping, value, and auditing. Keep the payload disciplined so downstream routing stays stable and privacy review remains manageable.

Which timestamp should drive attribution?

Use the timestamp for the business action that actually happened: lifecycle change time, deal-stage change time, or closed-won time. Import time is useful for logs, but it is usually the wrong timestamp for attribution windows.

How should recycled leads be handled?

Treat recycled or reopened leads as a distinct lifecycle action, not as a brand-new lead. Recounting the same person as a fresh acquisition can inflate performance and confuse bid optimization.

Can the same HubSpot event feed both LinkedIn and Meta?

Yes, if TrackLayer owns the canonical event model. One CRM event can be normalized once and then mapped into the right object for each destination, with platform-specific naming and value logic handled at the edge.

Next reads

Related implementation guides

We use essential cookies to keep the site secure and functional. Analytics and third-party tags run only with your consent. See our Cookie Policy.

We use essential cookies to keep the site secure and functional. Analytics and third-party tags run only with your consent. See our Cookie Policy.