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

# Klaviyo

> Profiles, lists, events, derived segments and campaign sends, in Klaviyo's JSON:API

A Klaviyo account over the API a lifecycle marketing integration actually uses:
profiles with real upsert semantics, lists and membership as relationships, consent
per channel with the bulk jobs that move it, event ingestion that creates its metric
and its profile on first use, segments whose membership is computed from their
definition, and campaigns that queue a send job and go out when the sandbox is
advanced.

Klaviyo has **no sandbox at all** — no test mode, no mock server, and no way to try
a campaign send without a real account and real profiles. Anyone building an agent
that touches Klaviyo today is testing against production or not testing.

**Slug:** `klaviyo`

## Segment membership is derived, never stored

This is the behaviour the twin exists for. What agents do with Klaviyo more than
anything else is write a segment definition and then check who is in it, and a
segment whose membership were frozen at creation would answer that with yesterday's
answer forever — while the caller believed its segment logic worked.

So `GET /api/segments/{id}/profiles` evaluates the definition against the profiles
the account holds *now*, every time it is read. Change a profile so it no longer
qualifies, read again, and it is gone. A `set_segment_lag` directive makes
membership lag by a number of advances, because Klaviyo's own is eventually
consistent in production and a twin that is instantly exact teaches the wrong thing.

## The SDK version *is* the API revision

Klaviyo versions its API by a dated `revision` header on every request — never a
path segment, never optional — and it generates its client per revision, pinning the
header inside the release. So the revision this image serves was not chosen: it is
`KlaviyoAPI._REVISION` out of the pinned `klaviyo-api` 24.0.0, which is
`2026-07-15`, and Klaviyo's published specification reports the same string. A
missing revision is refused rather than quietly served, because this image serves
exactly one and a fallback would be a claim it cannot honour.

## What it models

| Area               | Modelled                                                                                                                                                                                                                                                                                                                                                                                              |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The account        | One account per container with constant sender details and a constant public API key, `test_account: true`, three read-only flows, two lists, four profiles across three consent states, a derived segment, a template and a campaign already sent — so the first call a caller makes finds a furnished account rather than an empty one                                                              |
| Authentication     | `Authorization: Klaviyo-API-Key pk_…` and nothing else: `Bearer` is refused by name, the six-character public site id is refused as the wrong key off the same settings page, a malformed key and a foreign key are refused as credentials, and a revoked key is refused *as revoked* rather than as malformed                                                                                        |
| Scopes             | Per-resource read and write scopes as container state, with a `403` naming both the scope it wanted and the scopes the key holds — because widening the key is what a human does with that error                                                                                                                                                                                                      |
| Rate limits        | Klaviyo's two windows at once, per endpoint: a one-second burst bucket and a one-minute steady bucket, both enforced on every call, with the numbers read out of the pinned SDK's own docstrings. `429` carries `Retry-After` and **replaces** the three `RateLimit-*` headers, which ride on everything else                                                                                         |
| Profiles           | `POST /api/profiles` with the `409` carrying the existing profile's id, `POST /api/profile-import` as the upsert it is, a `PATCH` that merges `properties` rather than replacing them, custom properties, `location`, and a document whose `data.id` disagrees with the path refused with `source.pointer` naming it                                                                                  |
| Lists              | Create, update, membership added and removed through the relationship endpoint at `204` with no body, and `GET /api/lists/{id}/profiles` with each member's `joined_group_at` and subscription state. Adding a profile does not subscribe it, and a suppressed profile may be added and stays suppressed                                                                                              |
| Consent            | `NEVER_SUBSCRIBED`, `SUBSCRIBED` and `UNSUBSCRIBED` per channel, moved by bulk jobs that answer `202` and change nothing until the sandbox is advanced. Subscribing an already-subscribed profile does not re-stamp its consent timestamp, and a subscribe clears the soft suppressions while leaving a hard bounce where it is                                                                       |
| Suppressions       | A ledger with Klaviyo's five reasons, which a send reads to decide who it skips — and which no provider call can produce for a bounce, an invalid address or a spam complaint, so a directive does                                                                                                                                                                                                    |
| Events and metrics | `POST /api/events` at `202` with no body, the metric created by being named and never twice, the profile created from the event's own identifiers, both ecommerce metric shapes, `GET /api/events` with Klaviyo's filter grammar, and two identical calls making two events, because there is no idempotency key                                                                                      |
| Segments           | A definition over profile properties, list membership or channel consent, evaluated live. A condition the twin cannot evaluate is refused at creation rather than accepted as a segment that silently holds nobody                                                                                                                                                                                    |
| Campaigns          | Created as a `Draft` with its message, a template assigned by cloning the way Klaviyo clones, a send job that comes back `queued` with nothing sent, the send performed by `advance` with per-recipient results, and refusals for a second send, an audience that is empty, a message with no template and an edit after sending                                                                      |
| Bulk jobs          | Profile import `queued` → `processing` → `complete`, with the profiles readable only at the end, an import into a list, and per-row failures readable from `/import-errors` with the original payload a caller has to fix                                                                                                                                                                             |
| JSON:API           | `data` an object for one resource and an array for a collection — including the collection of one that `GET /api/accounts` is — `attributes` never at the top level, `relationships` carrying resource identifier objects with both links, `included` as a top-level array of full resource objects, sparse fieldsets that leave the others *absent* rather than null, and `links.self` on everything |
| Paging             | Cursors inside `links`, with **`links.next` as an absolute URL on the container's own host** so a paginating client stays inside the sandbox. Per-endpoint page sizes with their own maxima, refused above rather than clamped, `next` absent at the end rather than null, and a cursor replayed under a different filter refused rather than answered with the wrong rows                            |

Advancing the sandbox steps the asynchronous half: bulk jobs move one state, a
campaign send job performs its send, a scheduled campaign whose time has passed
queues one, and lagging segment membership catches up. Nothing else ages — profiles
do not decay and segments do not expire.

## Scenarios

`stocked-account`, `suppressed-profiles`, `pending-bulk-job`, `campaign-scheduled`,
`narrow-scopes`, `rate-limited`, `empty-account`.

## Not modelled

Flow creation and flow logic — flows are read-only, because a flow definition is a
visual builder's document — the client-side tracking endpoints and the public site
id beyond refusing it, Coupons, Reviews, Forms, Data Privacy deletion requests,
Webhooks and their delivery, Reporting and Predictive Analytics as computed values,
Catalog and CDP objects, A/B test campaign variations, SMS and push campaigns, and
OAuth app install — the twin is handed a private key.

Two things the twin is deliberately careful about. A route Klaviyo *has* and this
twin has not built is refused with a code Klaviyo never sends, never `not_found`:
revisions genuinely remove endpoints, so a caller reading `not_found` could
reasonably conclude the endpoint was retired. And `predictive_analytics` is `null`
on a free plan rather than a set of invented numbers — on a paid one the twin
reports the figures it can derive from the events it holds and omits the model
outputs it cannot compute. The twin's own README explains every such decision and
where each number came from.
