Server-side SDK (@personizely/sdk) – Preview
Preview, contact support to enable. The server-side SDK is in Preview. It isn't enabled by default; contact support to turn it on for your website. The API may change before general release.
@personizely/sdk is a JavaScript and TypeScript library for running Personizely Full-stack experiments and feature flags outside the storefront snippet: on your Node server, at the edge, or in a browser app. It downloads your website's campaign configuration once and decides each visitor's variation locally, with no network call per decision. Impressions, events and goals are sent to Personizely and show up in the same reports as campaigns run by the snippet.
Runs in: Node.js, edge runtimes and browsers. Zero dependencies, ESM and CommonJS builds, TypeScript types included.
How it works
- You create the campaign in Personizely as a full-stack experiment or personalization, with Full-stack experiments and feature flags and parameters per variation.
- The SDK loads your website's configuration when it starts, and can refresh it on an interval so changes you publish are picked up.
- You run a campaign for a visitor. The SDK checks the campaign's target, applies the traffic allocation and assigns a variation. Assignments are sticky: the same visitor gets the same variation on every call.
- The SDK records the impression the first time a visitor is counted in a campaign, as a variation or as control.
- You track conversions with trackEvent or trackGoal. They're attributed to the visitor's campaigns like any other conversion.
Scope: the SDK decides full-stack campaigns only. Content, price, theme, template, shipping and split URL campaigns change the page or the checkout, so the How Personizely works runs them. For those, runCampaign returns null.
Install and initialize
npm install @personizely/sdkimport { Personizely } from '@personizely/sdk'
const ply = new Personizely({ apiKey: 'YOUR_WEBSITE_KEY' })
await ply.ready()The apiKey here is your website's key: the name of the JavaScript snippet file shown in the dashboard when you select Manual integration, without the .js extension. It isn't a REST API key (ply_live_…), and it's safe to use in browser code.
Option | What it does | Default |
|---|---|---|
apiKey | Your website's public key | Required |
visitor | The default visitor: { id, attributes, context } | In the browser, the visitor ID used by the Personizely snippet |
pollInterval | Refresh the configuration every N milliseconds | Off |
bucketing | How assignments are kept sticky (see Bucketing) | hashBucketing() |
onAudienceUnresolved | What to do when a target uses data the SDK can't evaluate: 'exclude' keeps the visitor out, 'include' lets them in | 'exclude' |
consent | false sends tracking without consent, as the snippet does for visitors who declined | Consent given |
datafile | Pass the configuration yourself instead of downloading it, for tests or pre-rendering | Downloaded |
fetch | A custom fetch implementation | Global fetch |
onError | Called with network and refresh errors | None |
API overview
Client methods
Method | Returns | What it does |
|---|---|---|
ready() | Promise<void> | Resolves when the configuration is loaded |
setVisitor(id, attributes?, context?) | The client | Sets the visitor for later calls (one visitor at a time: browser or single-user scripts) |
forVisitor(id, attributes?, context?) | A visitor scope | Creates a per-request scope with the same methods, for servers handling many visitors at once |
refresh() | Promise<void> | Reloads the configuration now |
destroy() | – | Stops automatic refreshing |
Visitor methods
Available on the client (after setVisitor) and on every scope from forVisitor.
Method | Returns | What it does |
|---|---|---|
runCampaign(idOrName) | Promise<CampaignResult | null> | Decides one full-stack campaign. null if the campaign isn't found, isn't full-stack, or the visitor isn't in its target or traffic allocation. |
runAllCampaigns() | Promise<CampaignResultSet> | Decides every full-stack campaign at once |
trackEvent(name, value?) | Promise<void> | Records a custom event; completes matching Event goals |
trackGoal(nameOrId, value?) | Promise<void> | Records a conversion for a goal, by ID or exact name. Throws if no goal matches. |
identifyVisitor(traits) | Promise<void> | Saves profile fields, as ply.identifyVisitor does |
setData(key, value) | The scope | Sets a value used by the Data targeting filter |
flush() | Promise<void> | Waits for all pending tracking requests. Call it before a short-lived process, such as a serverless function, exits. |
Use the same event name format as the JavaScript API (lowercase letters, digits and underscores, at least 3 characters), so the same Event goals and filters work in both. Values are monetary amounts in whole units of your store currency (29.99, not 2999).
Results
A CampaignResult has campaignId, campaignName, variationId, variationName, control (true for the control group) and getFeature(key). A feature has enabled and get(key, fallback) for parameter values. A feature that isn't part of the assigned variation is returned with enabled: false, so you can check flags without null checks.
A CampaignResultSet from runAllCampaigns() has get(campaignId), has(campaignId), getFeature(key) (searches all results) and can be iterated.
Example
import { Personizely } from '@personizely/sdk'
const ply = new Personizely({ apiKey: process.env.PLY_WEBSITE_KEY, pollInterval: 60_000 })
await ply.ready()
app.get('/checkout', async (req, res) => {
const visitor = ply.forVisitor(req.cookies._ply, { plan: req.user?.plan }, {
url: req.originalUrl,
userAgent: req.headers['user-agent'],
ip: req.ip
})
const result = await visitor.runCampaign(3107)
const flag = result?.getFeature('express_checkout')
res.render('checkout', {
expressCheckout: flag?.enabled ?? false,
buttonColor: flag?.get('button_color', '#111111')
})
})
app.post('/orders', async (req, res) => {
const visitor = ply.forVisitor(req.cookies._ply)
await visitor.trackGoal('Order placed', req.body.total)
res.sendStatus(201)
})context describes the request so targets can be evaluated: url, referrer, userAgent, utm, locale, ip, cart and cookies. Everything is optional. In the browser, the SDK fills it in from the page.
Bucketing
By default, the SDK assigns variations with a deterministic hash of the visitor ID and campaign ID, so assignments are sticky across calls, processes and servers without storing anything. Other options:
Adapter | Behavior |
|---|---|
hashBucketing(salt?) | Default. Deterministic; change the salt to reshuffle assignments. |
cookieBucketing() | Browser only. Random assignment stored in a cookie. |
memoryBucketing() | Random assignment kept in memory, for tests. |
Custom BucketingAdapter | Store assignments yourself, for example in Redis, with read and write methods |
Behavior details and limitations
- Decide each campaign in one place. The SDK and the storefront snippet assign variations independently. If both run the same campaign for a visitor, they can assign different variations. Run a given full-stack campaign either with ply.runCampaign in the browser or with the SDK, not both.
- Use the same visitor ID as the snippet if you want server-side conversions and storefront activity to belong to one visitor. In the browser, the SDK uses the snippet's visitor ID automatically. On the server, read it from the _ply cookie, as in the example.
- Some audience filters need the visitor's history. Targets that use data only Personizely's servers have, such as visit history or location, are looked up when you provide a visitor ID and context. If they can't be evaluated, onAudienceUnresolved decides the outcome.
- Configuration changes aren't instant. Without pollInterval or refresh(), a running process keeps using the configuration it loaded at start.
- Preview. Method names and options may change before general release. Pin the package version.