> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Personalize website content: slots, segments, and the web SDK

> Define content slots per CDP segment, render them on your site with the web SDK, and measure the lift against a holdout — end to end.

# 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](/guides/cdp-segments) 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.

<RequestExample>
  ```bash theme={null}
  curl -X POST "https://api.orbit.devotel.io/api/v1/personalization/slots" \
    -H "Authorization: Bearer $SESSION_JWT" \
    -H "Content-Type: application/json" \
    -d '{
      "slot": "hero-banner",
      "segment_label": "engaged",
      "content": "<h2>Welcome back, see what is new</h2><p>Fresh releases picked for you.</p>",
      "cta_url": "https://shop.example.com/new",
      "cta_text": "Browse new arrivals",
      "variant": "a",
      "priority": 10
    }'
  ```
</RequestExample>

**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`:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/personalization/slots" \
  -H "Authorization: Bearer $SESSION_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "slot": "hero-banner",
    "content": "<h2>Welcome</h2><p>Pick up where you left off.</p>"
  }'
```

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](/api-reference/personalization).

## 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`:

```html theme={null}
<div id="hero-banner"></div>
<script type="module">
  import { OrbitPersonalization } from '@devotel-orbit/web';

  const personalization = new OrbitPersonalization({
    publicKey: 'dv_live_pk_your_public_key_here',
    apiUrl: 'https://api.orbit.devotel.io',
  });

  // Identify the visitor (when known) so their segment resolves; the
  // SDK uses a sticky anonymous id until you call identify.
  await personalization.identify({ email: 'user@example.com' });

  // Render the slot into the placeholder div. `{ html: true }` is the
  // opt-in that lets the server-sanitized markup be injected as HTML;
  // without it the content is set as plain text.
  await personalization.render('hero-banner', '#hero-banner', { html: true });
</script>
```

`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](/sdks/web) 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:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/personalization/slots/hero-banner/preview?segment_label=at_risk" \
  -H "Authorization: Bearer $SESSION_JWT"
```

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:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/personalization/slots/hero-banner/lift?variant_a=__default__&variant_b=b&conversion_event=clicked_cta&since_days=14" \
  -H "Authorization: Bearer $SESSION_JWT"
```

* `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](/guides/campaign-ab-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.**

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/personalization/slots" \
  -H "Authorization: Bearer $SESSION_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "slot": "hero-banner",
    "segment_label": "at_risk",
    "content": "<h2>We kept your cart warm</h2><p>Finish checkout today and take 15% off.</p>",
    "cta_url": "https://shop.example.com/cart",
    "cta_text": "Resume checkout",
    "variant": "b"
  }'
```

**2. Create the fallback default.**

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/personalization/slots" \
  -H "Authorization: Bearer $SESSION_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "slot": "hero-banner",
    "content": "<h2>Welcome</h2><p>Pick up where you left off.</p>"
  }'
```

**3. Preview the at-risk experience.**

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/personalization/slots/hero-banner/preview?segment_label=at_risk" \
  -H "Authorization: Bearer $SESSION_JWT"
```

**4. Render on the site** with the snippet in step 4, then fire the conversion event when a visitor clicks the slot's CTA:

```ts theme={null}
document.querySelector('#hero-banner').addEventListener('click', () => {
  personalization.track({ name: 'clicked_cta' });
});
```

**5. After a week, check the lift:**

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/personalization/slots/hero-banner/lift?variant_a=__default__&variant_b=b&conversion_event=clicked_cta&segment_label=at_risk&since_days=7" \
  -H "Authorization: Bearer $SESSION_JWT"
```

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

* [Personalization API reference](/api-reference/personalization) — full slot CRUD contract
* [CDP segments](/guides/cdp-segments) — where segment labels come from
* [Web SDK](/sdks/web) — `OrbitPersonalization` module reference
* [Campaign A/B testing](/guides/campaign-ab-testing) — the same experiment discipline for messages
