# Klaviyo

- Tool: Bubble
- Difficulty: Intermediate
- Time required: 45–75 minutes
- Last updated: July 2026

## TL;DR

Connect Bubble to Klaviyo using the API Connector plugin with a Klaviyo Private API key in a Klaviyo-API-Key Authorization header (not Bearer) marked Private, plus a required revision header. All calls run server-side from Bubble's servers, keeping the private key out of the browser. Use this to sync e-commerce purchase events, add subscribers to lists, and look up profiles for personalized flows. Free tier supports up to 250 profiles.

## Bubble to Klaviyo: e-commerce event sync and list management

Klaviyo is a top choice for Bubble e-commerce apps that want to send purchase confirmation emails, abandoned-cart reminders, or post-purchase upsell sequences driven by what users actually do in the app. The integration works in two complementary modes. The first is server-side: your Bubble workflows call the Klaviyo API directly through the API Connector whenever a user purchases, signs up, or performs a tracked action — profiles are created or updated and events are posted to Klaviyo. The second is browser-side: Klaviyo's JavaScript snippet (using the Public key only) tracks page views, product interactions, and custom events from within the Bubble page itself. Together, these modes give Klaviyo the behavioral data it needs to power its segmentation and flow engine. The two Klaviyo-specific details to get right from the start: the Authorization header prefix is 'Klaviyo-API-Key' rather than 'Bearer', and every v2 API call requires a 'revision' date header. Missing either of these causes 401 errors or deprecated API behavior that is hard to diagnose.

## Before you start

- A Klaviyo account — the free plan covers up to 250 profiles; paid plans scale with list size
- A Bubble app on any plan (outbound API Connector calls work on the free plan; the JS tracking snippet in page header requires a paid Bubble plan)
- The API Connector plugin installed in your Bubble app (Plugins tab → Add plugins → search 'API Connector' by Bubble → Install)
- A Klaviyo list ID for any list subscription workflows (find it in Klaviyo → Lists → select list → Settings tab)

## Step-by-step guide

### 1. Step 1 — Generate a Klaviyo Private API key with required scopes

Log in to Klaviyo and navigate to Settings → API Keys (accessible from the account menu in the lower left). You will see two sections: Public API Key and Private API Keys. The Public API Key (which starts with a short alphanumeric string) is safe to use in browser-side JavaScript — you will need this later if you add the tracking snippet to your Bubble page header. For the API Connector, you need a Private API Key. Click Create API Key. Give it a name like 'Bubble Integration'. Klaviyo lets you scope the key's access. At minimum, enable: Profiles (Read + Write) for profile lookup and creation, Lists (Read + Write) for list subscription, and Metrics (Read) if you plan to pull event data into Bubble dashboards. If you will post custom events, also enable Events (Write). Click Create. Klaviyo shows the full private key once — it starts with `pk_` followed by a long string. Copy it immediately. If you lose it, you must generate a new key and update the Bubble API Connector. Keep both the Public key and the Private key accessible — you will use them in different places. Never put the Private key in the Klaviyo JavaScript snippet or any client-side code.

**Expected result:** You have a Klaviyo Private API key starting with pk_ and a Public API Key noted separately. You also have one or more Klaviyo list IDs noted for use in subscription workflows.

### 2. Step 2 — Add the API Connector and configure Klaviyo shared headers

In your Bubble editor, click the Plugins tab in the left sidebar. Click Add plugins. Search for 'API Connector' and install the plugin named API Connector by Bubble (the official blue Bubble icon plugin). Open the API Connector configuration panel and click Add another API. In the API Name field, type 'Klaviyo'. In the Authentication dropdown, select None — you will configure authentication manually as shared headers. In the Shared headers section, add the first header: key is `Authorization`, value is `Klaviyo-API-Key pk_xxxxxxxxxxxxxxxxxxxxxxxxxx` — paste your Private API key after 'Klaviyo-API-Key ' with one space between them. This is the most critical formatting detail: the Authorization prefix is 'Klaviyo-API-Key', not 'Bearer'. Using 'Bearer' causes 401 errors that look identical to a wrong API key, making them difficult to diagnose. Tick the Private checkbox on this header — this prevents the private key from being included in any page HTML sent to browsers. Add a second header: key is `revision`, value is `2024-02-15`. This version date header is required by Klaviyo's v2 API for every call. Without it, Klaviyo either returns an error or falls back to deprecated response formats. Add a third header: key is `Content-Type`, value is `application/json`. Neither revision nor Content-Type need the Private checkbox.

