Integrate Pendo with Optimizely Web Experimentation
TL;DR
Pendo is a product experience platform that combines product analytics with in-app guidance — it autocaptures page views and feature clicks, layers funnels, paths, and retention on top of that behavioral data, and lets you ship in-app guides, polls, and NPS surveys without engineering work. Integrating Pendo with Optimizely Web Experimentation attaches experiment and variation data to the Pendo visitor, so you can segment Pendo's funnels and retention reports by variation, target guides at the users who saw a specific variation, and quantify how an experiment moved the product-usage metrics you already track in Pendo.
Optimizely does not publish an official Pendo integration — Pendo is not in Optimizely's list of apps maintained by third parties, and Pendo's own analytics agent does not autodetect Optimizely state the way some tools do. The integration is therefore a custom one. This guide covers a single supported method: a custom JSON integration that uses Optimizely's Custom Analytics Integration with the track_layer_decision callback to call Pendo's browser agent (pendo.track() and a visitor metadata update) at decision time. A dedicated section covers an important constraint — pendo.track() requires Pendo's Track Events feature to be enabled for your subscription — so the integration is verified end to end before you rely on the data.
How the Integration Works
When Optimizely buckets a visitor into an experiment or personalization campaign, it fires a decision callback. The custom integration captures this callback and sends data to Pendo in two forms: a discrete Track Event named "Experiment Viewed" carrying campaign metadata, and a visitor metadata update (via pendo.identify()) so the experiment context attaches to the Pendo visitor profile and is available for segmentation. Pendo then associates the experiment with the visitor's subsequent activity, making it possible to build segments, funnels, and guide-targeting rules around the variation a visitor received.
flowchart LR
A[Visitor lands on page] --> B[Optimizely makes bucketing decision]
B --> C[Decision callback fires]
C --> D["pendo.identify() updates visitor metadata"]
C --> E["pendo.track('Experiment Viewed')"]
D --> F[Experiment context on Pendo visitor profile]
E --> G[Pendo Track Event stream]
F --> H[Pendo: segments, funnels, retention, guide targeting]
G --> H
H --> I[Segment product analytics and guides by variation]
Unlike a pure web-analytics tool, Pendo's value here is the combination of analytics and in-app guidance. The experiment property is not only a dimension for funnels and retention — it is also a segment rule you can use to target a guide, a poll, or an NPS survey at exactly the visitors who saw a given variation.
Custom Integration Event Properties
The following properties are sent with the "Experiment Viewed" Track Event in the custom integration:
Property | Type | Description |
|---|---|---|
| string | The campaign (layer) ID in Optimizely |
| string | The experiment ID within the campaign |
| string | Human-readable experiment name |
| string | The assigned variation ID |
| string | Human-readable variation name or |
| boolean | Whether the visitor is in the holdback group |
The experiment_name/variation_name pair is also written to the Pendo visitor profile as a metadata field (keyed [Optimizely] ExperimentName) so it persists on the visitor across sessions and is available as a segment rule.
Prerequisites
Before configuring the integration, confirm the following:
Pendo agent (install script) is installed and initialized on your site, with
pendo.initialize()called for known visitors (so the visitor has a stablevisitorId). The agent is what exposes the globalwindow.pendoobject the integration calls.Track Events feature is enabled for your Pendo subscription, and your Pendo agent is on web SDK version 2.14.3 or later — client-side Track Events (
pendo.track()) are only available from that version. If Track Events is not enabled,pendo.track()does nothing and the "Experiment Viewed" event never appears. Confirm this with your Pendo administrator before relying on the integration.Optimizely Web Experimentation snippet is deployed on the same pages.
Mask descriptive names is disabled (Optimizely Settings > Privacy), otherwise Pendo receives numeric IDs instead of readable experiment and variation names. Disabling it exposes your variation names in client-side source.
You have access to the Pendo application (Data Explorer and the Track Events page) to verify incoming events.
You have admin access to your Optimizely project (Settings > Integrations) to create the custom JSON integration.
Load Order
The track_layer_decision callback fires immediately when Optimizely makes a bucketing decision. Because the Pendo agent loads asynchronously, the callback uses window.optimizely.get('utils').waitUntil() to defer execution until window.pendo and its track function exist. This means strict load order between the two snippets is not critical — the decision is held until Pendo initializes, then sent. As a general rule, install the Pendo agent in the <head> so it initializes as early as possible.
A typical <head> configuration looks like this:
<head>
<!-- 1. Pendo agent install script (loads first).
Copy the complete snippet from Pendo > Settings > Subscription Settings
(App Details). It hardcodes your app's API key. -->
<script>
(function(apiKey){
(function(p,e,n,d,o){var v,w,x,y,z;o=p[d]=p[d]||{};o._q=o._q||[];
v=['initialize','identify','updateOptions','pageLoad','track'];for(w=0,x=v.length;w<x;++w)(function(m){
o[m]=o[m]||function(){o._q[m===v[0]?'unshift':'push']([m].concat([].slice.call(arguments,0)));};})(v[w]);
y=e.createElement(n);y.async=!0;y.src='https://cdn.pendo.io/agent/static/'+apiKey+'/pendo.js';
z=e.getElementsByTagName(n)[0];z.parentNode.insertBefore(y,z);})(window,document,'script','pendo');
// Identify the visitor as early as you can.
pendo.initialize({
visitor: { id: 'VISITOR_UNIQUE_ID' /* required; use your stable user ID */ },
account: { id: 'ACCOUNT_UNIQUE_ID' /* optional */ }
});
})('YOUR_PENDO_API_KEY');
</script>
<!-- 2. Optimizely Web snippet (loads second) -->
<script src="https://cdn.optimizely.com/js/YOUR_PROJECT_ID.js"></script>
</head>
Copy the full agent snippet from Pendo > Settings > Subscription Settings (App Details) — it already contains your app's API key. Do not reconstruct it by hand. The integration depends only on the global pendo object that the snippet creates and the pendo.track / pendo.identify methods it exposes.
Method 1: Custom JSON Integration
Because Optimizely does not ship a native Pendo integration, the custom JSON integration is the path to use. It uses Optimizely's Custom Analytics Integration (JSON plugin) with the track_layer_decision callback to call the Pendo browser agent directly at decision time.
Note: Optimizely's custom integration code does not run in Preview Mode — this is consistent with all built-in integrations. To generate a decision that fires the callback, browse the live experiment as a bucketed visitor rather than relying on Preview.
Creating the JSON Integration
In your Optimizely project, go to Settings > Integrations.
Click Create Analytics Integration > Using JSON.
Paste the following configuration:
{
"plugin_type": "analytics_integration",
"name": "Pendo (Custom)",
"form_schema": [],
"description": "Sends Optimizely experiment decisions to Pendo as a Track Event and a visitor metadata update via the Pendo browser agent",
"options": {
"track_layer_decision": "var state = window['optimizely'].get('state');\nvar expName = String(campaignId);\nvar varName = String(variationId);\n\nif (state) {\n try {\n var decisionObj = state.getDecisionObject({ campaignId: campaignId });\n if (decisionObj) {\n expName = decisionObj.experiment || expName;\n varName = decisionObj.variation || varName;\n }\n } catch (e) {}\n}\n\nvar variationValue = isHoldback ? 'holdback' : varName;\nvar propertyKey = '[Optimizely] ' + expName;\n\nvar utils = window['optimizely'].get('utils');\nutils.waitUntil(function() {\n return typeof window.pendo !== 'undefined' && typeof window.pendo.track === 'function';\n}).then(function() {\n var visitorUpdate = {};\n visitorUpdate[propertyKey] = variationValue;\n try {\n if (typeof window.pendo.identify === 'function') {\n window.pendo.identify({ visitor: visitorUpdate });\n }\n } catch (e) {}\n\n window.pendo.track('Experiment Viewed', {\n campaign_id: String(campaignId),\n experiment_id: String(experimentId),\n experiment_name: expName,\n variation_id: String(variationId),\n variation_name: variationValue,\n is_holdback: isHoldback\n });\n});\n"
}
}
Click Save Integration.
Toggle the integration to Enabled in Settings > Integrations.
Optionally check Enable this integration by default for all new experiments.
For existing experiments, open each experiment's Manage Experiments > Integrations tab and enable the "Pendo (Custom)" integration, then Save.
What the Callback Does
The options.track_layer_decision callback fires each time Optimizely makes a bucketing decision. Here is a breakdown of the logic:
Retrieving human-readable names: state.getDecisionObject({ campaignId }) looks up experiment and variation names from Optimizely's state API. If Mask descriptive names is enabled, the lookup returns numeric IDs and the callback falls back to them.
var state = window['optimizely'].get('state');
var expName = String(campaignId);
var varName = String(variationId);
if (state) {
try {
var decisionObj = state.getDecisionObject({ campaignId: campaignId });
if (decisionObj) {
expName = decisionObj.experiment || expName;
varName = decisionObj.variation || varName;
}
} catch (e) {}
}
Handling holdbacks: For experiments running at less than 100% traffic, Optimizely still makes a bucketing decision for visitors held back from the experiment and exposes isHoldback. The callback records those visitors with variation_name: 'holdback' rather than dropping or mislabeling them.
var variationValue = isHoldback ? 'holdback' : varName;
var propertyKey = '[Optimizely] ' + expName;
Waiting for Pendo to be ready: optimizely.get('utils').waitUntil() defers execution until window.pendo.track exists. Because the Pendo agent loads asynchronously, this avoids calling an undefined function if the agent has not initialized yet.
var utils = window['optimizely'].get('utils');
utils.waitUntil(function() {
return typeof window.pendo !== 'undefined' && typeof window.pendo.track === 'function';
}).then(function() {
// Pendo agent calls run here once Pendo is ready
});
Updating visitor metadata: pendo.identify({ visitor: { ... } }) updates the current Pendo visitor's metadata with the experiment context, keyed [Optimizely] ExperimentName. This makes the variation available as a Pendo segment rule and persists it on the visitor profile. The call is wrapped in a try/catch so an unexpected agent state never blocks the Track Event.
var visitorUpdate = {};
visitorUpdate[propertyKey] = variationValue;
window.pendo.identify({ visitor: visitorUpdate });
Sending the Track Event: pendo.track('Experiment Viewed', { ... }) records a discrete Track Event with full campaign metadata. This is the event that Pendo's Track Events page, Data Explorer, funnels, and paths index, enabling experiment analysis that starts from experiment entry.
window.pendo.track('Experiment Viewed', {
campaign_id: String(campaignId),
experiment_id: String(experimentId),
experiment_name: expName,
variation_id: String(variationId),
variation_name: variationValue,
is_holdback: isHoldback
});
Verifying the Integration
After enabling the integration, verify that data reaches Pendo before treating results as reliable.
Console Verification
Open your browser's developer console on a page with an active experiment:
// Check if the Pendo agent is loaded and Track Events are usable
console.log('Pendo loaded:', typeof window.pendo !== 'undefined');
console.log('pendo.track available:', typeof window.pendo === 'object'
&& typeof window.pendo.track === 'function');
// Check Optimizely experiment state
var state = window.optimizely && window.optimizely.get('state');
if (state) {
var campaigns = state.getCampaignStates({ isActive: true });
for (var id in campaigns) {
var c = campaigns[id];
console.log('Campaign ' + id + ':', {
experimentId: c.experiment && c.experiment.id,
variationId: c.variation && c.variation.id,
isHoldback: c.isInCampaignHoldback
});
}
}
// Manually fire a test Track Event to confirm the agent path is wired up
window.pendo.track('Integration Test', { source: 'console' });
If pendo.track is a function but no event ever appears in Pendo, the most common cause is that the Track Events feature is not enabled for your subscription (see Gotchas).
Pendo Data Explorer and Track Events Page
In the Pendo application, browse a page with an active experiment in a separate tab to generate a decision.
Open Product > Track Events. Within a few minutes (standard Pendo processing time), an Experiment Viewed Track Event should appear in the list.
Open the event's details page and review the Event breakdown table — confirm
experiment_name,variation_name, andcampaign_idcarry the expected values for your assigned variation.Optionally open the event View in Data Explorer to filter and group by
variation_name.
Pendo Visitor Profile
To verify the visitor metadata update:
Find the visitor you used during the verification run (search by
visitorIdin People > Visitors).Open the visitor's profile and look for a metadata field named
[Optimizely] Your Experiment Name.Confirm the value matches the variation you were assigned.
Note: Pendo distinguishes between agent (auto-collected) metadata and custom metadata. Depending on your subscription's metadata configuration, a custom visitor field may need to exist before values land on the profile and become usable in segments. If the value appears on the Track Event but not on the visitor profile, confirm the custom metadata field is configured in Pendo.
Analyzing Experiments in Pendo
Segments
Pendo Segments save a set of rules you can reuse across reports and guide targeting. Create one segment per variation:
Open the segment builder and add a rule.
Under Product Usage, select Track Events, choose Experiment Viewed, and add an event-property filter where
experiment_nameequals your experiment ANDvariation_nameequals "Variation A". Alternatively, build the rule on the[Optimizely] Your Experiment Namevisitor metadata field if it is configured.Save it as "Checkout Redesign — Variation A".
Repeat for the control.
Apply these segments across Data Explorer, funnels, paths, and retention to compare behavior between groups without rebuilding filters.
Funnels and Paths
Funnels segmented by variation are the most direct way to measure experiment impact on a conversion sequence:
Build a funnel (for example, Page View → Feature Click → Track Event → Conversion).
Apply your variation segment to split the funnel by variation.
Compare step-by-step conversion between control and treatment.
Paths show the sequence of pages and features a visitor moves through. Start a path from the Experiment Viewed Track Event filtered to your experiment and compare the routes taken by each variation segment — useful for multivariate tests and personalization campaigns where navigation impact matters as much as direct conversion.
Retention
Retention measures whether a variation produces a lasting change in engagement. Set the entry to the Experiment Viewed Track Event filtered by your experiment, set the return event to a key feature or page, apply your variation segments to the cohort breakdown, and compare the retention curves between control and treatment over your chosen window.
Targeting Guides by Variation
Pendo's distinct advantage over chart-only analytics tools is in-app guidance. Use a variation segment as the audience for a Pendo guide, poll, or NPS survey to message exactly the visitors who saw a given variation — for example, surveying the treatment group about a redesigned checkout while the experiment runs.
Note: Guide-eligibility segments cannot use event-property filters and are limited to "used" or "not used" Track Event rules. To target a guide at a specific variation, build the segment on the
[Optimizely] Your Experiment Namevisitor metadata field rather than on an event property.
Gotchas
pendo.track() Requires the Track Events Feature
pendo.track() only sends data if Pendo's Track Events feature is enabled for your subscription and your agent is web SDK 2.14.3 or later. If Track Events is not enabled, the call is effectively a no-op: it neither throws nor sends, and the "Experiment Viewed" event silently never appears. This is the single most common reason the integration looks correct but produces no data. Confirm Track Events is enabled with your Pendo administrator before relying on the integration, and verify the agent version in the console.
Visitor Identity Must Be Stable
Pendo attaches the Track Event and the metadata update to the current Pendo visitor. If a visitor is anonymous (Pendo assigned an anonymous ID because pendo.initialize() ran without a known visitor.id), the experiment data attaches to that anonymous record. When the same person is later identified with a real ID, Pendo does not retroactively merge the anonymous experiment data onto the known profile. Call pendo.initialize() with a stable visitor.id as early as possible so experiment data lands on the right visitor.
Custom Metadata Fields May Need Pre-Configuration
Track Event properties require no setup — they flow through automatically. Visitor metadata written via pendo.identify(), however, may need a corresponding custom metadata field configured in Pendo before it appears on the profile and becomes usable in segments. If variation_name shows up on the Track Event but the [Optimizely] field never appears on the visitor, configure the custom field in Pendo's metadata settings.
Mask Descriptive Names Hides Readable Values
If Mask descriptive names is enabled in Optimizely's privacy settings, Pendo receives 16-digit numeric IDs instead of experiment and variation names, making the Track Events and segments hard to read. Disable it — but note that doing so exposes your variation names in client-side source code.
16-Digit IDs Can Be Mistaken for Card Numbers
Optimizely's experiment and variation IDs are 16-digit numbers. Downstream redaction tooling can flag a bare 16-digit string as a credit card number and strip it. The callback sends IDs as strings via String(...) and always pairs them with a name; keep that pattern rather than sending raw 16-digit values.
Property Naming and Size Limits
Pendo enforces constraints on Track Event property names and payloads:
Property names: use only letters, numbers, and underscores; do not begin or end with double underscores; keep names under 32 characters; do not start with a number. Names that violate these rules do not display in Pendo. The property keys in this integration (
campaign_id,experiment_name, and so on) comply, but note that a visitor-metadata key like[Optimizely] ExperimentNameis for metadata, not event-property names.Property payload size: the entire
"properties": {}object cannot exceed 512 bytes, and an event-property map over 25KB is discarded and replaced with a system-generatederrorproperty. Avoid usingerroras a property name — Pendo reserves it.
Holdback Visitors
For experiments below 100% traffic, Optimizely makes a bucketing decision even for held-back visitors and sets isHoldback. This integration records them as variation_name: 'holdback' so you can analyze the holdback group. If you would rather exclude them, gate the Pendo calls on isHoldback === false.
Preview Mode Does Not Fire the Callback
Optimizely's custom integration code does not execute in Preview Mode. To verify the integration, browse the live experiment as a bucketed visitor — Preview will not generate an "Experiment Viewed" event.
Troubleshooting
No "Experiment Viewed" Events Appear in Pendo
Track Events not enabled: The most common cause. Confirm the Track Events feature is enabled for your subscription and the agent is web SDK 2.14.3 or later.
Integration not enabled: Confirm the integration is toggled on in Settings > Integrations and enabled (Tracked) for the specific experiment in Manage Experiments > Integrations.
Pendo agent blocked or not initialized: Ad blockers and privacy extensions can block
cdn.pendo.ioand Pendo data endpoints. Ifwindow.pendo.tracknever becomes a function,waitUntilwaits indefinitely. Check the network tab for blocked requests.Visitor not bucketed: The callback fires only on an active bucketing decision. If the visitor does not meet the experiment's audience conditions, no decision fires.
Tested in Preview Mode: Custom integration code does not run in Preview. Browse the live experiment instead.
Events Appear but Names Are Numeric
Mask descriptive names is enabled. Disable it in Optimizely Settings > Privacy, then generate a fresh decision — existing events are not retroactively renamed.
Visitor Metadata Not Showing on the Profile
The custom metadata field may not be configured in Pendo. Confirm the
[Optimizely]field exists in Pendo's metadata settings.The visitor may be anonymous.
pendo.identify()updates the current visitor; if that visitor was never identified with a stable ID, the data will not merge onto a later known profile.
Track Event Properties Missing or Dropped
Naming: A property name that starts with a number, exceeds 32 characters, or uses unsupported characters will not display. Verify your property keys.
Size: A
propertiesobject over 512 bytes triggers anevent properties JSON too largestate, and a map over 25KB is replaced with anerrorproperty. Keep the payload small.
Data Discrepancies Between Platforms
Differences between Optimizely visitor counts and Pendo counts are expected:
Counting unit: Optimizely counts unique cookie-based visitors; Pendo counts by its own visitor/account identity model.
Identity resolution: Anonymous-to-known merges in Pendo, and unstable
visitorIdvalues, shift counts relative to Optimizely decisions.Ad blockers: Extensions may block Pendo or Optimizely independently, skewing counts in either direction.
Processing delay: Track Events appear after standard Pendo processing (typically minutes), and events with timestamps older than seven days are not processed on the normal schedule.
Expect a 5–15% discrepancy between Optimizely visitor counts and Pendo Track Event counts for the same experiment. Investigate further if the gap exceeds 20%.