Personalize website content
On-site personalization has three moving parts and this guide connects them: you define slot variants in the dashboard or via the API, the web SDK renders the right variant per visitor, and the lift report tells you whether the personalized copy actually converts better than the default. Work through all three and you have a closed loop instead of a guessed-at hero banner.1. What a slot is
A slot is a named placement on your site — a<div> the web SDK fills in, such as hero-banner, cart-empty, or checkout-promo. For each slot you store one or more variants keyed on:
slot— the placement name (lowercase letters, digits, hyphen, underscore),segment_label— the CDP segment the variant targets, andvariant— a name for A/B testing within that segment (optional).
(slot, segment_label, variant) combination. A duplicate create returns 409 Conflict, so edit the existing variant or deactivate it first. Keep a default variant — one with no segment_label — as the fallback every visitor sees when no segment-specific variant matches.
2. Segment labels and where they come from
Variants target the lifecycle segment Orbit’s CDP assigns to each contact. The fixed label set:champion— highest engagement and valuehigh_value_engaged— active, high-LTV contactsengaged— recent activity, normal valuenew— recently acquired, not yet classifiedpassive— low recent activityat_risk— engagement dropping, churn risk risingdormant— no recent activitylost— lapsed/churned
engaged to at_risk starts seeing the at-risk variant without any content edit on your side. See the CDP segments guide for how segments and lifecycle facts are built; the labels above are the same vocabulary the personalization API accepts as segment_label.
3. Create your first slot variant
Slot management endpoints require a session JWT from a dashboard user with the owner or admin role. You can also create and edit variants in the dashboard under the personalization screen, where no API call is needed.content into the page when you opt in with { html: true }, so the API rejects unsafe markup up front with a 422: no <script>, <iframe>, <object>, <embed>, <style>, <link>, <meta>, or <base> tags, no inline event handlers (onclick=…), no javascript: URLs, and data: URLs only for images. Text formatting, headings, lists, links, images, and tables pass. Fix the rejected construct rather than working around the check — the markup is rendered on every visitor’s browser.
Fallback. Create the default variant by omitting segment_label:
PATCH /api/v1/personalization/slots/{id} (send only the fields to change), pause it with { "active": false }, or remove it with DELETE. The full request/response contract is in the Personalization API reference.
4. Render on the site
Rendering uses the public key (dv_live_pk_*) — never a secret key in browser code. The personalization module lives on OrbitPersonalization:
render() resolves the visitor’s segment, fetches the winning variant from the runtime endpoint GET /sdk/personalize, injects the content, stamps data-orbit-slot / data-orbit-segment / data-orbit-variant attributes on the element, and fires a personalization_rendered event — the impression data the lift report counts on. If the fetch fails or times out (5-second ceiling), the element keeps its existing markup, so the page degrades gracefully. See the Web SDK reference for the module’s full surface.
5. Preview before launch
Resolve a slot exactly as the runtime endpoint would, without activating anything on a live page:data.resolved — the variant that would win for that segment. null means no active variant matches, so the SDK would leave your page markup untouched. Omit segment_label to check the default content.
6. Prove lift against the holdout
Everyrender() call logs a personalization_rendered event with the slot, segment, and variant. Every track() call — a CTA click, a purchase, any event name — is a candidate conversion. The lift endpoint joins the two, so no extra instrumentation is needed:
variant_a— the baseline. Pass__default__to compare against the segment’s untagged default content, the closest thing this feature has to a holdout.variant_b— the treatment under test. Must differ fromvariant_a, otherwise the request is rejected with422.conversion_event— the event name that counts as a conversion.segment_label(optional) — restrict the report to one segment.since_days(optional) — lookback window from 1 to 365 days (default 30).
is_significant flag. state is no_traffic until either arm has been served and collecting while only one has traffic — the statistical fields stay null until both arms have data. Read significance before declaring a winner; a week of traffic on a busy page usually clears collecting.
Read the guide on campaign A/B testing if you run message-level tests too — the discipline (holdout, one variable, significance threshold) is the same here.
7. End-to-end example
Goal: re-engageat_risk visitors on the homepage hero while engaged visitors get the new-arrivals copy.
1. Create the treatment variant.
is_significant is true and the treatment wins, keep it — or promote variant b into the default so every segment gets the winner. If the default wins, deactivate variant_b with PATCH /api/v1/personalization/slots/{id} ({ "active": false }) and the fallback resumes.
See also
- Personalization API reference — full slot CRUD contract
- CDP segments — where segment labels come from
- Web SDK —
OrbitPersonalizationmodule reference - Campaign A/B testing — the same experiment discipline for messages