```
{
  "api_name": "Klaviyo",
  "base_url": "https://a.klaviyo.com",
  "shared_headers": [
    {
      "key": "Authorization",
      "value": "Klaviyo-API-Key pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "private": true
    },
    {
      "key": "revision",
      "value": "2024-02-15",
      "private": false
    },
    {
      "key": "Content-Type",
      "value": "application/json",
      "private": false
    }
  ]
}
```

**Expected result:** The Klaviyo API is saved in the API Connector with three shared headers — Authorization (Private), revision, and Content-Type. The base URL is set to https://a.klaviyo.com.

### 3. Step 3 — Create the searchProfiles GET call for email lookup

Inside the Klaviyo API, click New call. Name it 'searchProfiles'. Set the method to GET. URL: `/api/profiles`. Add a URL parameter key `filter` with a dynamic value — the Klaviyo filter syntax for email lookup is `equals(email,'<email_address>')` where the single quotes inside the parentheses are part of the required syntax. The `<email_address>` placeholder becomes a dynamic input field in Bubble workflows. Set Use as to Data, because you will use the results list to check whether a profile exists before deciding to create or update. Click Initialize call and enter a real email address that exists as a Klaviyo profile in your account — Klaviyo must return a response with at least one profile for Bubble to detect the nested JSON:API response schema. Klaviyo's v2 API wraps results in a JSON:API envelope: `{ data: [ { id, type, attributes: { email, first_name, ... } } ] }`. In Bubble, you reference fields as 'data[0]'s attributes > email' or 'data[0]'s id'. The profile ID from this call — 'data[0]'s id' — is what you will pass to the addToList and updateProfile calls, so make sure Bubble detects it during initialization.

```
GET https://a.klaviyo.com/api/profiles
  ?filter=equals(email,'<email_address>')

Headers (shared):
  Authorization: Klaviyo-API-Key pk_xxxx  [Private]
  revision: 2024-02-15

Sample response:
{
  "data": [
    {
      "type": "profile",
      "id": "01HXXXXXXXXXXXXXXXXXXXXXX",
      "attributes": {
        "email": "user@example.com",
        "first_name": "Jane",
        "last_name": "Smith",
        "phone_number": "+15551234567",
        "properties": {}
      }
    }
  ]
}
```

**Expected result:** The searchProfiles call is saved and initialized. Bubble exposes 'data[0]'s id' and 'data[0]'s attributes > email' as accessible fields for use in workflow conditions.

### 4. Step 4 — Create the createProfile POST call

In the same Klaviyo API, click New call. Name it 'createProfile'. Set the method to POST. URL: `/api/profiles`. Set Body type to JSON body. Klaviyo's profile creation endpoint uses a JSON:API-style request body — the data object must have a `type` field set to the string 'profile' and an `attributes` object with the profile fields. Include `email` as a required attribute and optionally `first_name`, `last_name`, `phone_number`, and a `properties` object for custom attributes. Use angle-bracket placeholders for the dynamic fields that Bubble will populate from workflow data. Set Use as to Action, because this call creates a record. Click Initialize call and provide real test values — use an email address that does NOT already exist as a Klaviyo profile, since the create endpoint returns 409 Conflict for duplicate emails. After initialization, Bubble captures the response which includes the new profile's `id` in the `data.id` field — this ID is what you need for the addToList call in the next step. If the initialize call returns a 409, delete the test profile in Klaviyo (Audience → Profiles → search by email → delete) and retry with a different email.

```
POST https://a.klaviyo.com/api/profiles

Headers (shared):
  Authorization: Klaviyo-API-Key pk_xxxx  [Private]
  revision: 2024-02-15
  Content-Type: application/json

Body:
{
  "data": {
    "type": "profile",
    "attributes": {
      "email": "<email>",
      "first_name": "<first_name>",
      "last_name": "<last_name>",
      "properties": {
        "plan": "<plan_tier>",
        "source": "Bubble signup"
      }
    }
  }
}
```

**Expected result:** The createProfile call is saved and initialized. The new profile's ID is accessible in workflows as 'Result of createProfile's data > id'.

### 5. Step 5 — Create the addToList POST call and wire the full signup workflow

