Skip to main content

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, and
  • variant — a name for A/B testing within that segment (optional).
Only one active variant may exist per (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 value
  • high_value_engaged — active, high-LTV contacts
  • engaged — recent activity, normal value
  • new — recently acquired, not yet classified
  • passive — low recent activity
  • at_risk — engagement dropping, churn risk rising
  • dormant — no recent activity
  • lost — lapsed/churned
The label lives on the contact’s score record and updates as their engagement shifts, so a visitor who drifts from 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.
Markup rules. The SDK injects 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:
Update a variant with 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:
The response echoes the slot and requested segment and carries 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

Every render() 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 from variant_a, otherwise the request is rejected with 422.
  • 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).
The report returns each arm’s impressions, conversions, and conversion rate, plus absolute and relative lift, a 95% confidence interval, a z-score, a p-value, and an 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-engage at_risk visitors on the homepage hero while engaged visitors get the new-arrivals copy. 1. Create the treatment variant.
2. Create the fallback default.
3. Preview the at-risk experience.
4. Render on the site with the snippet in step 4, then fire the conversion event when a visitor clicks the slot’s CTA:
5. After a week, check the lift:
If 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