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

# Klaviyo

> Sync Klaviyo lists and segments into Sotto, and push user profiles, custom data, and SMS consent to Klaviyo.

The Klaviyo integration keeps your Sotto and Klaviyo audiences in step, in both
directions. You connect it with a Klaviyo **private API key**, and each of the
three capabilities can be toggled separately. **All capabilities start disabled
when you first connect.** Saving a key stores your credentials and tests them,
but nothing syncs or pushes until you switch each capability on:

| Capability | Direction | What moves |
| - | - | - |
| [Profile push](#profile-push) | Sotto → Klaviyo | Name, email, and phone number as Klaviyo profile attributes, in near real time. Optionally includes your Sotto custom data fields and the date the user was created in Sotto as Klaviyo profile properties. |
| [SMS consent sync](#sms-consent-sync) | Sotto → Klaviyo | SMS subscribe and unsubscribe state. |
| [Tap events](#tap-events) | Sotto → Klaviyo | Each phygital device tap attributed to a user, as a Klaviyo metric event. |
| [List and segment sync](#list-and-segment-sync) | Klaviyo → Sotto | Membership of the Klaviyo lists and segments you choose, refreshed every 3 hours. |

## API key and scopes

Sotto authenticates to Klaviyo with a private API key, created in Klaviyo under
**Settings → Account → API keys**. A full-access key enables everything. If you
prefer a restricted key (recommended), grant it the scopes for the features you
plan to use:

| Sotto feature | Required Klaviyo scopes |
| - | - |
| Connection test | `lists:read` |
| Profile push (with or without custom data) | `profiles:write` |
| SMS consent sync | `lists:write`, `profiles:write`, `subscriptions:write` |
| Tap events | `events:write` |
| List and segment sync | `lists:read`, `segments:read`, `profiles:read` |

To enable the full integration, grant all seven scopes: `lists:read`,
`lists:write`, `segments:read`, `profiles:read`, `profiles:write`,
`subscriptions:write`, and `events:write`.

<Warning>
  Sotto's connection test validates the key with a **read-only** call, so it
  only proves the `lists:read` scope. A key missing the write scopes will test
  as healthy and then be rejected when the first profile or consent push runs.
  When Klaviyo rejects the key as unauthorized, Sotto **pauses all pushes for
  your account automatically** (no further calls are attempted) and the
  integration status shows the reason. (Only definitive authentication
  failures pause pushes; a transient Klaviyo error or outage never does.)
  Klaviyo keys cannot have scopes added after creation, so the fix is to
  create a new key with the right scopes and save it in Sotto: the save
  re-tests the key and pushes resume automatically. Testing the connection
  after fixing the key in Klaviyo resumes pushes the same way. Users whose
  changes were skipped while paused catch up on their next change.
</Warning>

<Note>
  The API key is write-only in Sotto: it is never displayed again after you
  save it. To rotate it, generate a new key in Klaviyo with the same scopes and
  enter it in Sotto; the old key can then be deleted in Klaviyo.
</Note>

## Profile push

**Required scopes:** `profiles:write`

Whenever a user is created or changes in Sotto, their profile is pushed to
Klaviyo within seconds. A push is triggered when a user:

* is created or updated (including imports and API updates),
* opts in to or out of SMS messaging,
* has a custom data value change, or
* records a purchase.

**Changes that arrive *from* Klaviyo are never pushed back.** Users created by
[list and segment sync](#list-and-segment-sync) and users created or updated
through the [inbound endpoint](#creating-users-from-klaviyo) below are excluded
from profile push for that change. Your Klaviyo data is not echoed back at
you, and a Klaviyo flow that calls the inbound endpoint cannot create an update
loop. The next genuine Sotto-side change to such a user (a reply, a purchase, a
portal edit) pushes normally.

### Field mapping

| Sotto field | Klaviyo field |
| - | - |
| Phone number | `phone_number` (profile attribute) |
| Email address | `email` (profile attribute) |
| First name | `first_name` (profile attribute) |
| Last name | `last_name` (profile attribute) |
| Custom data fields | Profile **properties**, one per field, named `sotto_<field_name>`: `favorite_hobbies` becomes `sotto_favorite_hobbies`. A field whose name already starts with `sotto_` is prefixed too: `sotto_x` becomes `sotto_sotto_x`. See [Custom data](#custom-data). |
| Date the user was created in Sotto | `sotto_date_created` (profile property). Sent only with custom data on. See [Date created](#date-created). |

Pushes are **upserts**: Klaviyo matches profiles by phone number and email, so
existing profiles are updated in place and no duplicates are created. Sotto
stores the resulting Klaviyo profile ID alongside the user.

Pushes are also **self-healing**: a transient failure (for example a Klaviyo
rate limit) is retried automatically for a few minutes. If a push cannot be
delivered, it is dropped rather than queued forever: the user's next change
pushes their complete, current profile, so nothing stays stale for long.

### Custom data

Custom data is an option of the profile push, not a separate push: the
properties travel in the same Klaviyo call as the profile attributes, so it
needs no extra scope and cannot be enabled on its own.

* **Profile push on, custom data on**: attributes, custom data properties
  and [`sotto_date_created`](#date-created) are pushed.
* **Profile push on, custom data off**: only the profile attributes are
  pushed. Use this to share contact fields with Klaviyo without sharing your
  business-specific custom data.
* **Profile push off**: nothing is pushed to the profile, and custom data
  cannot be enabled.

Every custom data property is prefixed `sotto_`, the same namespace the
[tap event properties](#event-properties) use. Klaviyo keeps the properties
from all of your integrations side by side on the profile, so the prefix keeps
Sotto's properties apart from the ones your other tools write, and a Sotto
field can never overwrite one of theirs. Properties written before the prefix
was introduced (see the [changelog](/changelog)) kept the bare field name, for
example `favorite_hobbies`. They stay on your profiles but no longer update;
build segments and flows on the `sotto_` names.

<Info>
  Turning custom data off stops future property updates; it does not remove
  properties already written, because Klaviyo merges properties on update and
  never deletes them.
</Info>

### Date created

With custom data on, Sotto also sends one stock property,
`sotto_date_created`: the date and time the user was first created in Sotto,
in UTC ISO 8601 to the second, for example `2026-04-16T19:35:13+00:00`.
Klaviyo reads it as a date, so you can segment on it with date conditions
such as *is after* or *is in the last*. It is the creation date, not the
opt-in date: users imported from Shopify, a CSV file, or an external signup
carry the date they were imported. There is no backfill; a profile that was
pushed before the property existed receives it on its next push.

<Note>
  `date_created` is a reserved name and cannot be used for a custom data
  field, because the field would push to the same `sotto_date_created`
  property. Look-alikes are rejected too: matching ignores case and treats
  spaces and hyphens as underscores, so `Date Created` and `date-created` are
  blocked. Names that only contain it, such as `first_order_date_created`,
  are allowed.
</Note>

## SMS consent sync

**Required scopes:** `lists:write`, `profiles:write`, `subscriptions:write`

Consent sync reflects each user's Sotto SMS consent onto their Klaviyo profile.
It is **one-way, from Sotto to Klaviyo**: changes made in Klaviyo do not affect
consent in Sotto, and Sotto remains the source of truth for SMS consent.

| Sotto consent state | Result in Klaviyo |
| - | - |
| Opted in | SMS marketing consent set to `SUBSCRIBED`. |
| Opted out | SMS marketing consent set to `UNSUBSCRIBED`. |
| Initial / pending opt-in | No action. Unconfirmed states are never pushed. |

A few things to know:

* Consent is matched and applied by **phone number**, and is profile-level in
  Klaviyo. No Klaviyo list is required.
* Optionally, Sotto can also add subscribed users to a specific Klaviyo list.
  Contact [support@withsotto.com](mailto:support@withsotto.com) to configure a
  target list.
* Klaviyo processes consent changes as an asynchronous job, so the new state
  can take 30 to 90 seconds to appear in Klaviyo.
* Consent sync is best-effort: a Klaviyo-side failure (for example, no SMS
  sending number for the user's region) never blocks or reverses the profile
  push.

## Tap events

**Required scopes:** `events:write`

When a tap on one of your phygital devices is attributed to a user, Sotto sends
Klaviyo a metric event for that profile. The events land on the metric
**Phygital Device Tap** unless you set your own name in Sotto.

Only attributed taps are sent. A tap that Sotto cannot tie to a user, because
nobody has identified themselves yet, produces no event.

### Event properties

Every property is prefixed `sotto_`, so tap events never collide with metrics
your other tools write:

| Property | Notes |
| - | - |
| `sotto_tap_id` | Identifies the tap. Also the event's `unique_id`, so a retry cannot double-count. |
| `sotto_tap_type` | How the tap arrived, for example `nfc`. |
| `sotto_qr_code_id` | The code behind the tap. |
| `sotto_device_id`, `sotto_device_name`, `sotto_device_identifier` | The device. Absent for a plain QR scan with no device. |
| `sotto_device_tap_count` | See the warning below. |
| `sotto_device_labels`, `sotto_device_label_ids` | Your own label taxonomy. `sotto_device_labels` holds the `key:value` form, so it can be filtered on directly. |
| `sotto_location_id`, `sotto_location_name`, `sotto_location_latitude`, `sotto_location_longitude` | The device's location. Off by default; switch it on in Sotto if you want consumer location data in Klaviyo. |

<Warning>
  `sotto_device_tap_count` is the **device's** lifetime tap count, across
  everyone who has ever tapped it. It is not the number of times this profile
  has tapped. Building a Klaviyo segment on it as a per-profile total gives
  numbers that look plausible and are wrong.
</Warning>

<Note>
  Klaviyo accepts events asynchronously: a successful send means Klaviyo has
  **accepted** the event, not that it has finished processing it and made it
  visible on the profile. Events normally appear within seconds, but if you are
  reconciling counts, allow for that gap before treating an event as lost.
</Note>

## List and segment sync

**Required scopes:** `lists:read`, `segments:read`, `profiles:read`

Sotto imports the membership of the Klaviyo lists and segments you select. Each
one appears in Sotto as a segment named `Klaviyo - {name}` and refreshes every
3 hours:

* Members are matched to Sotto users by **phone number**. Klaviyo profiles
  without a phone number cannot be matched and are skipped.
* A profile with no matching Sotto user creates a new user with the profile's
  phone number, name, and email. Created users start without SMS consent;
  syncing a list never opts anyone in.
* Users created this way are **not pushed back to Klaviyo**; their profile
  already exists on your side, so the sync never writes to Klaviyo.
* Membership is mirrored on every refresh: users are added when they join the
  Klaviyo list or segment and removed when they leave it.
* Joining or leaving a synced segment fires segment entry and exit events, so
  these segments can drive Sotto automations and campaign targeting like any
  other segment.

## Creating users from Klaviyo

Independently of the sync above, your Klaviyo flows can create or update Sotto
users directly, for example from a webhook action in a Klaviyo signup flow.

Send a `POST` to `/api/v1/klaviyo/users`, authenticated with your **Sotto API
key** in the `X-Bigco-Api-Key` header. This is not the Klaviyo API key, and
not the request signing used by the rest of the Sotto API: Klaviyo's webhook
action can only send static headers, so this endpoint deliberately uses simple
key-header authentication. In Klaviyo's webhook action, add a header named
`X-Bigco-Api-Key` with your Sotto API key as its value.

Updates made through this endpoint are **never echoed back** to your Klaviyo
account by profile push, so it is safe to call from flows that trigger on
profile changes.

<ParamField body="phone_number" type="string" required>
  The user's phone number in E.164 format. Users are matched by phone number,
  so repeated calls update the same user. A missing or invalid phone number is
  rejected with `422` and nothing is stored.
</ParamField>

<ParamField body="email_address" type="string">
  The user's email address.
</ParamField>

<ParamField body="first_name" type="string">
  The user's first name.
</ParamField>

<ParamField body="last_name" type="string">
  The user's last name.
</ParamField>

<ParamField body="date_of_birth" type="string">
  The user's date of birth, in `MM/DD/YYYY` format.
</ParamField>

<ParamField body="zip_code" type="string">
  The user's postal code.
</ParamField>

<ParamField body="custom_data" type="object">
  Custom data values, keyed by the **Sotto** field name (`loyalty_tier`), not
  the name the field has as a Klaviyo profile property (`sotto_loyalty_tier`).
  A flow that maps a profile property back into this field must send the Sotto
  name. Every field name must already be configured for your brand; an
  unrecognized field name, including a `sotto_`-prefixed one, is rejected with
  `400` and nothing from that request is stored.
</ParamField>

<ParamField body="bigco_opt_in" type="boolean" default="false">
  When `true`, starts the SMS opt-in flow for users who have not already opted
  in. The endpoint never opts a user in directly.
</ParamField>

```json Example request body theme={"dark"}
{
  "phone_number": "+15551234567",
  "email_address": "jane.doe@example.com",
  "first_name": "Jane",
  "last_name": "Doe",
  "custom_data": {
    "loyalty_tier": "Gold"
  },
  "bigco_opt_in": true
}
```

### Error responses

Errors use the standard envelope (see [Errors](/errors)):

| Status | `detail.type` | Meaning |
| - | - | - |
| `400` | `custom_data_not_found` | A `custom_data` field name is not configured for your brand. The message names the offending field. |
| `422` | `invalid_phone_number` | The phone number is missing or not a valid number. |
| `401` | `not_authenticated` | The `X-Bigco-Api-Key` value is not a valid API key. |

A `200` response means the user was created or updated. Watch your Klaviyo
flow's webhook analytics for non-`200` responses: a misconfigured flow (for
example, a mis-mapped phone number field) fails loudly rather than silently.

<Note>
  Need help wiring a Klaviyo flow to this endpoint, or deciding between the
  push, the sync, and the inbound API? Reach out to
  [support@withsotto.com](mailto:support@withsotto.com).
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.