From klaviyo-pack
Implements Klaviyo webhooks with HMAC-SHA256 signature verification, event handling, idempotency, and API subscriptions for profile/list/segment/campaign/flow events.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin klaviyo-packThis skill is limited to using the following tools:
Set up Klaviyo webhooks with HMAC-SHA256 signature verification, event routing, idempotency handling, and the Webhooks API for programmatic subscription management.
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.
Installs Klaviyo Node.js SDK, configures private API key auth in .env, initializes resource-specific API clients, and verifies connection. Supports Python SDK.
Handles Instantly.ai v2 webhook events for email campaigns (sent, opened, clicked, replied, bounced) and lead updates. Use for webhook endpoints or CRM sync pipelines.
Share bugs, ideas, or general feedback.
Set up Klaviyo webhooks with HMAC-SHA256 signature verification, event routing, idempotency handling, and the Webhooks API for programmatic subscription management.
webhooks:read, webhooks:writeKlaviyo webhooks fire when specific topics occur in your account. Each webhook is signed with a secret key using HMAC-SHA256.
| Topic Category | Example Topics |
|---|---|
| Profile | profile.created, profile.updated, profile.deleted |
| List | list.member.added, list.member.removed |
| Segment | segment.member.added, segment.member.removed |
| Campaign | campaign.sent, campaign.delivered |
| Flow | flow.triggered, flow.message.sent |
| Event | Custom metric events |
import { ApiKeySession, WebhooksApi } from 'klaviyo-api';
const session = new ApiKeySession(process.env.KLAVIYO_PRIVATE_KEY!);
const webhooksApi = new WebhooksApi(session);
// Create a webhook subscription
const webhook = await webhooksApi.createWebhook({
data: {
type: 'webhook',
attributes: {
name: 'Profile Updates',
endpointUrl: 'https://your-app.com/webhooks/klaviyo',
// The secret used for HMAC-SHA256 signing
// Store this as KLAVIYO_WEBHOOK_SIGNING_SECRET
description: 'Receives profile create/update events',
},
relationships: {
webhookTopics: {
data: [
{ type: 'webhook-topic', id: 'profile.created' },
{ type: 'webhook-topic', id: 'profile.updated' },
],
},
},
},
});
console.log('Webhook ID:', webhook.body.data.id);
// Save the signing secret from the response
// src/klaviyo/webhook-verify.ts
import crypto from 'crypto';
/**
* Verify Klaviyo webhook HMAC-SHA256 signature.
* Klaviyo sends the signature in the webhook-signature header.
*/
export function verifyWebhookSignature(
rawBody: Buffer | string,
signature: string,
secret: string
): boolean {
if (!signature || !secret) return false;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(typeof rawBody === 'string' ? rawBody : rawBody.toString())
.digest('base64');
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch {
return false;
}
}
import express from 'express';
import { verifyWebhookSignature } from './klaviyo/webhook-verify';
const app = express();
// CRITICAL: Use raw body parser for signature verification
app.post('/webhooks/klaviyo',
express.raw({ type: 'application/json' }),
async (req, res) => {
// 1. Verify signature
const signature = req.headers['webhook-signature'] as string;
if (!verifyWebhookSignature(
req.body,
signature,
process.env.KLAVIYO_WEBHOOK_SIGNING_SECRET!
)) {
console.warn('[Webhook] Invalid signature rejected');
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Parse event
const event = JSON.parse(req.body.toString());
// 3. Check idempotency (prevent duplicate processing)
const eventId = event.id || event.data?.id;
if (eventId && await isAlreadyProcessed(eventId)) {
return res.status(200).json({ status: 'already_processed' });
}
// 4. Route to handler
try {
await routeWebhookEvent(event);
if (eventId) await markProcessed(eventId);
res.status(200).json({ received: true });
} catch (error) {
console.error('[Webhook] Processing failed:', error);
res.status(500).json({ error: 'Processing failed' });
}
}
);
// src/klaviyo/webhook-router.ts
type WebhookHandler = (data: any) => Promise<void>;
const handlers: Record<string, WebhookHandler> = {
'profile.created': async (data) => {
const profile = data.attributes;
console.log(`New profile: ${profile.email}`);
// Sync to your database, trigger welcome flow, etc.
await db.users.upsert({
email: profile.email,
firstName: profile.firstName,
klaviyoProfileId: data.id,
});
},
'profile.updated': async (data) => {
const profile = data.attributes;
console.log(`Updated profile: ${profile.email}`);
await db.users.update({
where: { klaviyoProfileId: data.id },
data: { firstName: profile.firstName, lastName: profile.lastName },
});
},
'list.member.added': async (data) => {
console.log(`Profile ${data.relationships.profile.data.id} added to list ${data.relationships.list.data.id}`);
},
'campaign.sent': async (data) => {
console.log(`Campaign sent: ${data.attributes.name}`);
await analytics.track('campaign_sent', { campaignId: data.id });
},
};
export async function routeWebhookEvent(event: any): Promise<void> {
const topic = event.type || event.topic;
const handler = handlers[topic];
if (!handler) {
console.log(`[Webhook] Unhandled topic: ${topic}`);
return;
}
await handler(event.data || event);
}
// src/klaviyo/webhook-idempotency.ts
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
const TTL_SECONDS = 86400 * 7; // 7 days
export async function isAlreadyProcessed(eventId: string): Promise<boolean> {
const key = `klaviyo:webhook:${eventId}`;
return (await redis.exists(key)) === 1;
}
export async function markProcessed(eventId: string): Promise<void> {
const key = `klaviyo:webhook:${eventId}`;
await redis.setex(key, TTL_SECONDS, new Date().toISOString());
}
// List all webhooks
const webhooks = await webhooksApi.getWebhooks();
for (const wh of webhooks.body.data) {
console.log(`${wh.attributes.name}: ${wh.attributes.endpointUrl}`);
}
// Get webhook topics (available event types)
const topics = await webhooksApi.getWebhookTopics();
for (const topic of topics.body.data) {
console.log(`Topic: ${topic.id} - ${topic.attributes.description}`);
}
// Delete a webhook
await webhooksApi.deleteWebhook({ id: 'WEBHOOK_ID' });
# 1. Start your app
npm run dev # localhost:3000
# 2. Expose via ngrok
ngrok http 3000
# 3. Register ngrok URL as webhook endpoint in Klaviyo
# https://abc123.ngrok.io/webhooks/klaviyo
# 4. Trigger an event (e.g., create a profile) and watch your logs
| Issue | Cause | Solution |
|---|---|---|
| Invalid signature | Wrong signing secret | Verify secret matches webhook creation response |
| Duplicate events | No idempotency | Track event IDs in Redis/DB |
| Webhook timeout | Slow processing | Return 200 immediately, process async |
| Missing events | Wrong topics subscribed | Check webhook topic subscriptions |
| Body parse error | Using JSON body parser | Must use express.raw() for signature verification |
For performance optimization, see klaviyo-performance-tuning.