Connect HubSpot to see real contact names, company details, and deal values directly in your analytics. Instead of seeing anonymous GA4 client IDs, you’ll see actual people who filled out your forms, their company information, and the value of deals they’re associated with.
This integration requires a Pro plan or higher. The HubSpot tab will appear in Settings only for users on eligible plans.
Setup Overview
There are five steps. Complete them in order, each one depends on the last.
1
Connect HubSpot
2
GA4 Property
3
Tracking Script
4
Form Setup
5
Sync Contacts
Step 1: Connect Your HubSpot Account
The first thing to do is authorise AttributeIQ to read your HubSpot contacts and deals. This is a one-time OAuth flow, you’ll never need to do it again.
How to connect
1
In your app, go to Settings → Integrations → HubSpot
2
Click Connect HubSpot and complete the OAuth authorisation
3
You’ll be redirected back and should see a “Connected” status with your portal ID
Step 2: GA4 Client ID Property in HubSpot
AttributeIQ automatically creates the ga4_client_id contact property in your HubSpot account when you connect in Step 1. In most cases you don’t need to do anything here.
If the automatic creation fails, for example, due to HubSpot permission restrictions, follow the manual steps below.
Manual setup
1
Log into HubSpot and click the settings gear in the top-right corner
2
Go to Properties → Contact properties
3
Click Create property
4
Set Label to GA4 Client ID
5
Set Internal name to exactly ga4_client_id, this must match precisely
6
Set Field type to Single-line text, then click Create
Step 3: Add the Tracking Script to Your Website
This script does three things: it captures the visitor’s GA4 client ID, tracks every page visit to build a session journey, and, when someone submits a HubSpot embedded form, links their email to their session automatically.
Paste this script into your website’s <head> section, replacing G-XXXXXXXXXX with your GA4 Measurement ID.
Tracking script
<!-- AttributeIQ + GA4 tracking -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
/* ── Base GA4 setup ──────────────────────────────────────── */
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
/* ── Persistent user ID ──────────────────────────────────── */
function getOrCreatePersistentUserId() {
let persistentId = localStorage.getItem('persistent_user_id');
if (!persistentId) {
persistentId = 'user_' + Date.now() + '_' + Math.random().toString(36).substring(2, 10);
localStorage.setItem('persistent_user_id', persistentId);
}
return persistentId;
}
const persistentUserId = getOrCreatePersistentUserId();
/* ── GA4 client ID capture ───────────────────────────────── */
gtag('get', 'G-XXXXXXXXXX', 'client_id', (id) => {
window.ga4ClientId = id;
localStorage.setItem('ga4_client_id', id);
console.log('AttributeIQ: GA4 Client ID captured:', id);
});
/* ── Page visit tracking ─────────────────────────────────── */
function trackPage() {
const id = localStorage.getItem('ga4_client_id') || window.ga4ClientId;
if (!id) return;
fetch('https://attribute-iq.com/api/track/track-page-visit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pageUrl: location.href, ga4ClientId: id, persistentUserId: persistentUserId }),
}).catch(() => {});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', trackPage);
} else { trackPage(); }
/* ── HubSpot embedded form tracking ────────────────────────
Only relevant if you use HubSpot's embedded forms.
This handles injecting the GA4 ID into the form payload
and capturing identity once the form is submitted.
If you only use custom HTML forms, this block is inactive
but harmless to leave in.
─────────────────────────────────────────────────────────── */
window.addEventListener('message', function(e) {
if (e.data?.type !== 'hsFormCallback') return;
if (e.data.eventName === 'onBeforeFormSubmit' && Array.isArray(e.data.data)) {
const id = localStorage.getItem('ga4_client_id') || window.ga4ClientId;
if (id) {
e.data.data.push({ name: 'ga4_client_id', value: id });
}
}
if (e.data.eventName === 'onFormSubmit') {
const email = e.data.data?.find(function(f) { return f.name === 'email'; })?.value;
const id = localStorage.getItem('ga4_client_id') || window.ga4ClientId;
if (email && id) {
fetch('https://attribute-iq.com/api/track/capture-identity', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email,
ga4_client_id: id,
persistent_user_id: persistentUserId,
property_id: 'default'
}),
}).catch(() => {});
}
}
});
</script>
Verify the Script is Working
Script is live on your site
Paste the script into your <head>, publish, and load any page.
GA4 client ID is being captured
Open your browser’s developer console (F12). You should see “AttributeIQ: GA4 Client ID captured: xxx”.
HubSpot embedded forms are tracked
Submit a test HubSpot form. The contact in HubSpot should have the GA4 Client ID field populated.
Step 4: Custom Form Setup (Optional)
If you use HubSpot embedded forms, the script from Step 3 already handles everything, skip ahead to Step 5. If you have custom HTML forms that use JavaScript to submit data directly to HubSpot’s API, follow the steps below to connect them.
Setting up a custom HTML form
1
In HubSpot go to Marketing → Forms and click Create form
2
Add a field for each field on your front-end form. The internal name of each field must exactly match what you pass in the snippet’s fields array, for example, firstname, email, message
3
Add one more field: search for ga4_client_id and select it, then set it as a Hidden field
4
Save and publish the form. Copy the form GUID from the URL, it looks like xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
5
In the snippet below, replace YOUR_FORM_GUID with your form GUID, update the fields array to match your form, and replace YOUR_FORM_SELECTOR with your form’s CSS selector (e.g. .contact-form form)
6
Paste the snippet into the <head> of the page where your form lives
Custom form snippet
document.addEventListener('DOMContentLoaded', function() {
// Replace YOUR_FORM_SELECTOR with your form's CSS selector
// e.g. '.contact-form form', '#signup form', '.hero-form form'
const form = document.querySelector('YOUR_FORM_SELECTOR');
if (!form) return;
form.addEventListener('submit', function(e) {
// Add a line here for each field in your form
const name = form.querySelector('input[name="name"]')?.value || '';
const email = form.querySelector('input[type="email"]')?.value || '';
const subject = form.querySelector('select[name="subject"]')?.value || '';
const message = form.querySelector('textarea[name="message"]')?.value || '';
// These are set automatically by the main script, do not change
const ga4ClientId = localStorage.getItem('ga4_client_id') || window.ga4ClientId;
const persistentUserId = localStorage.getItem('persistent_user_id');
if (!email || !ga4ClientId) return;
// Link this visitor’s email to their GA4 session
fetch('https://attribute-iq.com/api/track/capture-identity', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email,
ga4_client_id: ga4ClientId,
persistent_user_id: persistentUserId,
property_id: 'default'
})
}).catch(() => {});
// Send form data to HubSpot
// Replace YOUR_PORTAL_ID and YOUR_FORM_GUID with your values
// Update the fields array to match your form’s internal names
fetch('https://api.hsforms.com/submissions/v3/integration/submit/YOUR_PORTAL_ID/YOUR_FORM_GUID', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fields: [
{ name: 'firstname', value: name },
{ name: 'email', value: email },
{ name: 'subject', value: subject },
{ name: 'message', value: message },
{ name: 'ga4_client_id', value: ga4ClientId }, // always include this
],
context: { pageUri: window.location.href, pageName: document.title }
})
}).catch(() => {});
});
});
Step 5: Sync Contacts
With everything in place, run your first sync to pull contacts from HubSpot. AttributeIQ will import every contact that has a ga4_client_id property populated and match them to their session journeys.
Running a sync
1
In the HubSpot tab, click Sync now
2
Wait for the sync to complete, you’ll see a “Synced X contacts” confirmation
3
Go to Journey Explorer. Within 2–3 minutes, anonymous IDs will be replaced with real contact names
Contacts sync automatically every 6 hours. Click Sync now at any time for an immediate update.
Troubleshooting
GA4 client ID not appearing in HubSpot
Possible causes
The ga4_client_id property hasn’t been created in HubSpot
The property’s internal name doesn’t match ga4_client_id exactly
The form doesn’t include the ga4_client_id field
The field hasn’t been added as a hidden field in the HubSpot form builder
Solution: Verify the property exists with the correct internal name. Add it as a hidden field to your HubSpot form.
Sync returns 0 contacts
Possible causes
No contacts in HubSpot have the ga4_client_id property populated yet
The HubSpot connection token has expired or been revoked
The ga4_client_id property doesn’t exist in your HubSpot account
Solution: Submit a test form first to create a contact with a GA4 client ID, then re-run the sync.
No contact names in Journey Explorer
Possible causes
A sync hasn’t been run since the contacts were created
The GA4 client ID in the journey doesn’t match any synced contact
The contact’s GA4 client ID was populated after the last sync
Solution: Run a manual sync in Settings → HubSpot. Wait 2–3 minutes and refresh Journey Explorer.