In the Klaviyo API, click New call. Name it 'addToList'. Set the method to POST. URL: `/api/lists/<list_id>/relationships/profiles` — the `<list_id>` placeholder is replaced with the actual Klaviyo list ID when you invoke the call from a workflow. Set Body type to JSON body. The request body must include a `data` array where each item has `type: profile` and the profile `id` you want to add. Set Use as to Action. Click Initialize call using a real list ID from your Klaviyo account and a real profile ID from a profile you created in step 4. Now open the Bubble page where your signup form lives. In the workflow editor, click the Submit Button is clicked event. Build a three-step chain: Step 1 runs searchProfiles with the submitted email. Step 2 runs createProfile only when 'Result of Step 1's data:count = 0' (no existing profile). Step 3 runs addToList with the profile ID — use 'Result of Step 2's data > id' if a new profile was created, or 'Result of Step 1's data[0]'s id' if the profile already existed. You can use a Bubble custom state to store the resolved profile ID between the conditional branches before passing it to addToList. RapidDev's team has wired up Klaviyo list enrollment for dozens of Bubble e-commerce apps — if your segmentation logic is complex, a free scoping call is available at rapidevelopers.com/contact.

```
POST https://a.klaviyo.com/api/lists/<list_id>/relationships/profiles

Headers (shared):
  Authorization: Klaviyo-API-Key pk_xxxx  [Private]
  revision: 2024-02-15
  Content-Type: application/json

Body:
{
  "data": [
    {
      "type": "profile",
      "id": "<profile_id>"
    }
  ]
}

Workflow chain:
  Step 1: searchProfiles (email = Input Email's value)
  Step 2 (if data:count = 0): createProfile (email, first_name, last_name)
  Step 3: addToList
    list_id    = YOUR_KLAVIYO_LIST_ID
    profile_id = Step 2's data > id (new) OR Step 1's data[0]'s id (existing)
```

**Expected result:** The addToList call is saved and initialized. Submitting the signup form with a new email creates a Klaviyo profile and adds it to the list. Submitting with an existing email looks up the existing profile and adds it to the list without creating a duplicate.

### 6. Step 6 — Create the createEvent POST call for purchase and behavior tracking

In the Klaviyo API, click New call. Name it 'createEvent'. Set method to POST. URL: `/api/events`. Set Body type to JSON body. Klaviyo's event API uses the JSON:API envelope format. The data object must specify `type: event`, and the attributes include `metric.data.name` (the event name like 'Placed Order'), the profile object with the user's email, and a `properties` object for event-specific data like order total, product names, or SKUs. This is the most flexible call — any string name you use for the metric automatically creates that metric in Klaviyo's dashboard. Set Use as to Action. Click Initialize call with real values including a real email and a recognizable event name. After initialization, wire this call to Bubble's purchase confirmation workflow — typically triggered after a payment plugin (Stripe, etc.) confirms a successful charge. Map order data from Bubble's database fields to the event properties. Klaviyo ingests the event, matches the email to an existing profile or creates a new one, and triggers any flows that listen for that metric name. Note: Klaviyo's private key rate limit is approximately 75 requests per second — for bulk event posting, Bubble's scheduled Backend Workflow (paid plan) with delays between steps prevents rate limit errors.

```
POST https://a.klaviyo.com/api/events

Headers (shared):
  Authorization: Klaviyo-API-Key pk_xxxx  [Private]
  revision: 2024-02-15
  Content-Type: application/json

Body:
{
  "data": {
    "type": "event",
    "attributes": {
      "metric": {
        "data": {
          "type": "metric",
          "attributes": {
            "name": "<event_name>"
          }
        }
      },
      "profile": {
        "data": {
          "type": "profile",
          "attributes": {
            "email": "<customer_email>"
          }
        }
      },
      "properties": {
        "OrderId": "<order_id>",
        "Value": "<order_total>",
        "ItemNames": "<product_names>"
      },
      "value": "<order_total>"
    }
  }
}
```

**Expected result:** The createEvent call is saved and initialized. When a user completes a purchase, the Bubble workflow fires the event to Klaviyo, the profile is created or updated, and any matching Klaviyo flows begin sending emails automatically.

## Best practices

