Theme
Audience Segments + Story Variants
Personalize what a Story renders based on visitor signals (country, language, referrer, custom cookies, etc.) without forking the URL.
Conceptual model
text
┌─ Space ────────────────────────────────────────────────────┐
│ ┌─ AudienceSegment "EU-visitors" ──────────────────────┐ │
│ │ rules: [country in {DE, AT, CH, FR}, ...] │ │
│ │ priority: 10 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌─ AudienceSegment "free-trial-users" ─────────────────┐ │
│ │ rules: [cookie peacock_plan == "free"] │ │
│ │ priority: 20 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌─ Story "/pricing" ───────────────────────────────────┐ │
│ │ default content: ... │ │
│ │ variants: │ │
│ │ └─ Variant for segment "EU-visitors" (EUR prices) │ │
│ │ └─ Variant for segment "free-trial-users" (CTA) │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘The CDN matches segments left-to-right by priority, picks the highest-priority match, then serves the variant content (or the Story default if no variant exists for that segment).
Segments
http
GET /v1/spaces/{slug}/audience-segments
POST /v1/spaces/{slug}/audience-segments
PATCH /v1/spaces/{slug}/audience-segments/{id}
DELETE /v1/spaces/{slug}/audience-segments/{id}POST body:
json
{
"key": "eu-visitors",
"label": "EU Visitors",
"priority": 10,
"rules": [
{ "type": "country", "operator": "in", "value": ["DE", "AT", "CH", "FR"] }
]
}Rule types: country, language, referrer, cookie, query, utm_source, utm_campaign. Operators: eq, neq, in, not_in, contains, starts_with, regex.
Country detection
country rules need a CF-IPCountry header set by Cloudflare or a trusted edge. Without a trusted-edge marker, country rules are advisory — they match against the request header but downstream code can't trust it (security audit fix, 2026-05-21).
Variants
http
GET /v1/spaces/{slug}/stories/{uuid}/variants
POST /v1/spaces/{slug}/stories/{uuid}/variants
PATCH /v1/spaces/{slug}/stories/{uuid}/variants/{id}
DELETE /v1/spaces/{slug}/stories/{uuid}/variants/{id}POST body:
json
{
"audience_segment_id": 4,
"content": { "type": "doc", "blocks": [...] }
}The content shape is identical to a normal Story version — the variant fully overrides the default.
CDN resolution
On every CDN read, the controller:
- Reads request signals (Accept-Language, CF-IPCountry, cookies, query).
- Iterates active segments in
priorityorder. - Picks the first segment whose rules ALL match.
- If a Variant exists for that segment on this Story, serves it.
- Otherwise serves the Story default.
Response carries meta.served_segment (key) so the frontend can display "We're showing you the X variant" if desired.
Edge caches must Vary: X-Peacock-Segment — the SDK auto-sets this header on subsequent requests from the resolved segment so cached variants don't shadow each other.
See also
/guide/personalisation— conceptual guide/api/cdn— the CDN read flow that does the matching/api/datasources— for content that varies by data, not audience