Agent Skills: Klaviyo Data Handling

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/klaviyo-data-handling

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-data-handling

Skill Files

Browse the full folder contents for klaviyo-data-handling.

Download Skill

Loading file tree…

plugins/saas-packs/klaviyo-pack/skills/klaviyo-data-handling/SKILL.md

Skill Metadata

Name
klaviyo-data-handling
Description
|

Klaviyo Data Handling

Overview

Handle profile data, PII, and privacy compliance with Klaviyo's Data Privacy API, GDPR right-to-deletion, CCPA requests, and safe logging patterns.

Prerequisites

  • klaviyo-api SDK installed
  • API key with data-privacy:write scope (for deletion requests)
  • Understanding of GDPR/CCPA requirements
  • Audit logging infrastructure

Klaviyo Data Privacy API

Klaviyo provides a dedicated Data Privacy API for GDPR/CCPA profile deletion. When you delete a profile via this API, Klaviyo performs a full GDPR erasure -- the profile is permanently removed and cannot be recovered.

Instructions

Step 1: GDPR Profile Deletion (Right to Erasure)

import { ApiKeySession, DataPrivacyApi } from 'klaviyo-api';

const session = new ApiKeySession(process.env.KLAVIYO_PRIVATE_KEY!);
const dataPrivacyApi = new DataPrivacyApi(session);

/**
 * Request profile deletion via Klaviyo's Data Privacy API.
 * Accepts ONE identifier: email, phone_number, or profile ID.
 * Providing multiple identifiers returns an error.
 *
 * WARNING: This is irreversible. Profile is permanently erased.
 */
async function requestProfileDeletion(identifier: {
  email?: string;
  phoneNumber?: string;
  profileId?: string;
}): Promise<void> {
  // Build the profile identifier (only ONE allowed)
  const profileData: any = { type: 'profile' };

  if (identifier.email) {
    profileData.attributes = { email: identifier.email };
  } else if (identifier.phoneNumber) {
    profileData.attributes = { phone_number: identifier.phoneNumber };
  } else if (identifier.profileId) {
    profileData.id = identifier.profileId;
  } else {
    throw new Error('Must provide exactly one identifier: email, phoneNumber, or profileId');
  }

  await dataPrivacyApi.requestProfileDeletion({
    data: {
      type: 'data-privacy-deletion-job',
      attributes: {
        profile: { data: profileData },
      },
    },
  });

  // Audit log (required for compliance)
  await auditLog({
    action: 'GDPR_DELETION_REQUESTED',
    identifier: identifier.email || identifier.phoneNumber || identifier.profileId!,
    service: 'klaviyo',
    timestamp: new Date().toISOString(),
  });

  console.log(`Deletion requested for ${JSON.stringify(identifier)}`);
}

// Usage
await requestProfileDeletion({ email: 'user-wants-deletion@example.com' });

Step 2: Data Subject Access Request (DSAR)

import { ProfilesApi, EventsApi, ListsApi } from 'klaviyo-api';

/**
 * Export all Klaviyo data for a given profile (GDPR Article 15).
 * Returns all profile attributes, event history, and list memberships.
 */
async function exportProfileData(email: string): Promise<{
  profile: any;
  events: any[];
  lists: any[];
}> {
  const profilesApi = new ProfilesApi(session);
  const eventsApi = new EventsApi(session);

  // 1. Get profile
  const profiles = await profilesApi.getProfiles({
    filter: `equals(email,"${email}")`,
  });
  const profile = profiles.body.data[0];
  if (!profile) throw new Error(`No profile found for ${email}`);

  // 2. Get profile's events
  const events = await eventsApi.getEvents({
    filter: `equals(profile_id,"${profile.id}")`,
    sort: '-datetime',
  });

  // 3. Get profile's list memberships
  const profileLists = await profilesApi.getProfileLists({ id: profile.id });

  return {
    profile: {
      id: profile.id,
      ...profile.attributes,
    },
    events: events.body.data.map(e => ({
      metric: e.attributes.metricId,
      datetime: e.attributes.datetime,
      properties: e.attributes.eventProperties,
    })),
    lists: profileLists.body.data.map(l => ({
      id: l.id,
      name: l.attributes.name,
    })),
  };
}

