Skip to main content
AI/MLjeremylongshore

klaviyo-sdk-patterns

'Apply production-ready Klaviyo SDK patterns for the klaviyo-api package.

Stars
2,267
Source
jeremylongshore/claude-code-plugins-plus-skills
Updated
2026-05-31
Slug
jeremylongshore--claude-code-plugins-plus-skills--klaviyo-sdk-patterns
View on GitHubRaw SKILL.md

// install — copy + paste into any project

mkdir -p .claude/skills && curl -fsSL https://raw.githubusercontent.com/jeremylongshore/claude-code-plugins-plus-skills/HEAD/plugins/saas-packs/klaviyo-pack/skills/klaviyo-sdk-patterns/SKILL.md -o .claude/skills/klaviyo-sdk-patterns.md

Drops the SKILL.md into .claude/skills/klaviyo-sdk-patterns.md. Works with Claude Code, Cursor, and any agent that loads SKILL.md files from .claude/skills/.

Klaviyo SDK Patterns

Overview

Production-ready patterns for the klaviyo-api Node.js SDK: singleton sessions, type-safe wrappers, retry logic, cursor pagination, and multi-tenant support. Read the target project's Klaviyo files, then Write or Edit the src/klaviyo/ modules below into place so every call goes through one consistent, retry-aware layer instead of ad-hoc new ApiKeySession(...) calls scattered across the codebase.

The six patterns are summarized here with the essential skeleton; the full, copy-paste implementation for all of them lives in references/implementation.md, and combined worked examples with expected output are in references/examples.md.

Prerequisites

  • klaviyo-api package installed in the target project.
  • The klaviyo-install-auth setup completed, so KLAVIYO_PRIVATE_KEY is available in the environment.
  • A TypeScript project with strict mode enabled — every pattern is typed.

Instructions

Step 1: Singleton session (the foundation)

Create one lazily-initialized ApiKeySession and reuse it everywhere. Read the key from the environment, fail fast if it is missing, and expose a reset hook for tests.

// src/klaviyo/session.ts
import { ApiKeySession } from 'klaviyo-api';

let _session: ApiKeySession | null = null;

export function getSession(apiKey?: string): ApiKeySession {
  if (!_session) {
    const key = apiKey || process.env.KLAVIYO_PRIVATE_KEY;
    if (!key) throw new Error('KLAVIYO_PRIVATE_KEY is required');
    _session = new ApiKeySession(key);
  }
  return _session;
}
export function resetSession(): void { _session = null; }

Steps 2-6: the rest of the layer

Each builds on the session singleton. Write the corresponding file from references/implementation.md:

  • Step 2 — Type-safe API wrapper (api.ts): lazy getters for all 11 API clients (Profiles, Events, Lists, …) so unused clients are never constructed.
  • Step 3 — Error wrapper (errors.ts): parseKlaviyoError normalizes the raw error and safeCall returns { data, error } instead of throwing.
  • Step 4 — Retry (retry.ts): withRetry retries only on 429/5xx, honoring Klaviyo's Retry-After header, else exponential backoff with jitter.
  • Step 5 — Pagination (pagination.ts): paginate turns any cursor-based list endpoint into an AsyncGenerator, extracting page[cursor] for you.
  • Step 6 — Multi-tenant factory (multi-tenant.ts): getApisForTenant caches one client set per tenant id, isolating each customer's API key.

Output

Applying this skill produces a src/klaviyo/ module set:

File Exports Purpose
session.ts getSession, resetSession One shared authenticated session
api.ts default apis Lazy, type-safe access to every API client
errors.ts parseKlaviyoError, safeCall Non-throwing typed error results
retry.ts withRetry Rate-limit/5xx retry honoring Retry-After
pagination.ts paginate Async iteration over cursor pages
multi-tenant.ts getApisForTenant Per-tenant client isolation

Callers then read as const { data, error } = await safeCall(() => apis.profiles.getProfiles(...)) instead of managing sessions and try/catch by hand.

SDK Conventions

Convention Example
Property casing firstName (not first_name)
Response access response.body.data (not response.data)
Payload structure { data: { type: 'profile', attributes: { ... } } }
Filter syntax equals(email,"user@example.com")
Sort syntax '-datetime' (descending), 'datetime' (ascending)
Include relations { include: ['lists'] }

Error Handling

Error Status Retryable Solution
Invalid API key 401 No Check KLAVIYO_PRIVATE_KEY
Missing scope 403 No Add required scope to API key
Validation error 400 No Fix request payload
Rate limited 429 Yes Honor Retry-After header
Server error 500/503 Yes Retry with backoff
Conflict 409 No Resource already exists; use update

Examples

A quick taste — wrap any call so a failure returns a typed error instead of throwing:

import apis from './klaviyo/api';
import { safeCall } from './klaviyo/errors';

const { data, error } = await safeCall(
  () => apis.profiles.getProfiles({ pageSize: 20 }),
  'list profiles',
);
if (error) console.error(`Failed (${error.status}):`, error.errors[0].detail);
else console.log(`Fetched ${data!.body.data.length} profiles`);

Full worked examples — retrying a rate-limited write, paginating every profile, and serving two tenants from one process, each with expected output — are in references/examples.md.

Resources

Next Steps

Once the src/klaviyo/ layer is in place, apply the patterns in klaviyo-core-workflow-a for profile and list management — those workflows assume apis, safeCall, withRetry, and paginate already exist.