Klaviyo Core Workflow A -- Profiles, Lists & Subscriptions
Overview
Primary money-path workflow: create/update profiles, manage lists, and subscribe contacts for email and SMS marketing via the klaviyo-api SDK.
Prerequisites
- Completed
klaviyo-install-authsetup - API key with scopes:
profiles:read,profiles:write,lists:read,lists:write
Instructions
Step 1: Create or Update a Profile
import { ApiKeySession, ProfilesApi, ProfileEnum } from 'klaviyo-api';
const session = new ApiKeySession(process.env.KLAVIYO_PRIVATE_KEY!);
const profilesApi = new ProfilesApi(session);
// Create a new profile (409 if email already exists)
const newProfile = await profilesApi.createProfile({
data: {
type: ProfileEnum.Profile,
attributes: {
email: 'customer@example.com',
firstName: 'Jane',
lastName: 'Doe',
phoneNumber: '+15551234567',
location: {
city: 'Atlanta',
region: 'GA',
country: 'US',
zip: '30309',
},
properties: {
plan: 'pro',
signupSource: 'website',
lifetime_value: 250.00,
},
},
},
});
console.log('Profile ID:', newProfile.body.data.id);
// Create OR update (upsert) -- preferred for syncing
const upserted = await profilesApi.createOrUpdateProfile({
data: {
type: ProfileEnum.Profile,
attributes: {
email: 'customer@example.com',
firstName: 'Jane',
lastName: 'Doe-Smith', // Updated last name
properties: {
plan: 'enterprise', // Updated plan
lastLogin: new Date().toISOString(),
},
},
},
});
console.log('Upserted profile:', upserted.body.data.id);
Step 2: Create a List
import { ListsApi, ListEnum } from 'klaviyo-api';
const listsApi = new ListsApi(session);
// Create a new list
const list = await listsApi.createList({
data: {
type: ListEnum.List,
attributes: {
name: 'Newsletter Subscribers',
},
},
});
const listId = list.body.data.id;
console.log('List created:', listId);
// Get all lists
const allLists = await listsApi.getLists();
for (const l of allLists.body.data) {
console.log(`${l.attributes.name} (${l.id})`);
}
Step 3: Add Profiles to a List
// Add existing profiles to a list (does NOT change subscription status)
await listsApi.createListRelationships({
id: listId,
relationshipType: 'profiles' as any,
body: {
data: [
{ type: ProfileEnum.Profile, id: 'PROFILE_ID_1' },
{ type: ProfileEnum.Profile, id: 'PROFILE_ID_2' },
],
},
});
Step 4: Subscribe Profiles (Email + SMS Consent)
// Subscribe profiles to a list WITH marketing consent
// This is the correct way to add subscribers (not just list members)
await profilesApi.subscribeProfiles({
data: {
type: 'profile-subscription-bulk-create-job',
attributes: {
profiles: {
data: [
{
type: ProfileEnum.Profile,
attributes: {
email: 'subscriber@example.com',
phoneNumber: '+15559876543',
subscriptions: {
email: {
marketing: {
consent: 'SUBSCRIBED',
consentTimestamp: new Date().toISOString(),
},
},
sms: {
marketing: {
consent: 'SUBSCRIBED',
consentTimestamp: new Date().toISOString(),
},
},
},
},
},
],
},
},
relationships: {
list: {
data: {
type: ListEnum.List,
id: listId,
},
},
},
},
});
console.log('Profile subscribed to email + SMS');
Step 5: Query Profiles with Filters
// Filter profiles by custom property
const proUsers = await profilesApi.getProfiles({
filter: 'equals(properties.plan,"pro")',
sort: '-created', // Newest first
});
// Filter by date range
const recentProfiles = await profilesApi.getProfiles({
filter: 'greater-than(created,2024-01-01T00:00:00Z)',
});
// Filter by email domain
const gmailUsers = await profilesApi.getProfiles({
filter: 'contains(email,"@gmail.com")',
});
// Get profiles for a specific list
const listMembers = await listsApi.getListProfiles({ id: listId });
for (const member of listMembers.body.data) {
console.log(member.attributes.email);
}
Step 6: Bulk Profile Import
// Batch create/update up to 100 profiles at a time
const profiles = customers.map(c => ({
type: ProfileEnum.Profile as const,
attributes: {
email: c.email,
firstName: c.firstName,
lastName: c.lastName,
properties: { source: 'bulk-import', importedAt: new Date().toISOString() },
},
}));
// Process in batches of 100
for (let i = 0; i < profiles.length; i += 100) {
const batch = profiles.slice(i, i + 100);
await Promise.all(
batch.map(p => profilesApi.createOrUpdateProfile({ data: p }))
);
console.log(`Imported ${Math.min(i + 100, profiles.length)}/${profiles.length}`);
}
Output
- Profiles created/updated in Klaviyo
- Lists created and populated
- Subscribers opted in with consent timestamps
- Queryable customer data for segmentation
Error Handling
| Error | Status | Cause | Solution |
|-------|--------|-------|----------|
| Duplicate profile | 409 | Email exists | Use createOrUpdateProfile (upsert) |
| Invalid phone | 400 | Wrong format | Use E.164 format: +15551234567 |
| List not found | 404 | Wrong list ID | Verify list ID via getLists() |
| Missing consent | 400 | No consent timestamp | Always include consentTimestamp |
| Rate limited | 429 | >75 req/s burst | See klaviyo-rate-limits |
Resources
Next Steps
For event tracking and campaign triggers, see klaviyo-core-workflow-b.