From klaviyo-pack
Apply Klaviyo security best practices for API key management and access control. Use when securing API keys, configuring OAuth scopes, implementing webhook signature verification, or auditing Klaviyo security configuration. Trigger with phrases like "klaviyo security", "klaviyo secrets", "secure klaviyo", "klaviyo API key security", "klaviyo OAuth".
npx claudepluginhub flight505/skill-forge --plugin klaviyo-packThis skill is limited to using the following tools:
Security best practices for Klaviyo: API key types, OAuth scopes, webhook HMAC-SHA256 signature verification, and secret rotation procedures.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Share bugs, ideas, or general feedback.
Security best practices for Klaviyo: API key types, OAuth scopes, webhook HMAC-SHA256 signature verification, and secret rotation procedures.
| Key Type | Format | Use Case | Sensitivity |
|---|---|---|---|
| Private API Key | pk_* (40+ chars) | Server-side REST API | CRITICAL -- never expose client-side |
| Public API Key | 6 alphanumeric chars | Client-side Track/Identify only | Low -- safe in browser JS |
Private keys authenticate via Authorization: Klaviyo-API-Key pk_*** header. Public keys pass as company_id query parameter.
# .env (NEVER commit)
KLAVIYO_PRIVATE_KEY=pk_***************************************
KLAVIYO_PUBLIC_KEY=UXxxXx
KLAVIYO_WEBHOOK_SIGNING_SECRET=whsec_*************************
# .gitignore -- mandatory entries
.env
.env.local
.env.*.local
// src/config/klaviyo.ts -- validated config loader
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing required env: ${name}`);
return value;
}
export const klaviyoConfig = {
privateKey: requireEnv('KLAVIYO_PRIVATE_KEY'),
publicKey: process.env.KLAVIYO_PUBLIC_KEY || '',
webhookSecret: process.env.KLAVIYO_WEBHOOK_SIGNING_SECRET || '',
};
Create separate API keys per environment with minimal scopes:
| Environment | Recommended Scopes | Rationale |
|---|---|---|
| Development | profiles:read, events:read, lists:read | Read-only exploration |
| Staging | profiles:read/write, events:write, lists:read/write | Full test coverage |
| Production | Exact scopes your app needs | Minimize blast radius |
| CI/CD | profiles:read, events:read | Smoke tests only |
# Use separate env vars per environment
KLAVIYO_PRIVATE_KEY_DEV=pk_dev_***
KLAVIYO_PRIVATE_KEY_STAGING=pk_staging_***
KLAVIYO_PRIVATE_KEY_PROD=pk_prod_***
Klaviyo signs webhook payloads using HMAC-SHA256 with your webhook signing secret.
// src/klaviyo/webhook-verify.ts
import crypto from 'crypto';
/**
* Verify Klaviyo webhook signature.
* Klaviyo uses the webhook signing secret (set when creating the webhook)
* to compute an HMAC-SHA256 signature of the payload.
*/
export function verifyKlaviyoWebhookSignature(
payload: Buffer | string,
signature: string,
secret: string
): boolean {
if (!signature || !secret) return false;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(typeof payload === 'string' ? payload : payload.toString())
.digest('base64');
// Timing-safe comparison to prevent timing attacks
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch {
return false; // Different lengths
}
}
import express from 'express';
app.post('/webhooks/klaviyo',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['klaviyo-webhook-signature'] as string;
if (!verifyKlaviyoWebhookSignature(
req.body,
signature,
process.env.KLAVIYO_WEBHOOK_SIGNING_SECRET!
)) {
console.warn('[Security] Invalid webhook signature rejected');
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString());
// Process verified event...
res.status(200).json({ received: true });
}
);
# 1. Generate new key in Klaviyo dashboard (Settings > API Keys)
# - Name it with date: "Production API Key 2025-03"
# - Assign same scopes as the old key
# 2. Deploy new key (zero-downtime)
# Update secret in your deployment platform:
# - Vercel: vercel env add KLAVIYO_PRIVATE_KEY production
# - AWS: aws secretsmanager update-secret --secret-id klaviyo-key --secret-string pk_new_***
# - GCP: echo -n "pk_new_***" | gcloud secrets versions add klaviyo-key --data-file=-
# 3. Verify new key works
curl -s -w "%{http_code}" -o /dev/null \
-H "Authorization: Klaviyo-API-Key pk_new_***" \
-H "revision: 2024-10-15" \
"https://a.klaviyo.com/api/accounts/"
# 4. Revoke old key in Klaviyo dashboard
# Settings > API Keys > Delete old key
# 5. Audit: check logs for any 401s after rotation
.env files in .gitignoregit log -p | grep pk_)| Security Issue | Detection | Mitigation |
|---|---|---|
| Leaked private key | Git scanning, trufflehog | Revoke immediately, rotate |
| Excessive scopes | Scope audit | Reduce to minimum required |
| Missing webhook verification | Code review | Add HMAC check |
| Key not rotated | Age > 90 days | Schedule rotation |
| 401s after rotation | Log monitoring | Verify all services updated |
For production deployment, see klaviyo-prod-checklist.