If you're seeing a pile of HubSpot contacts with an original source of Offline Sources and no ad click history, the usual cause is an app sign-up that creates contacts through the API without passing along the visitor's HubSpot browser cookie.
I see this setup at a lot of SaaS companies. Marketing runs paid ads, HubSpot tracks the visits, and engineering wires the sign-up form into HubSpot. Everything looks connected, but the contacts that come out the other end have no ad click, no landing page, and no session history attached to them.
A Common Example
- Someone clicks a Google Ads ad.
- They land on a landing page on
company.com. - They click the call to action and go straight to a sign-up form.
- The sign-up form lives in the app, usually on a separate domain like
app.company.com, so it can't be a HubSpot form. - Engineering is asked to "connect HubSpot," so they create the contact with the HubSpot API using an upsert, or a search for the contact by email followed by an update or insert (same thing, just more roundabout).
The contact gets created with whatever the sign-up form collected: email, name, maybe a company. What's missing is the link to everything that person did before they signed up.
How HubSpot Tracking Works
When someone visits a site with the HubSpot tracking code, HubSpot starts an anonymous profile for them, stored in the hubspotutk cookie. It records that they came from an ad, which pages they viewed, and what else they did. It just doesn't know who they are yet.
When that person submits a HubSpot form, HubSpot creates a contact and merges the anonymous history into it. If you open a contact record and look below the form submission in the timeline, you'll see every page view and ad click that happened before it.
A HubSpot form does this gluing for you. When you create contacts through the API you have to glue it together yourself. It's not difficult, but it's commonly missed.
What Broken Attribution Looks Like
- Hundreds or thousands of contacts with an original source of Offline Sources
- Contact records that only contain the fields from the sign-up form
- Sometimes a little more data from billing or profile updates, because developers often sync any change to a user into HubSpot, but still no browser session data
- Paid ad campaigns that look like they produce nothing in HubSpot reports
The last one is the costly part. If you send conversions from HubSpot back to Google Ads or Meta for conversion optimized campaigns, those conversions depend on the ad click being attached to the contact. With no session data there's no click to attribute, so the ad platforms never learn which clicks turned into sign-ups and can't optimize toward them.
Fix #1: Pass the HubSpot Cookie to the API
This is the clean fix, and it's the way HubSpot documents it. Send the sign-up to HubSpot through the Forms API with the value of the visitor's hubspotutk cookie in the context.hutk field. HubSpot then treats it like a regular form submission and merges the anonymous session history into the contact.
One detail that trips people up: the CRM Contacts API (the upsert endpoint) has no field for hutk. Engineering can keep the upsert for syncing user data, but the sign-up itself needs to go through a form submission to carry the cookie.
// Server side, after a successful sign-up
const hutk = req.cookies.hubspotutk; // set by the HubSpot tracking code
await fetch(
`https://api.hsforms.com/submissions/v3/integration/secure/submit/${PORTAL_ID}/${FORM_GUID}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${HUBSPOT_TOKEN}`,
},
body: JSON.stringify({
fields: [
{ name: 'email', value: user.email },
{ name: 'firstname', value: user.firstName },
{ name: 'lastname', value: user.lastName },
],
context: {
hutk,
pageUri: 'https://app.company.com/signup',
pageName: 'Sign Up',
},
}),
}
);
A few things to check:
- The
hubspotutkcookie is set on the root domain, so a server onapp.company.comwill receive it in the request as long as the HubSpot tracking code runs oncompany.com. - If the app is on a completely different domain, add it to HubSpot's tracking domains so the cookie carries across.
- Create a form in HubSpot for the sign-up so the submission has somewhere to land. It's also a handy way to report on sign-ups.
Fix #2: Identify in the Browser
If you can't get engineering time, or they won't change the sign-up flow, you can make the connection from the browser instead. After the person signs up, call HubSpot's identify method with their email on a page that loads the tracking code:
var _hsq = (window._hsq = window._hsq || []);
_hsq.push(['identify', { email: user.email }]);
_hsq.push(['trackPageView']);
The trackPageView call matters. identify only stores the email in the tracker and nothing is sent to HubSpot until the next page view or event. As long as the email matches the contact the server created, HubSpot ties the browser session to that contact.
It works, but it's a monkey patch.
Where Identify Causes Problems
Only call identify with the person's own email, at sign-up. Anywhere else in the app gets risky:
- Invite a friend forms: the email typed in belongs to someone else, so identifying with it ties your user's browser to the wrong contact.
-
Email changes in a profile: if you don't call
identify, the contact keeps the old email, which breaks email marketing (one of the main reasons you're paying for HubSpot). If you do call it, HubSpot treats the new email as a new person and creates a brand new contact, even though the browser cookie is the same one already tied to the original contact. HubSpot's documentation doesn't mention this anywhere, but it's what happens in practice.
Segment handles this with an alias call that links an old identity to a new one, which is one of the reasons I like it for this kind of work (more on that on my Segment consulting page). HubSpot doesn't have an equivalent, so with this workaround it's best to leave profile updates alone.
Fix the Root Cause When You Can
I always push for fix #1. It's a small change for an engineering team, it follows HubSpot's documented approach, and it avoids every edge case above. The browser workaround is fine for getting attribution back quickly while you wait on engineering, but it shouldn't be the long-term setup.
If your HubSpot attribution is broken and you want help tracking down where the session is getting lost, take a look at my HubSpot development work.
Top comments (0)