Agent Skills: Klaviyo SDK Patterns

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/klaviyo-sdk-patterns

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/klaviyo-pack/skills/klaviyo-sdk-patterns

Skill Files

Browse the full folder contents for klaviyo-sdk-patterns.

Download Skill

Loading file tree…

plugins/saas-packs/klaviyo-pack/skills/klaviyo-sdk-patterns/SKILL.md

Skill Metadata

Name
klaviyo-sdk-patterns
Description
|

Klaviyo SDK Patterns

Overview

Production-ready patterns for the klaviyo-api Node.js SDK: singleton sessions, type-safe wrappers, retry logic, pagination, and multi-tenant support.

Prerequisites

  • klaviyo-api package installed
  • Completed klaviyo-install-auth setup
  • TypeScript project with strict mode

Instructions

Step 1: Singleton Session Pattern

// 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;
}

// For testing: reset the singleton
export function resetSession(): void {
  _session = null;
}

Step 2: Type-Safe API Wrapper

// src/klaviyo/api.ts
import {
  ApiKeySession,
  ProfilesApi,
  EventsApi,
  ListsApi,
  SegmentsApi,
  CampaignsApi,
  FlowsApi,
  MetricsApi,
  TemplatesApi,
  CatalogsApi,
  DataPrivacyApi,
  WebhooksApi,
} from 'klaviyo-api';
import { getSession } from './session';

// Lazy-initialized API clients -- avoids creating unused clients
const apis = {
  get profiles() { return new ProfilesApi(getSession()); },
  get events() { return new EventsApi(getSession()); },
  get lists() { return new ListsApi(getSession()); },
  get segments() { return new SegmentsApi(getSession()); },
  get campaigns() { return new CampaignsApi(getSession()); },
  get flows() { return new FlowsApi(getSession()); },
  get metrics() { return new MetricsApi(getSession()); },
  get templates() { return new TemplatesApi(getSession()); },
  get catalogs() { return new CatalogsApi(getSession()); },
  get dataPrivacy() { return new DataPrivacyApi(getSession()); },
  get webhooks() { return new WebhooksApi(getSession()); },
};

export default apis;

Step 3: Error Handling Wrapper

// src/klaviyo/errors.ts

export interface KlaviyoApiError {
  status: number;
  statusText: string;
  errors: Array<{ id: string; code: string; title: string; detail: string }>;
  retryAfter?: number;
}

export function parseKlaviyoError(error: any): KlaviyoApiError {
  return {
    status: error.status || 500,
    statusText: error.statusText || 'Unknown Error',
    errors: error.body?.errors || [{ id: '', code: 'unknown', title: 'Unknown', detail: error.message }],
    retryAfter: error.headers?.['retry-after'] ? parseInt(error.headers['retry-after']) : undefined,
  };
}

export async function safeCall<T>(
  operation: () => Promise<T>,
  context: string
): Promise<{ data: T | null; error: KlaviyoApiError | null }> {
  try {
    const data = await operation();
    return { data, error: null };
  } catch (err: any) {
    const parsed = parseKlaviyoError(err);
    console.error(`[Klaviyo] ${context} failed:`, {
      status: parsed.status,
      errors: parsed.errors.map(e => e.detail),
    });
    return { data: null, error: parsed };
  }
}

Step 4: Retry with Retry-After Header

// src/klaviyo/retry.ts

export async function withRetry<T>(
  operation: () => Promise<T>,
  options = { maxRetries: 3, baseDelayMs: 1000 }
): Promise<T> {
  for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error: any) {
      if (attempt === options.maxRetries) throw error;

      const status = error.status;
      // Only retry on 429 (rate limit) and 5xx (server errors)
      if (status !== 429 && (status < 500 || status >= 600)) throw error;

      // Honor Klaviyo's Retry-After header (seconds)
      const retryAfter = error.headers?.['retry-after'];
      const delay = retryAfter
        ? parseInt(retryAfter) * 1000
        : options.baseDelayMs * Math.pow(2, attempt) + Math.random() * 500;

      console.log(`[Klaviyo] Retry ${attempt + 1}/${options.maxRetries} in ${delay.toFixed(0)}ms`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error('Unreachable');
}

Step 5: Cursor-Based Pagination

// src/klaviyo/pagination.ts

/**
 * Auto-paginate any Klaviyo list endpoint.
 * Klaviyo uses cursor-based pagination with `page[cursor]` param.
 * Each page returns max 20 items (some endpoints allow up to 100).
 */
export async function* paginate<T>(
  fetcher: (pageCursor?: string) => Promise<{
    body: { data: T[]; links?: { next?: string } };
  }>
): AsyncGenerator<T> {
  let cursor: string | undefined;

  do {
    const response = await fetcher(cursor);
    for (const item of response.body.data) {
      yield item;
    }

    // Extract cursor from next link URL
    const nextLink = response.body.links?.next;
    if (nextLink) {
      const url = new URL(nextLink);
      cursor = url.searchParams.get('page[cursor]') || undefined;
    } else {
      cursor = undefined;
    }
  } while (cursor);
}

// Usage: iterate all profiles
// for await (const profile of paginate(cursor => profilesApi.getProfiles({ pageCursor: cursor }))) {
//   console.log(profile.attributes.email);
// }

Step 6: Multi-Tenant Factory

// src/klaviyo/multi-tenant.ts
import { ApiKeySession, ProfilesApi, EventsApi, ListsApi } from 'klaviyo-api';

interface TenantApis {
  profiles: ProfilesApi;
  events: EventsApi;
  lists: ListsApi;
}

const tenantCache = new Map<string, TenantApis>();

export function getApisForTenant(tenantId: string, apiKey: string): TenantApis {
  if (!tenantCache.has(tenantId)) {
    const session = new ApiKeySession(apiKey);
    tenantCache.set(tenantId, {
      profiles: new ProfilesApi(session),
      events: new EventsApi(session),
      lists: new ListsApi(session),
    });
  }
  return tenantCache.get(tenantId)!;
}

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 |

Resources

Next Steps

Apply patterns in klaviyo-core-workflow-a for profile and list management.