Klaviyo E-Commerce Marketing Skill
Load with: base.md + (typescript.md or python.md)
For integrating Klaviyo email/SMS marketing - customer profiles, event tracking, campaigns, flows, and segmentation.
Sources: Klaviyo API Docs | API Reference
Why Klaviyo
| Feature | Benefit | |---------|---------| | E-commerce Native | Built for online stores, deep integrations | | Event-Based | Trigger flows from any customer action | | Segmentation | Advanced filtering on behavior + properties | | Email + SMS | Unified platform for both channels | | Analytics | Revenue attribution per campaign |
API Basics
Base URLs
| Type | URL |
|------|-----|
| Server-side (Private) | https://a.klaviyo.com/api |
| Client-side (Public) | https://a.klaviyo.com/client |
Authentication
// Server-side: Private API Key
const headers = {
"Authorization": "Klaviyo-API-Key pk_xxxxxxxxxxxxxxxxxxxxxxxx",
"Content-Type": "application/json",
"revision": "2024-10-15", // API version
};
// Client-side: Public API Key (6 characters)
const publicKey = "XXXXXX"; // Company ID
// Use as query param: ?company_id=XXXXXX
API Key Scopes
| Scope | Access | |-------|--------| | Read-only | View data only | | Full | Read + write (default) | | Custom | Specific permissions |
Installation
Node.js
npm install klaviyo-api
// lib/klaviyo.ts
import { ApiClient, EventsApi, ProfilesApi, ListsApi } from "klaviyo-api";
const client = new ApiClient();
client.setApiKey(process.env.KLAVIYO_PRIVATE_KEY!);
export const eventsApi = new EventsApi(client);
export const profilesApi = new ProfilesApi(client);
export const listsApi = new ListsApi(client);
Python
pip install klaviyo-api
# lib/klaviyo.py
from klaviyo_api import KlaviyoAPI
klaviyo = KlaviyoAPI(
api_key=os.environ["KLAVIYO_PRIVATE_KEY"],
max_delay=60,
max_retries=3
)
Direct HTTP (Any Language)
// lib/klaviyo.ts
const KLAVIYO_BASE_URL = "https://a.klaviyo.com/api";
async function klaviyoRequest(
endpoint: string,
method: "GET" | "POST" | "PATCH" | "DELETE" = "GET",
body?: object
) {
const response = await fetch(`${KLAVIYO_BASE_URL}${endpoint}`, {
method,
headers: {
Authorization: `Klaviyo-API-Key ${process.env.KLAVIYO_PRIVATE_KEY}`,
"Content-Type": "application/json",
revision: "2024-10-15",
},
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Klaviyo API error: ${JSON.stringify(error)}`);
}
return response.json();
}
Profiles (Customers)
Create/Update Profile
// Upsert profile (create or update)
async function upsertProfile(data: ProfileInput) {
return klaviyoRequest("/profiles", "POST", {
data: {
type: "profile",
attributes: {
email: data.email,
phone_number: data.phone, // E.164 format: +1234567890
first_name: data.firstName,
last_name: data.lastName,
properties: {
// Custom properties
lifetime_value: data.ltv,
plan: data.plan,
signup_source: data.source,
},
location: {
city: data.city,
region: data.state,
country: data.country,
zip: data.zip,
},
},
},
});
}
# Python
def upsert_profile(data):
return klaviyo.Profiles.create_or_update_profile({
"data": {
"type": "profile",
"attributes": {
"email": data["email"],
"first_name": data["first_name"],
"last_name": data["last_name"],
"properties": {
"plan": data.get("plan"),
}
}
}
})
Get Profile
async function getProfileByEmail(email: string) {
const response = await klaviyoRequest(
`/profiles?filter=equals(email,"${email}")`
);
return response.data[0];
}
async function getProfileById(profileId: string) {
return klaviyoRequest(`/profiles/${profileId}`);
}
Update Profile Properties
async function updateProfileProperties(
profileId: string,
properties: Record<string, any>
) {
return klaviyoRequest(`/profiles/${profileId}`, "PATCH", {
data: {
type: "profile",
id: profileId,
attributes: {
properties,
},
},
});
}
// Usage
await updateProfileProperties("profile_id", {
last_purchase_date: new Date().toISOString(),
total_orders: 5,
vip_status: true,
});
Events (Tracking)
Track Event (Server-Side)
async function trackEvent(data: EventInput) {
return klaviyoRequest("/events", "POST", {
data: {
type: "event",
attributes: {
profile: {
data: {
type: "profile",
attributes: {
email: data.email,
// or phone_number, or external_id
},
},
},
metric: {
data: {
type: "metric",
attributes: {
name: data.eventName,
},
},
},
properties: data.properties,
value: data.value, // For revenue tracking
unique_id: data.uniqueId, // Deduplication
time: data.timestamp || new Date().toISOString(),
},
},
});
}
Common E-Commerce Events
// Viewed Product
await trackEvent({
email: customer.email,
eventName: "Viewed Product",
properties: {
ProductID: product.id,
ProductName: product.name,
ProductURL: product.url,
ImageURL: product.image,
Price: product.price,
Categories: product.categories,
},
});
// Added to Cart
await trackEvent({
email: customer.email,
eventName: "Added to Cart",
properties: {
ProductID: product.id,
ProductName: product.name,
Quantity: quantity,
Price: product.price,
CartTotal: cart.total,
ItemNames: cart.items.map(i => i.name),
},
value: product.price * quantity,
});
// Started Checkout
await trackEvent({
email: customer.email,
eventName: "Started Checkout",
properties: {
CheckoutURL: checkout.url,
ItemCount: cart.itemCount,
Categories: cart.categories,
ItemNames: cart.items.map(i => i.name),
},
value: cart.total,
});
// Placed Order
await trackEvent({
email: customer.email,
eventName: "Placed Order",
properties: {
OrderId: order.id,
ItemCount: order.itemCount,
Categories: order.categories,
ItemNames: order.items.map(i => i.name),
Items: order.items.map(i => ({
ProductID: i.productId,
ProductName: i.name,
Quantity: i.quantity,
Price: i.price,
ImageURL: i.image,
ProductURL: i.url,
})),
BillingAddress: order.billingAddress,
ShippingAddress: order.shippingAddress,
},
value: order.total,
uniqueId: order.id, // Prevent duplicate orders
});
// Fulfilled Order
await trackEvent({
email: customer.email,
eventName: "Fulfilled Order",
properties: {
OrderId: order.id,
TrackingNumber: fulfillment.trackingNumber,
TrackingURL: fulfillment.trackingUrl,
Carrier: fulfillment.carrier,
},
});
// Cancelled Order
await trackEvent({
email: customer.email,
eventName: "Cancelled Order",
properties: {
OrderId: order.id,
Reason: cancellation.reason,
},
value: -order.total, // Negative value for refunds
});
Client-Side Tracking (JavaScript)
<!-- Add to your site -->
<script async src="https://static.klaviyo.com/onsite/js/klaviyo.js?company_id=XXXXXX"></script>
<script>
// Identify user
klaviyo.identify({
email: "customer@example.com",
first_name: "John",
last_name: "Doe",
});
// Track event
klaviyo.track("Viewed Product", {
ProductID: "prod_123",
ProductName: "Blue T-Shirt",
Price: 29.99,
});
// Track with value
klaviyo.track("Added to Cart", {
ProductID: "prod_123",
ProductName: "Blue T-Shirt",
Price: 29.99,
$value: 29.99, // Revenue tracking
});
</script>
Lists & Segments
Add Profile to List
async function addToList(listId: string, emails: string[]) {
return klaviyoRequest(`/lists/${listId}/relationships/profiles`, "POST", {
data: emails.map(email => ({
type: "profile",
attributes: { email },
})),
});
}
// By profile ID
async function addProfileToList(listId: string, profileId: string) {
return klaviyoRequest(`/lists/${listId}/relationships/profiles`, "POST", {
data: [{ type: "profile", id: profileId }],
});
}
Remove from List
async function removeFromList(listId: string, profileId: string) {
return klaviyoRequest(
`/lists/${listId}/relationships/profiles`,
"DELETE",
{
data: [{ type: "profile", id: profileId }],
}
);
}
Get List Members
async function getListMembers(listId: string, cursor?: string) {
const params = new URLSearchParams({
"page[size]": "100",
});
if (cursor) {
params.set("page[cursor]", cursor);
}
return klaviyoRequest(`/lists/${listId}/profiles?${params}`);
}
Create List
async function createList(name: string) {
return klaviyoRequest("/lists", "POST", {
data: {
type: "list",
attributes: { name },
},
});
}
Campaigns
Get Campaigns
async function getCampaigns(status?: "draft" | "scheduled" | "sent") {
const params = new URLSearchParams();
if (status) {
params.set("filter", `equals(status,"${status}")`);
}
return klaviyoRequest(`/campaigns?${params}`);
}
Get Campaign Performance
async function getCampaignMetrics(campaignId: string) {
return klaviyoRequest(
`/campaign-recipient-estimations/${campaignId}`,
"GET"
);
}
Flows (Automations)
Get Flows
async function getFlows() {
return klaviyoRequest("/flows");
}
async function getFlowById(flowId: string) {
return klaviyoRequest(`/flows/${flowId}`);
}
Common Flow Triggers
| Flow Type | Trigger Event | |-----------|---------------| | Welcome Series | Added to List | | Abandoned Cart | Added to Cart + No Purchase | | Browse Abandon | Viewed Product + No Cart | | Post-Purchase | Placed Order | | Winback | No Order in X Days | | Review Request | Fulfilled Order |
Webhooks
Create Webhook
async function createWebhook(data: WebhookInput) {
return klaviyoRequest("/webhooks", "POST", {
data: {
type: "webhook",
attributes: {
name: data.name,
endpoint_url: data.url,
secret_key: data.secret,
topics: data.topics, // e.g., ["profile.created", "event.created"]
},
},
});
}
Webhook Topics
| Topic | Trigger |
|-------|---------|
| profile.created | New profile created |
| profile.updated | Profile properties changed |
| profile.merged | Profiles merged |
| event.created | New event tracked |
| list.member.added | Profile added to list |
| list.member.removed | Profile removed from list |
Verify Webhook Signature
import crypto from "crypto";
function verifyKlaviyoWebhook(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("base64");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express handler
app.post("/webhooks/klaviyo", (req, res) => {
const signature = req.headers["klaviyo-webhook-signature"] as string;
if (!verifyKlaviyoWebhook(JSON.stringify(req.body), signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}
const { type, data } = req.body;
switch (type) {
case "profile.created":
handleNewProfile(data);
break;
case "event.created":
handleNewEvent(data);
break;
}
res.status(200).json({ received: true });
});
Rate Limits
| Window | Limit | |--------|-------| | Burst | 75 requests/second | | Steady | 700 requests/minute |
Handle Rate Limiting
async function klaviyoRequestWithRetry(
endpoint: string,
method: "GET" | "POST" | "PATCH" | "DELETE" = "GET",
body?: object,
retries = 3
): Promise<any> {
for (let attempt = 0; attempt < retries; attempt++) {
const response = await fetch(`${KLAVIYO_BASE_URL}${endpoint}`, {
method,
headers: {
Authorization: `Klaviyo-API-Key ${process.env.KLAVIYO_PRIVATE_KEY}`,
"Content-Type": "application/json",
revision: "2024-10-15",
},
body: body ? JSON.stringify(body) : undefined,
});
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get("Retry-After") || "5");
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
if (!response.ok) {
throw new Error(`Klaviyo error: ${response.status}`);
}
return response.json();
}
throw new Error("Max retries exceeded");
}
Pagination
async function getAllProfiles() {
const profiles = [];
let cursor: string | undefined;
do {
const params = new URLSearchParams({ "page[size]": "100" });
if (cursor) {
params.set("page[cursor]", cursor);
}
const response = await klaviyoRequest(`/profiles?${params}`);
profiles.push(...response.data);
cursor = response.links?.next
? new URL(response.links.next).searchParams.get("page[cursor]")
: undefined;
} while (cursor);
return profiles;
}
Filtering & Sorting
// Filter by date
const recentEvents = await klaviyoRequest(
`/events?filter=greater-than(datetime,2024-01-01T00:00:00Z)`
);
// Filter by property
const vipProfiles = await klaviyoRequest(
`/profiles?filter=equals(properties.vip_status,true)`
);
// Multiple filters (AND)
const filtered = await klaviyoRequest(
`/profiles?filter=and(equals(properties.plan,"pro"),greater-than(properties.ltv,1000))`
);
// Sorting
const sorted = await klaviyoRequest(
`/profiles?sort=-created` // Descending by created date
);
// Sparse fieldsets (only return specific fields)
const sparse = await klaviyoRequest(
`/profiles?fields[profile]=email,first_name,properties`
);
Integration Patterns
E-Commerce Order Sync
// After order is placed
async function syncOrderToKlaviyo(order: Order) {
// 1. Upsert customer profile
await upsertProfile({
email: order.customerEmail,
firstName: order.customerFirstName,
lastName: order.customerLastName,
phone: order.customerPhone,
});
// 2. Update lifetime metrics
await updateProfileProperties(
await getProfileIdByEmail(order.customerEmail),
{
last_order_date: new Date().toISOString(),
total_orders: order.customerOrderCount,
lifetime_value: order.customerLifetimeValue,
}
);
// 3. Track order event
await trackEvent({
email: order.customerEmail,
eventName: "Placed Order",
properties: {
OrderId: order.id,
Items: order.items,
// ... other properties
},
value: order.total,
uniqueId: order.id,
});
}
Subscription Status Sync
// When subscription changes
async function syncSubscriptionStatus(user: User, status: string) {
await updateProfileProperties(user.klaviyoProfileId, {
subscription_status: status,
subscription_plan: user.plan,
subscription_updated_at: new Date().toISOString(),
});
await trackEvent({
email: user.email,
eventName: `Subscription ${status}`,
properties: {
plan: user.plan,
mrr: user.mrr,
},
value: status === "cancelled" ? -user.mrr : user.mrr,
});
}
Environment Variables
# .env
KLAVIYO_PRIVATE_KEY=pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
KLAVIYO_PUBLIC_KEY=XXXXXX
KLAVIYO_WEBHOOK_SECRET=your_webhook_secret
Add to credentials.md:
'KLAVIYO_PRIVATE_KEY': r'pk_[a-f0-9]{32}',
'KLAVIYO_PUBLIC_KEY': r'[A-Z0-9]{6}',
Checklist
Setup
- [ ] Klaviyo account created
- [ ] Private API key generated
- [ ] Public API key noted (company ID)
- [ ] API revision set in headers
Integration
- [ ] Profile sync on signup/update
- [ ] Key events tracked (view, cart, order)
- [ ] Order events include Items array
- [ ] Revenue tracked with $value
- [ ] Unique IDs for deduplication
Testing
- [ ] Test profile creation
- [ ] Test event tracking
- [ ] Verify events in Klaviyo dashboard
- [ ] Test webhook delivery
- [ ] Test rate limit handling
Anti-Patterns
- Missing email/phone - Every profile needs at least one identifier
- Duplicate events - Use unique_id for orders/transactions
- Missing Items array - Required for product recommendations
- Client-side only - Server-side tracking is more reliable
- Ignoring rate limits - Implement exponential backoff
- Hardcoded API keys - Use environment variables
- Missing revenue tracking - Include $value for ROI attribution