- Never put the Klaviyo Private API key in the JavaScript tracking snippet or any client-side code. The JS snippet uses the Public key only. The Private key lives exclusively in the API Connector shared header marked Private.
- Always include the 'revision: 2024-02-15' header at the shared API level in the API Connector so it applies to every Klaviyo call automatically — forgetting it on individual calls causes inconsistent behavior that is difficult to debug.
- Use the searchProfiles call before createProfile to avoid duplicate profile creation. Klaviyo returns 409 Conflict for duplicate emails, so a search-first pattern prevents errors and keeps your Klaviyo profile list clean.
- Store the Klaviyo profile ID in the matching Bubble user record immediately after creation. Subsequent workflows that update the profile, add it to lists, or post events can reference the stored ID directly instead of making a lookup call every time — this saves WU and stays under rate limits.
- Enable privacy rules on any Bubble data type that stores Klaviyo profile IDs or customer event data. Navigate to Data tab → Privacy and restrict 'Find in searches' and 'View all fields' by user role. Without privacy rules, any logged-in Bubble user can query all stored data through Bubble's built-in API.
- Use Klaviyo's batch endpoints for bulk operations such as importing large contact lists. Sending hundreds of individual createProfile calls in a loop consumes Bubble WU and risks hitting the 75 req/sec rate limit. The batch profiles endpoint accepts arrays of profile objects in a single POST.
- Prefer Klaviyo's behavioral event tracking ('Placed Order', 'Viewed Product') over manual list segment updates where possible. Klaviyo's flow engine is optimized for event triggers and produces better segmentation than manually managing list membership for every user state change.
- Monitor Bubble's Logs tab → Workflow logs regularly to catch Klaviyo API errors early. A failed createEvent call means a user's purchase was not recorded in Klaviyo and post-purchase flows will not fire — catching this quickly prevents gaps in your email marketing data.

## Use cases

### E-commerce purchase event → Klaviyo flow trigger

When a Bubble user completes a purchase, a Bubble workflow fires a POST to Klaviyo's /api/events endpoint with the order total, product list, and customer email. Klaviyo ingests this as a 'Placed Order' event and uses it to trigger post-purchase email sequences, upsell flows, and loyalty campaigns automatically without any manual list management.

Prompt example:

```
After the payment workflow completes, run: API Connector → Klaviyo createEvent (type=Placed Order, email=Current User's email, value=Order's total, properties=Order's item list as JSON). Klaviyo matches the email to an existing profile or creates a new one and triggers the post-purchase flow.
```

### Signup form → Klaviyo list subscription

A Bubble landing page collects email addresses for a waitlist or newsletter. On form submit, the workflow first looks up the Klaviyo profile by email, creates it if it does not exist, then adds the profile ID to the correct Klaviyo list. This is a three-step chain in Bubble — lookup, create if needed, subscribe — but results in clean Klaviyo list membership with the correct double-opt-in flow.

Prompt example:

```
When the signup button is clicked: Step 1 — searchProfiles by email. Step 2 — if no result, createProfile with email and name. Step 3 — addToList with the profile ID from step 1 or 2 and the waitlist's Klaviyo list ID.
```

### Profile enrichment from Bubble user data

When a Bubble user completes their profile (adds company, job title, or plan tier), a Bubble workflow PATCH-updates the corresponding Klaviyo profile with the new properties. Klaviyo uses these properties for segmentation — for example, sending different onboarding sequences to free-plan vs paid-plan users based on a custom 'plan' profile property.

Prompt example:

```
On user profile update workflow: API Connector → Klaviyo updateProfile (profile_id=Current User's klaviyo_id, attributes.properties.plan=Current User's plan, attributes.properties.company=Current User's company). Klaviyo updates the profile properties and re-evaluates any active segment conditions.
```

## Troubleshooting

### Every API call returns 401 Unauthorized even though the API key looks correct

Cause: The Authorization header prefix is 'Bearer' instead of 'Klaviyo-API-Key', or the Private checkbox is unticked, or the key was copied incorrectly and is missing characters.

Solution: Open API Connector → Klaviyo → shared headers. Confirm the Authorization value is exactly 'Klaviyo-API-Key pk_xxx...' with 'Klaviyo-API-Key' as the prefix (not 'Bearer'). Confirm the Private checkbox is ticked. Copy the key fresh from Klaviyo Settings → API Keys and replace the header value. The 'Klaviyo-API-Key' prefix is Klaviyo-specific and different from most APIs — this is the single most common configuration mistake for this integration.

### API calls return errors or unexpected deprecated response formats

Cause: The required 'revision' header is missing or contains an outdated or misspelled date string. Klaviyo's v2 API requires this header on every call.

Solution: Open API Connector → Klaviyo → shared headers and confirm you have a header with key 'revision' and value '2024-02-15'. Check that the header is at the shared level (not inside a specific call) so it applies to all requests. The date format must be YYYY-MM-DD exactly. If Klaviyo releases a newer API revision, update this date to the new version string found in Klaviyo's API changelog.

### Initialize call fails with 'There was an issue setting up your call'

Cause: The Initialize call requires a real, successful response from Klaviyo. Common causes: using an email that already exists for the createProfile call (returns 409), using an incorrect list ID for addToList (returns 404), or having malformed JSON in the body.

