Integrate HubSpot with Optimizely Web Experimentation
TL;DR
HubSpot ships its own A/B testing for landing pages, emails and CTAs, which means most people arriving at this question are not asking how to connect two tools. They are asking whether they need the second one. The honest answer depends on what you are testing and on how much statistical rigour the decision deserves — and for a large class of marketing tests, HubSpot's built-in split is genuinely enough.
Where it stops being enough is specific and worth naming up front. This guide covers the comparison first, then the integration for teams who need both: installing Optimizely Web Experimentation alongside the HubSpot tracking code, forwarding HubSpot custom behavioral events into Optimizely as conversions, and writing the variation back onto the contact record so the CRM can segment on it. If you are still deciding, the experiment design guide is the better place to start.
HubSpot A/B Testing or Optimizely Web Experimentation
These are alternatives for any given test, not layers. Running both on the same page at the same time splits your traffic twice and neither tool knows about the other's assignment.
Consideration | HubSpot A/B testing | Optimizely Web Experimentation |
|---|---|---|
Where it runs | HubSpot-hosted pages and emails only | Any page with the snippet |
What it can vary | Whole page or email variants | Any DOM change, on any page |
Traffic split | 50/50, two variants | Arbitrary splits, many variants |
Statistics | Simple comparison, no sequential correction | Stats Engine with always-valid inference |
Targeting | HubSpot lists and page rules | Audience conditions, URL rules, custom attributes |
Conversion definition | HubSpot form submissions and page views | Any custom event you push |
Cost | Included in Marketing Hub Professional and above | Separate licence |
Choose HubSpot when the test is one HubSpot landing page against another, the conversion is a HubSpot form, and the decision is reversible and cheap. Choose Optimizely when the test spans pages HubSpot does not host, needs more than two variants, needs targeting HubSpot cannot express, or when the decision is expensive enough that peeking at an uncorrected p-value would be a real risk — the sequential testing article explains why that last point matters more than it sounds.
How the Two Systems Meet in the Page
Both tools are browser scripts on the same page. HubSpot's tracking code identifies the visitor and records behaviour against a contact; the Optimizely snippet decides a variation and records custom events against that decision. Neither knows about the other until you connect them, and the connection runs in both directions.
flowchart TD
A[Visitor loads a page] --> B[Optimizely snippet decides the variation]
B --> C[HubSpot tracking code loads and identifies the contact]
B --> D[track_layer_decision writes the variation to _hsq]
C --> E[Visitor triggers a HubSpot behavioral event]
E --> F{Event in the allowlist?}
F -->|yes| G[Push onto the Optimizely queue as a custom event]
F -->|no| H[Dropped, silently]
D --> I[Contact property set, segmentable in HubSpot]Prerequisites
The Optimizely Web Experimentation snippet, installed on the pages under test.
The HubSpot tracking code installed, whether through a HubSpot-hosted page, the WordPress plugin, or a manual embed.
Custom behavioral events defined in HubSpot. These require Marketing Hub Enterprise, and their internal names have the form
pe<portalId>_<name>— that internal name is what arrives in the browser, not the friendly label.Custom events created in Optimizely for each behavioral event you forward, with API names noted. An event pushed under a name that does not exist in Optimizely is discarded without an error.
Two contact properties in HubSpot, single-line text, for the experiment and variation names.
Step 1: Install the Snippet Ahead of the HubSpot Tracking Code
The order matters in one direction only: the Optimizely snippet must run before the page paints, and the HubSpot tracking code must exist before you push to _hsq. Putting Optimizely first satisfies both.
<head>
<script src="https://cdn.optimizely.com/js/YOUR_PROJECT_ID.js"></script>
<!-- HubSpot tracking code below -->
</head>
On HubSpot-hosted pages, the snippet goes in Settings → Website → Pages → Site header HTML. Do not add async or defer — an asynchronous decision races the first paint and the visitor sees the original content flash before the variation replaces it.
Step 2: Forward HubSpot Behavioral Events as Optimizely Custom Events
HubSpot's _hsq queue is an array your code can wrap, exactly like Optimizely's. Wrapping it lets you observe every behavioral event the site sends without editing any of the call sites that send them.
The Event Map
Only forward events a metric will be built on. HubSpot portals accumulate behavioral events quickly, and forwarding all of them means creating and maintaining a matching custom event in Optimizely for each.
{
"events": {
"pe1234567_demo_requested": "demo_requested",
"pe1234567_pricing_viewed": "pricing_viewed",
"pe1234567_trial_started": "trial_started"
},
"currencyProperty": "dealAmount",
"numericProperty": "seats",
"properties": [
{ "from": "plan", "to": "Plan" },
{ "from": "lifecycleStage", "to": "Lifecycle Stage" },
{ "from": "source", "to": "Source" }
]
}
The portal id in those keys is not decoration. Two HubSpot portals — a sandbox and production, say — produce different internal names for the same friendly event label, so a map copied between environments silently forwards nothing.
revenue is a reserved tag and must be an integer number of cents; Optimizely discards a non-integer revenue value, so a deal amount of 54.99 sent unconverted yields a revenue metric reporting zero. value is a plain number and is where a seat count belongs.
The Forwarder
const FORWARDED_EVENTS = {
pe1234567_demo_requested: 'demo_requested',
pe1234567_pricing_viewed: 'pricing_viewed',
pe1234567_trial_started: 'trial_started',
};
function forwardToOptimizely(entry) {
const eventName = FORWARDED_EVENTS[entry.name];
if (!eventName) {
return;
}
const properties = entry.properties || {};
const tags = {};
if (typeof properties.dealAmount === 'number') {
tags.revenue = Math.round(properties.dealAmount * 100);
}
if (typeof properties.seats === 'number') {
tags.value = properties.seats;
}
window['optimizely'] = window['optimizely'] || [];
window['optimizely'].push({
type: 'event',
eventName: eventName,
tags: tags,
properties: {
Plan: properties.plan,
'Lifecycle Stage': properties.lifecycleStage,
Source: properties.source,
},
});
}
window._hsq = window._hsq || [];
const nativePush = window._hsq.push;
window._hsq.push = function (command) {
if (Array.isArray(command) && command[0] === 'trackCustomBehavioralEvent') {
forwardToOptimizely(command[1] || {});
}
return nativePush.apply(this, arguments);
};
What the Payload Looks Like
A demo request from a prospect on a £54.99 plan with five seats produces this:
{
"type": "event",
"eventName": "demo_requested",
"tags": { "revenue": 5499, "value": 5 },
"properties": { "Plan": "Pro", "Lifecycle Stage": "marketingqualifiedlead", "Source": "pricing-page" }
}
The HubSpot integration demo runs the same allowlist and mapping, including an event marketing added later that nobody put in the map.
Step 3: Write the Variation Back onto the HubSpot Contact
The other direction is more valuable than it first looks. Once the variation is a contact property, HubSpot lists, workflows and reports can all segment on it — which is how you find out whether the variation that won on form fills also won on closed revenue three months later.
Optimizely Web Experimentation supports this through a custom analytics integration. The track_layer_decision field is a top-level script, not a function body: a bare return in it produces 'return' outside of function and the integration never runs.
{
"plugin_type": "analytics_integration",
"name": "HubSpot Contact Property",
"options": {
"track_layer_decision": "var experimentName = campaign.name;\nvar variationName = decision.variation_name;\nwindow['optimizely'].get('utils').waitUntil(function () {\n return window._hsq && typeof window._hsq.push === 'function';\n}).then(function () {\n window._hsq.push(['identify', {\n optimizely_experiment: experimentName,\n optimizely_variation: variationName\n }]);\n window._hsq.push(['trackPageView']);\n});"
}
}
Two details in there earn their place. The state lookups happen before waitUntil, because campaign and decision are in scope at call time and may not be inside a deferred callback. And the identify is followed by a trackPageView, because HubSpot only commits identify data to the contact on the next tracked interaction — an identify with nothing after it is discarded.
Gotchas
Both tools testing the same page. If a HubSpot A/B test and an Optimizely experiment run on one page, each visitor is split twice and neither report is trustworthy. Turn one off.
Behavioral events need Enterprise. On lower tiers there are no custom behavioral events to forward. The fallback is to listen for HubSpot form submissions with the hsFormCallback message event and push those instead.
Contact-property writes are not immediate.identify attaches data to the tracking cookie and commits it on the next tracked hit. A test that checks the contact record instantly will see nothing.
GDPR mode. With HubSpot's consent banner enabled, _hsq calls before consent are queued and may never be sent. Optimizely's own bucketing is unaffected, so the two systems can legitimately disagree on totals.
Verifying the Integration
Load a page with the console open, confirm window.optimizely is defined before HubSpot's script, and fire one allowlisted behavioral event by hand from the console. Confirm a matching push reaches the Optimizely queue with the properties you expect, then fire an event that is not in the map and confirm nothing is pushed.
For the return direction, activate an experiment, then open the contact record in HubSpot and confirm the two properties are populated after the next page view — not immediately. Once they are, build a HubSpot list on the variation property and check that its membership count is in the same neighbourhood as Optimizely's visitor count for that variation. They will not match exactly, because HubSpot only knows contacts it has identified, but a list with a handful of members against thousands of visitors means the identify is not committing.