Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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".
.claude/skills/jeremylongshore-klaviyo-security-basics/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | -2% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 38% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 41% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 43% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 36% | 0% |
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.
Keep every private key and the webhook signing secret out of source: load them from .env (git-ignored) through a validated config loader that throws on a missing secret, so misconfiguration fails at boot instead of at first API call.
typescript// src/config/klaviyo.ts -- validated config loader (skeleton) export const klaviyoConfig = { privateKey: requireEnv('KLAVIYO_PRIVATE_KEY'), // throws if absent publicKey: process.env.KLAVIYO_PUBLIC_KEY || '', webhookSecret: process.env.KLAVIYO_WEBHOOK_SIGNING_SECRET || '', };
Full .env template, .gitignore entries, and the requireEnv helper: implementation.md → Environment Variable Configuration.
Issue a separate key for each environment with only the scopes that environment needs — read-only in dev and CI, full read/write in staging, the exact production scope set in prod — so a leaked key has the smallest possible blast radius. Scope table and per-environment env-var layout: implementation.md → Least-Privilege API Key Scopes.
Klaviyo signs each webhook payload with your signing secret. Recompute the HMAC-SHA256 digest over the raw body and compare with crypto.timingSafeEqual to defeat timing attacks; reject anything that does not match with 401.
typescriptconst expected = crypto.createHmac('sha256', secret) .update(rawBody).digest('base64'); return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
Full verifier plus the Express raw-body middleware that returns 401 Invalid signature: implementation.md → Webhook Signature Verification and Express Webhook Middleware.
Rotate private keys on a schedule (quarterly) or immediately on suspected leak: generate a replacement with identical scopes, deploy it to the secret store, verify with a curl against /api/accounts/, then revoke the old key and watch logs for 401s. Full five-step runbook with per-platform commands: implementation.md → API Key Rotation Procedure.
.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 |
Applying this skill produces a hardened Klaviyo integration:
.env plus a src/config/klaviyo.ts loader that fails fast on amissing private key.
200 { "received": true } only for payloadswhose HMAC-SHA256 signature verifies, and 401 { "error": "Invalid signature" } for everything else.
Three worked scenarios — validated config loader, rejecting a forged webhook, and zero-downtime key rotation — with inputs and expected results are in examples.md. Quick sketch of the webhook case:
textPOST /webhooks/klaviyo (tampered body, original signature) → verifyKlaviyoWebhookSignature() recomputes HMAC → mismatch → 401 { "error": "Invalid signature" }, logged as rejected
See examples.md for the full walkthrough of each.
For production hardening beyond secrets — rate limits, monitoring, and deploy gates — see the klaviyo-prod-checklist skill in this pack.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-18 | pass→pass | 11,474 | 11,923 | +4% | 1 | 1 | 0% | 999 | 2,575 | +158% | 0 | 0 | — |
case-19 | pass→pass | 16,818 | 10,331 | -39% | 1 | 1 | 0% | 1,918 | 3,344 | +74% | 0 | 0 | — |
case-17 | pass→pass | 13,369 | 16,153 | +21% | 1 | 1 | 0% | 2,008 | 3,451 | +72% | 0 | 0 | — |
case-01 | pass→pass | 22,626 | 18,645 | -18% | 1 | 1 | 0% | 3,368 | 4,282 | +27% | 0 | 0 | — |
case-02 | fail→pass | 66,783 | 24,760 | -63% | 1 | 1 | 0% | 5,109 | 5,017 | -2% | 0 | 0 | — |
case-03 | pass→pass | 25,568 | 19,280 | -25% | 1 | 1 | 0% | 3,115 | 4,397 | +41% | 0 | 0 | — |
case-04 | pass→pass | 18,389 | 12,090 | -34% | 1 | 1 | 0% | 2,599 | 3,974 | +53% | 0 | 0 | — |
case-05 | pass→pass | 21,176 | 16,038 | -24% | 1 | 1 | 0% | 2,537 | 4,614 | +82% | 0 | 0 | — |
case-06 | fail→pass | 16,675 | 12,066 | -28% | 1 | 1 | 0% | 2,697 | 3,719 | +38% | 0 | 0 | — |
case-07 | fail→pass | 11,171 | 63,133 | +465% | 1 | 1 | 0% | 1,938 | 2,732 | +41% | 0 | 0 | — |
case-08 | fail→pass | 11,704 | 12,968 | +11% | 1 | 1 | 0% | 1,906 | 2,724 | +43% | 0 | 0 | — |
case-09 | fail→pass | 16,993 | 8,040 | -53% | 1 | 1 | 0% | 2,011 | 2,744 | +36% | 0 | 0 | — |
case-10 | pass→pass | 15,969 | 13,748 | -14% | 1 | 1 | 0% | 1,865 | 3,062 | +64% | 0 | 0 | — |
case-11 | pass→pass | 12,208 | 8,084 | -34% | 1 | 1 | 0% | 1,979 | 1,936 | -2% | 0 | 0 | — |
case-12 | pass→pass | 12,784 | 9,254 | -28% | 1 | 1 | 0% | 2,038 | 2,183 | +7% | 0 | 0 | — |
case-13 | pass→pass | 15,044 | 11,822 | -21% | 1 | 1 | 0% | 1,518 | 2,832 | +87% | 0 | 0 | — |
case-14 | pass→pass | 8,854 | 5,285 | -40% | 1 | 1 | 0% | 1,546 | 2,435 | +58% | 0 | 0 | — |
case-15 | pass→pass | 17,357 | 10,732 | -38% | 1 | 1 | 0% | 2,179 | 2,363 | +8% | 0 | 0 | — |
case-16 | pass→pass | 12,272 | 4,077 | -67% | 1 | 1 | 0% | 1,147 | 2,085 | +82% | 0 | 0 | — |
case-20 | fail→pass | 13,567 | 9,854 | -27% | 1 | 1 | 0% | 2,068 | 2,886 | +40% | 0 | 0 | — |
case-21 | pass→pass | 8,687 | 13,686 | +58% | 1 | 1 | 0% | 1,508 | 3,002 | +99% | 0 | 0 | — |
case-22 | fail→pass | 11,500 | 6,885 | -40% | 1 | 1 | 0% | 1,113 | 1,777 | +60% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +32 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.