Step 3: PII Detection and Redaction in Logs

// src/klaviyo/pii.ts

const PII_PATTERNS = [
  { type: 'email', regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g },
  { type: 'phone', regex: /\+?\d{10,15}/g },
  { type: 'api_key', regex: /pk_[a-zA-Z0-9]{20,}/g },
];

export function redactPII(text: string): string {
  let redacted = text;
  for (const pattern of PII_PATTERNS) {
    redacted = redacted.replace(pattern.regex, `[REDACTED:${pattern.type}]`);
  }
  return redacted;
}

export function redactObject(data: Record<string, any>): Record<string, any> {
  const sensitiveFields = ['email', 'phoneNumber', 'phone_number', 'firstName', 'lastName', 'apiKey'];
  const redacted = { ...data };

  for (const field of sensitiveFields) {
    if (redacted[field]) {
      redacted[field] = typeof redacted[field] === 'string'
        ? `${redacted[field].substring(0, 3)}***`
        : '[REDACTED]';
    }
  }

  return redacted;
}

// Usage: safe logging of Klaviyo API responses
console.log('Profile data:', redactObject(profile.attributes));

Step 4: Consent Management

/**
 * Record consent and subscribe to marketing.
 * Always include consent timestamp for GDPR compliance.
 */
async function recordConsent(
  email: string,
  channels: { email?: boolean; sms?: boolean },
  listId: string,
  consentSource: string
): Promise<void> {
  const subscriptions: any = {};
  const now = new Date().toISOString();

  if (channels.email) {
    subscriptions.email = {
      marketing: { consent: 'SUBSCRIBED', consentTimestamp: now },
    };
  }
  if (channels.sms) {
    subscriptions.sms = {
      marketing: { consent: 'SUBSCRIBED', consentTimestamp: now },
    };
  }

  await profilesApi.subscribeProfiles({
    data: {
      type: 'profile-subscription-bulk-create-job',
      attributes: {
        profiles: {
          data: [{
            type: 'profile' as any,
            attributes: {
              email,
              subscriptions,
            },
          }],
        },
        historicalImport: false,  // Set true for pre-existing consent
      },
      relationships: {
        list: { data: { type: 'list', id: listId } },
      },
    },
  });

  await auditLog({
    action: 'CONSENT_RECORDED',
    identifier: email,
    channels: Object.keys(channels).filter(c => channels[c as keyof typeof channels]),
    source: consentSource,
    timestamp: now,
  });
}

Step 5: Audit Logging

// src/klaviyo/audit.ts

interface AuditEntry {
  action: string;
  identifier: string;
  service: string;
  timestamp: string;
  details?: Record<string, any>;
}

async function auditLog(entry: AuditEntry): Promise<void> {
  // Write to your audit database (must be retained per GDPR)
  await db.auditLog.create({
    data: {
      ...entry,
      retainUntil: new Date(Date.now() + 7 * 365 * 24 * 60 * 60 * 1000), // 7 years
    },
  });

  console.log(`[AUDIT] ${entry.action}: ${entry.identifier} at ${entry.timestamp}`);
}

Data Classification for Klaviyo

| Data Category | Examples in Klaviyo | Handling | |--------------|---------------------|----------| | PII | email, phoneNumber, firstName, lastName | Redact in logs, encrypt at rest | | Sensitive | API keys, webhook secrets | Never log, rotate quarterly | | Behavioral | Events, page views, purchases | Anonymize where possible | | Marketing | List memberships, consent status | Audit trail required | | Derived | Segments, predictive analytics | No special handling |

Error Handling

| Issue | Cause | Solution | |-------|-------|----------| | Deletion request fails | Missing data-privacy:write scope | Update API key scopes | | Multiple identifiers error | Providing email AND phone | Use exactly one identifier | | Profile not found for DSAR | Wrong email or already deleted | Search by ID or phone instead | | PII in error logs | Unredacted API responses | Wrap logger with redactObject() |

Resources

Next Steps

For enterprise access control, see klaviyo-enterprise-rbac.