Solution: For createProfile: use an email address that does not exist in Klaviyo yet. For addToList: copy the list ID directly from Klaviyo → Audience → Lists → select list → Settings tab. For any call: read the exact error message Klaviyo returns in Bubble's API Connector response viewer — it identifies whether the problem is the credentials, a missing field, or invalid JSON syntax.

### Repeating group for Klaviyo profiles shows empty cells despite a successful API call

Cause: Klaviyo's JSON:API response format wraps all attributes inside a nested 'attributes' object: data[0] > attributes > email. Bubble's default path detection may not fully map the nested fields if the initialize call returned empty results.

Solution: Re-initialize the searchProfiles call using an email that actually has a Klaviyo profile. During initialization, Bubble reads the response and detects all nested field paths. After re-initialization, the repeating group cell bindings should expose 'data[0]'s attributes > email', 'data[0]'s attributes > first_name', etc. Check each cell binding individually in the Bubble editor to confirm the correct path is selected.

### Rate limit errors (429) when a workflow processes multiple users at once

Cause: Klaviyo's Private API key rate limit is approximately 75 requests per second. A Bubble workflow that loops through a list of users and fires createProfile or createEvent for each one in rapid succession can exceed this limit.

Solution: Switch to Bubble's Backend Workflow with scheduling to batch API calls with delays between them (requires paid Bubble plan). For bulk list imports, consider Klaviyo's bulk import endpoints which accept arrays of profiles in a single call rather than one profile per call. For real-time events, rate limiting is usually only a concern if many users trigger the same workflow simultaneously — monitor Bubble's Logs tab for 429 responses and add error-handling steps that retry after a delay.

## Frequently asked questions

### What is the difference between Klaviyo's Public key and Private key, and which one goes in the API Connector?

Klaviyo's Public key is designed for browser-side use — it is safe to include in JavaScript code that runs on your Bubble page (the tracking snippet). The Private key has full read and write access to your Klaviyo account and must stay server-side. In Bubble, the Private key goes in the API Connector Authorization header marked Private. The Public key goes in the optional JavaScript snippet pasted into Bubble's page header HTML. Never reverse these — putting the Private key in client-side code exposes your entire Klaviyo account.

### Why does Klaviyo require a 'revision' header and what value should I use?

Klaviyo's v2 API uses the revision header to specify which API version your calls target. This lets Klaviyo evolve the API without breaking existing integrations. The value '2024-02-15' targets the current stable version. If you omit this header, Klaviyo either returns an error or uses a deprecated API version with a different response format. Check Klaviyo's API changelog for the latest revision date if you are setting up a new integration.

### Why can I not add a profile directly to a Klaviyo list by email — why do I need the profile ID?

Klaviyo's list subscription endpoint (POST /api/lists/{id}/relationships/profiles) accepts profile IDs in the JSON:API format, not email addresses directly. The correct flow is: look up or create the profile first to get its ID, then pass that ID to the list subscription call. This three-step pattern (lookup → create if needed → subscribe) is the standard Klaviyo v2 pattern for list enrollment from external tools like Bubble.

### Do I need a paid Bubble plan to integrate with Klaviyo?

No — the API Connector plugin works on Bubble's free plan for all outbound Klaviyo API calls including profile management and event tracking. You need a paid Bubble plan only if you want to use Backend Workflows for scheduled or batch operations (like refreshing metrics data on a schedule), or to paste the Klaviyo JS tracking snippet into the Bubble page's HTML header section.

### Klaviyo's free tier only supports 250 profiles — what happens when I exceed that?

When you exceed 250 active profiles on Klaviyo's free plan, Klaviyo prompts you to upgrade to a paid tier. The paid pricing scales based on the number of active profiles in your account. API calls will continue to work after you exceed the free limit as long as your account is on a valid paid plan. Plan for this threshold early — for a Bubble app with a growing user base, budget for Klaviyo's paid tier as part of your infrastructure costs.

### Can Klaviyo send webhooks to Bubble when a profile unsubscribes or a flow completes?

Yes — Klaviyo supports outbound webhooks for events like profile unsubscribes, email bounces, and flow step completions. To receive these in Bubble, you need to create a Backend Workflow exposed as an API endpoint (paid Bubble plan required) and register the endpoint URL in Klaviyo's webhook settings. Use Bubble's 'Detect request data' step to parse the Klaviyo webhook payload and trigger Bubble database updates accordingly.

---

Source: https://www.rapidevelopers.com/bubble-integrations/klaviyo
© RapidDev — https://www.rapidevelopers.com/bubble-integrations/klaviyo
