JavaScript API (window.ply)
The Personizely JavaScript API is the window.ply object that the Personizely snippet adds to every page it runs on. You use it to identify visitors, track custom events and goals, pass your own data to targeting, show widgets on demand and run full-stack experiments from your own code. This page lists every public method, the DOM events Personizely dispatches, and the URL parameters for debugging and previews.
Available on: every platform where the Personizely snippet is installed (Shopify, Shopify Hydrogen, Wix, WordPress, Webflow, Square and any website using the script tag).
How it works
- The snippet defines window.ply when it loads. Code that runs before that can't call ply directly. Use the plyLayer queue instead.
- Tracking calls are queued until the visitor is ready. On a visitor's first page view, Personizely creates the visitor profile first. Events, goals and identify calls made in the meantime are held and sent once the profile exists, so you don't have to wait for anything before calling trackEvent or trackGoal.
- Data is attributed server-side. Events and goals you send are stored against the current visitor and session, then credited to the campaigns and widgets that visitor saw, using your attribution window.
Method | Use it to | Returns |
|---|---|---|
Save the visitor's email, name, consents or custom fields | Promise | |
Record a custom event, which can complete Event goals | undefined | |
Record a conversion for a Manual goal | undefined | |
Pass your own values to the Data targeting filter and to dynamic text | undefined | |
Show a widget from your own code | Promise | |
Run a full-stack campaign and read the visitor's variation | Variation or undefined |
Calling the API before the snippet loads
If your code may run before the snippet, push calls onto window.plyLayer. Each entry is an array whose first item is the method name and whose remaining items are its arguments.
<script>
window.plyLayer = window.plyLayer || [];
window.plyLayer.push(['identifyVisitor', { email: '[email protected]' }]);
window.plyLayer.push(['setData', 'loyaltyTier', 'gold']);
window.plyLayer.push(['trackEvent', 'newsletter_signup']);
</script>When the snippet loads, it runs every queued call in order. After that, anything you push to plyLayer runs immediately, so the same code works whether the snippet has loaded or not. Every method can be queued this way, including trackGoal, showWidget and runCampaign.
Calls pushed to plyLayer don't return a value to your code. When you need a return value, such as the variation from runCampaign, call window.ply directly after the page loads, for example from a load event listener.
Methods
ply.identifyVisitor(data)
Saves profile fields for the current visitor. Personizely merges them into the visitor's profile, which is used by the Visitor fields filter, by dynamic text and by integrations. If the email or phone matches an existing visitor, the two profiles are merged. See Identifying visitors.
Parameter | Type | Description |
|---|---|---|
data | object | The fields to save. Unknown fields are ignored. |
Supported fields:
Field | Type | Notes |
|---|---|---|
string | Must be a valid email address | |
phone | string | Numbers without a country code are converted to international format using the visitor's country |
firstName, lastName | string | |
companyName, companyTitle | string | |
address, bio | string | |
birthdate | string | YYYY-MM-DD, between 1900-01-01 and today |
marketingConsent, emailMarketingConsent, smsMarketingConsent, privacyConsent | boolean | |
customFieldValues | object | Keys are custom field IDs, values are the field values. Merged with the visitor's existing custom field values. |
Returns: a Promise that resolves once the request completes.
Throws: Invalid payload provided if data isn't a plain object (for example an array, a string or null).
ply.identifyVisitor({
email: '[email protected]',
firstName: 'Jane',
emailMarketingConsent: true,
customFieldValues: { '42': 'wholesale' }
}).then(() => {
console.log('Visitor identified');
});ply.trackEvent(event, value?)
Records a custom event for the current visitor. Every Event goal whose event name equals event and whose page setting matches the current page is completed at the same time. Event goals have no count limit, so every call counts. Events also feed the Event history targeting filter.
Parameter | Type | Description |
|---|---|---|
event | string | The event name. See the format rule below. |
value | number, optional | A monetary value in your store currency, in whole units (29.99, not 2999). Default 0. If you omit it and the event completes Event goals, the goals' default values are used. |
Event name format. The name must be lowercase letters, digits and underscores, at least 3 characters long, start with a letter and end with a letter or digit. Anything else throws an error.
Name | Valid? |
|---|---|
add_to_wishlist | Yes |
quiz_step2 | Yes |
Add to cart | No: uppercase and spaces |
add-to-cart | No: hyphens |
signup_ | No: ends with an underscore |
ab | No: shorter than 3 characters |
Returns: undefined.
Throws: Invalid event provided if event is empty or not a string, Invalid event format if the name breaks the format rule, Invalid value provided if value is set and isn't a number.
document.querySelector('#size-guide').addEventListener('click', () => {
ply.trackEvent('size_guide_open');
});
// With a value: an upsell accepted for $14.50
ply.trackEvent('upsell_accepted', 14.5);ply.trackGoal(nameOrId, value?)
Records a conversion for a goal whose action is Manual. Use it for conversions that only your code can detect, such as a booking confirmed by a third-party widget. Goals with other actions (Page view, Click, Event and so on) can't be triggered with trackGoal; for those, use trackEvent with an Event goal.
Parameter | Type | Description |
|---|---|---|
nameOrId | string or number | The goal's ID (a number) or its exact name (a string, case-sensitive) |
value | number, optional | A monetary value in whole units of your store currency. If omitted or 0, the goal's default value is used. |
The goal's page setting isn't checked; the conversion is recorded wherever you call it. Manual goals have no count limit, so every call records a goal hit.
Returns: undefined.
Throws: Invalid goal provided. Please provide a goal id or name. if nameOrId is missing or isn't a string or number, Invalid value provided if value isn't a number, and Could not find the goal with the name or id … if no Manual goal matches.
// By name
ply.trackGoal('Demo booked');
// By ID, with a value
ply.trackGoal(1873, 120);Pass a goal ID as a number. ply.trackGoal('1873') looks for a goal named "1873".
ply.setData(key, value)
Stores a value in the visitor's website data for the current browser session. Website data is what the Data targeting filter checks, and you can print it in widgets and campaigns with {data.key} dynamic text. See Website data.
Parameter | Type | Description |
|---|---|---|
key | string, or object | A key name, or an object of several key/value pairs to set at once |
value | any | The value to store. Omit it when key is an object. |
Returns: undefined.
ply.setData('loyaltyTier', 'gold');
ply.setData({ loyaltyTier: 'gold', pointsBalance: 1250 });You can also define data before the snippet loads with a global object, which is read once when the snippet starts:
<script>
window.plyData = { loyaltyTier: 'gold', pointsBalance: 1250 };
</script>- Data lasts for the browser session and is shared across tabs. It isn't saved to the visitor's profile; use identifyVisitor for that.
- Only top-level keys are matched by the Data filter.
- Changing window.plyData after the snippet has started has no effect. Use setData.
- Targets that were already evaluated on the page aren't re-checked when you call setData. Widget audiences are re-checked when a widget's trigger fires, and every target is re-checked on the next page view.
ply.showWidget(id, suppressRules?, step?)
Shows a widget from your own code, for example when a visitor clicks a custom button. The widget must be active.
Returns: a Promise that resolves to a truthy value when the widget is shown, or false when it isn't: the widget wasn't found, its rules didn't pass, or it's already visible.
document.querySelector('#open-quiz').addEventListener('click', () => {
ply.showWidget(5521, true).then((shown) => {
if (!shown) window.location.href = '/pages/quiz';
});
});- A view is recorded as with any other trigger, and the widget counts toward its Frequency rule.
- If the widget is in a widget A/B test and the visitor is in the control group, the view is recorded but nothing is displayed.
- If the ID doesn't match a widget, or the rules don't pass, the reason is logged to the browser console.
ply.runCampaign(id)
Runs a full-stack campaign and returns the variation the visitor is assigned to. Full-stack campaigns never run on their own; your code decides where and when they apply. The visitor's assignment is sticky, so calling runCampaign again on this page or a later one returns the same variation.
Parameter | Type | Description |
|---|---|---|
id | number | The campaign ID |
Returns: the visitor's variation, or undefined when the visitor is in the control group, outside the campaign's traffic allocation, or not matched by its target. undefined also means the campaign wasn't found or isn't active. Treat undefined as "show the original experience".
The returned variation has:
Member | Description |
|---|---|
id | The variation ID |
name | The variation name |
getFeature(key) | Returns the feature with that key. A feature that isn't part of the variation is returned disabled. |
Each feature has:
Method | Description |
|---|---|
isEnabled() | true if the variation turns the feature on |
getParameterValue(key) | The value the variation sets for that parameter, or undefined |
getId() | The feature key |
const variation = ply.runCampaign(3107);
const checkout = variation ? variation.getFeature('express_checkout') : null;
if (checkout && checkout.isEnabled()) {
renderExpressCheckout({
buttonColor: checkout.getParameterValue('button_color') || '#111111'
});
} else {
renderStandardCheckout();
}- The campaign view is recorded the first time you call runCampaign on a page, for both variations and control. Calling it again on the same page doesn't add views.
- runCampaign is synchronous. It's built for full-stack campaigns; for other campaign types, Personizely runs them automatically and the return value isn't meant to be used.
- To decide full-stack campaigns on your server instead, see the server-side SDK.
IDs are compared strictly. ply.runCampaign('3107') and ply.showWidget('5521') don't find anything; pass numbers.
DOM events
Personizely dispatches events you can listen to, for example to forward widget activity to your analytics or to run your own code when a campaign applies.
Widget and campaign events
These are dispatched on document as CustomEvents named personizely:<entity>:<type>. event.detail.id is the widget or campaign ID. Each event fires after Personizely has recorded the interaction, so it isn't dispatched in preview mode or for traffic excluded by your traffic filters.
Event | Fires when |
|---|---|
personizely:widget:view | A widget is shown. Also fires for visitors in a widget A/B test's control group, who don't see anything. |
personizely:widget:click | A visitor clicks a button, a linked image or a product in a widget. Buttons whose only action is to close the widget don't count. |
personizely:widget:submit | A form in a widget is submitted |
personizely:widget:spin | A wheel of fortune is spun |
personizely:widget:coupon_copy | A visitor copies a coupon code |
personizely:widget:coupon_apply | A coupon is applied to the cart (Shopify) |
personizely:widget:succeed | A countdown timer reaches zero, or a cart progress bar reaches its goal |
personizely:campaign:view | A visitor is counted in a campaign, as a variation or as control |
If you use a white-label brand, the prefix is your brand name in lowercase instead of personizely.
document.addEventListener('personizely:widget:submit', (event) => {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ event: 'ply_widget_submit', widget_id: event.detail.id });
});Exit intent
ply:exit is dispatched on window when a touch-device exit-intent signal is detected. event.detail.type is history (Back button), focus (tab or app switch) or scroll (quick scroll up). It's only dispatched on pages where a widget uses the exit-intent trigger with that fallback turned on.
Cart events (Shopify)
When a widget adds a product to the cart, Personizely dispatches the cart events common Shopify themes listen for, so their cart drawers and counters update: app:product:added, app:cart:updated, product:added, cartcount:update, cart:refresh and ajaxProduct:added. If your theme uses a different event to refresh its cart, listen for app:product:added and refresh it yourself.
URL parameters for debugging and preview
Parameter | Example | What it does |
|---|---|---|
ply_debug | ?ply_debug=1 | Logs what the snippet does to the browser console: which campaigns and widgets ran or didn't, and why. |
ply_disable | ?ply_disable=1 | Stops the snippet from running on that page view, so you see your site without any Personizely changes. |
ply_preview_campaign | ?ply_preview_campaign=3107.9921 | Previews one variation of a campaign. The value is <campaignId>.<variationId>. |
Each parameter needs a value: ?ply_debug on its own does nothing.
- ply_debug applies to the page you load. To keep debug logging on while you click through the site, set a cookie in the console: document.cookie = 'ply_debug=1; path=/'. Delete it when you're done. Without debug mode, you can still print the logs collected so far by running plyUnrollLogs() in the console.
- ply_disable applies only to that page view. The browser console shows Personizely will not run because it is explicitly disabled.
- ply_preview_campaign shows the saved version of the variation, ignoring the campaign's targeting, traffic allocation and schedule, and records nothing, so previews don't affect your results. Links you click keep the parameter, so you can browse the site in preview. The easiest way to get a preview link is the preview button in the campaign editor. See Preview and QA.
When the snippet starts normally, the console shows Personizely initialized. Startup time: …ms. See Verify and troubleshoot your installation.
Behavior details and limitations
- IDs are numbers. Widget, campaign and goal IDs are compared strictly. Passing them as strings fails silently for showWidget and runCampaign, and looks up a goal by name for trackGoal.
- Values are in whole currency units. trackEvent('x', 29.99) records $29.99 (in your website's currency), not 29.99 cents.
- trackEvent and trackGoal throw on invalid input. Wrap calls in try/catch if the input comes from user data, so an error doesn't stop the rest of your script.
- Event goals check the page, Manual goals don't. An Event goal limited to /checkout won't complete from a trackEvent call on /cart. A Manual goal is completed wherever you call trackGoal.
- Only Manual goals work with trackGoal. Calling it with the name of a Click or Page view goal throws "Could not find the goal".
- Consent. On Shopify, API calls carry the visitor's consent status from Shopify's Customer Privacy API, like all other Personizely tracking. See Cookies and consent.
- Single-page apps. On sites that change pages without a full reload, window.ply stays the same across navigations. Goal count limits per page reset on each detected page change.
- Paused tracking. When tracking is paused or your visitor quota is used up, the snippet doesn't run and window.ply may not be defined. Guard calls with window.ply && or use plyLayer. See Plans, visitor quota and pausing.
Examples
Identify logged-in customers on a custom site. Render the customer's email into the page and queue an identify call. Personizely can then target them by profile fields and send them to your email integrations.
<script>
window.plyLayer = window.plyLayer || [];
window.plyLayer.push(['identifyVisitor', { email: '{{ customer.email }}', firstName: '{{ customer.first_name }}' }]);
</script>Count a third-party booking as a conversion. Create a goal with action Manual named "Demo booked", then call it from the booking tool's success callback:
bookingWidget.on('confirmed', (booking) => {
if (window.ply) ply.trackGoal('Demo booked', booking.amount);
});Target by loyalty tier. Push the tier into website data, then build a target with the Data filter loyaltyTier is gold and use it on a personalization or widget:
window.plyLayer = window.plyLayer || [];
window.plyLayer.push(['setData', 'loyaltyTier', customer.tier]);Troubleshooting
- Invalid event format in the console → the event name has uppercase letters, spaces, hyphens or is shorter than 3 characters. Use a name like add_to_wishlist.
- Could not find the goal with the name or id … → the goal isn't a Manual goal, the name doesn't match exactly (it's case-sensitive), or you passed an ID as a string.
- ply is not defined → your code runs before the snippet loads, or the snippet isn't running on this page view. Use plyLayer, and check the console with ?ply_debug=1.
- showWidget resolves to false → the widget's rules don't pass on this page or it's already open. Pass true as the second argument to ignore rules, and check the console for the reason.
- runCampaign always returns undefined → you're in the control group, outside the traffic allocation or not in the target, or the campaign isn't active. Preview a variation with ply_preview_campaign to test the variation code path.
- Event goal not counting → the goal's event name doesn't exactly match the name you pass, or the goal's page setting doesn't match the page where you call trackEvent.