From klaviyo-pack
Optimizes Klaviyo costs by auditing active profiles, identifying unengaged contacts, and monitoring usage with klaviyo-api SDK.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin klaviyo-packThis skill is limited to using the following tools:
Optimize Klaviyo costs through active profile management, list hygiene, event sampling, and API usage monitoring. Klaviyo bills primarily by **active profiles** and **message volume**, not API calls.
Integrates Klaviyo email/SMS marketing API: manage profiles, track events, build flows, and segment customers. Use for e-commerce marketing features with Node.js, Python SDKs or direct HTTP.
Optimizes Klaviyo API performance using sparse fieldsets, LRU caching, batching, pagination, and rate limit handling for faster responses and higher throughput.
Optimizes Customer.io costs via profile audits, inactive user suppression/deletion, event deduplication, and usage monitoring with TypeScript scripts.
Share bugs, ideas, or general feedback.
Optimize Klaviyo costs through active profile management, list hygiene, event sampling, and API usage monitoring. Klaviyo bills primarily by active profiles and message volume, not API calls.
klaviyo-api SDK for programmatic managementKlaviyo bills based on active profiles (contacts who have received or been targeted by marketing), not API requests.
| Component | How It's Billed | Cost Driver |
|---|---|---|
| Per active profile tier | Number of marketable profiles | |
| SMS | Per message sent + carrier fees | Message volume |
| Push | Included with email plan | N/A |
| API calls | Free (rate limited, not billed) | N/A |
| Reviews | Per request volume | Review request sends |
| Active Profiles | Monthly Cost |
|---|---|
| 0 - 250 | Free |
| 251 - 500 | $20/mo |
| 501 - 1,000 | $30/mo |
| 1,001 - 1,500 | $45/mo |
| 1,501 - 5,000 | $60-$100/mo |
| 5,001 - 10,000 | $100-$150/mo |
| 10,001 - 25,000 | $150-$375/mo |
| 25,001+ | Custom pricing |
Key insight: Reducing active profiles has the biggest cost impact. Cleaning suppressed/unengaged contacts directly reduces your bill.
import { ApiKeySession, ProfilesApi, SegmentsApi } from 'klaviyo-api';
const session = new ApiKeySession(process.env.KLAVIYO_PRIVATE_KEY!);
const profilesApi = new ProfilesApi(session);
// Count total profiles
let totalProfiles = 0;
let cursor: string | undefined;
do {
const response = await profilesApi.getProfiles({
pageCursor: cursor,
fieldsProfile: ['email'], // Minimal fields for speed
});
totalProfiles += response.body.data.length;
const nextLink = response.body.links?.next;
cursor = nextLink ? new URL(nextLink).searchParams.get('page[cursor]') || undefined : undefined;
} while (cursor);
console.log(`Total profiles: ${totalProfiles}`);
// Find profiles that haven't opened/clicked in 180+ days
// Create a segment in Klaviyo for this, then query it
const segmentsApi = new SegmentsApi(session);
const segments = await segmentsApi.getSegments({
filter: 'equals(name,"Unengaged 180+ Days")',
});
if (segments.body.data.length > 0) {
const segmentId = segments.body.data[0].id;
const unengaged = await segmentsApi.getSegmentProfiles({
id: segmentId,
fieldsProfile: ['email', 'created'],
});
console.log(`Unengaged profiles: ${unengaged.body.data.length}+`);
}
// Move unengaged profiles to a suppressed list (removes from active count)
import { ListsApi, ListEnum, ProfileEnum } from 'klaviyo-api';
const listsApi = new ListsApi(session);
// Option 1: Unsubscribe (profile stays but isn't marketable = not billed)
await profilesApi.unsubscribeProfiles({
data: {
type: 'profile-subscription-bulk-delete-job',
attributes: {
profiles: {
data: unengagedEmails.map(email => ({
type: ProfileEnum.Profile,
attributes: {
email,
subscriptions: {
email: { marketing: { consent: 'UNSUBSCRIBED' } },
},
},
})),
},
},
relationships: {
list: { data: { type: ListEnum.List, id: 'MAIN_LIST_ID' } },
},
},
});
// Option 2: Suppress via profile update (add to global suppression)
for (const email of unengagedEmails) {
await profilesApi.createOrUpdateProfile({
data: {
type: ProfileEnum.Profile,
attributes: {
email,
properties: { suppressedAt: new Date().toISOString(), suppressReason: 'unengaged-180d' },
},
},
});
}
// Not all events need to be tracked -- sample non-critical ones
function shouldTrackEvent(eventName: string, samplingRates: Record<string, number>): boolean {
const rate = samplingRates[eventName] ?? 1.0; // Default: track everything
return Math.random() < rate;
}
const samplingConfig = {
'Placed Order': 1.0, // Always track (revenue attribution)
'Started Checkout': 1.0, // Always track (cart abandonment)
'Viewed Product': 0.25, // 25% sample (high volume, less critical)
'Page View': 0.1, // 10% sample (very high volume)
};
// Before tracking
if (shouldTrackEvent('Viewed Product', samplingConfig)) {
await eventsApi.createEvent({ /* ... */ });
}
// Track API call volume to detect runaway processes
class KlaviyoUsageTracker {
private callCount = 0;
private readonly startTime = Date.now();
track(): void {
this.callCount++;
// Warn if approaching steady rate limit
const elapsedMinutes = (Date.now() - this.startTime) / 60000;
const ratePerMinute = this.callCount / Math.max(elapsedMinutes, 1);
if (ratePerMinute > 500) {
console.warn(`[Klaviyo] High API rate: ${Math.round(ratePerMinute)} req/min (limit: 700)`);
}
}
getStats(): { totalCalls: number; ratePerMinute: number } {
const elapsedMinutes = (Date.now() - this.startTime) / 60000;
return {
totalCalls: this.callCount,
ratePerMinute: Math.round(this.callCount / Math.max(elapsedMinutes, 1)),
};
}
}
export const usageTracker = new KlaviyoUsageTracker();
| Issue | Cause | Solution |
|---|---|---|
| Unexpected bill increase | Unengaged profiles grew | Run suppression script |
| SMS costs spiking | Flow sending to full list | Add engaged-only segment filter |
| Duplicate profiles | Multiple identify calls | Merge duplicates, use createOrUpdateProfile |
| API rate limits hit | Bulk operations | Use queue with concurrency control |
For architecture patterns, see klaviyo-reference-architecture.