klaviyo-webhooks-events
Implement Klaviyo webhooks with HMAC-SHA256 signature verification and event handling. Use when setting up webhook endpoints, handling Klaviyo event notifications, or creating event-driven integrations with Klaviyo. Trigger with phrases like "klaviyo webhook", "klaviyo events", "klaviyo webhook signature", "handle klaviyo events", "klaviyo notifications".
What this skill does
# Klaviyo Webhooks & Events
## Overview
Set up Klaviyo webhooks with HMAC-SHA256 signature verification, event routing, idempotency handling, and the Webhooks API for programmatic subscription management.
## Prerequisites
- Klaviyo account with webhooks enabled
- HTTPS endpoint accessible from internet
- API key with scopes: `webhooks:read`, `webhooks:write`
- Redis or database for idempotency (recommended)
## Klaviyo Webhook Architecture
Klaviyo 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 |
## Instructions
### Step 1: Create a Webhook via API
```typescript
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
```
### Step 2: Signature Verification
```typescript
// 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;
}
}
```
### Step 3: Express Webhook Handler
```typescript
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' });
}
}
);
```
### Step 4: Event Router
```typescript
// 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);
}
```
### Step 5: Idempotency with Redis
```typescript
// 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());
}
```
### Step 6: List and Manage Webhooks
```typescript
// 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' });
```
## Testing Webhooks Locally
```bash
# 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
```
## Error Handling
| 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 |
## Resources
- [Webhooks API Overview](https://developers.klaviyo.com/en/reference/webhooks_api_overview)
- [Working with System Webhooks](https://developers.klaviyo.com/en/docs/working_with_system_webhooks)
- [Understanding Webhook Status Codes](https://developers.klaviyo.com/en/docs/understanding_webhook_status_codes)
## Next Steps
For performance optimization, see `klaviyo-performance-tuning`.
Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.