
Klaviyo Analyst
- 67 installs
- 93 repo stars
- Updated May 14, 2026
- thatrebeccarae/claude-marketing
Helps with ai & agent building tasks.
About
klaviyo-analyst is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- klaviyo-analyst
- AI & Agent Building
- AI-coding skill
Klaviyo Analyst by the numbers
- 67 all-time installs (skills.sh)
- +9 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #5,935 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thatrebeccarae/claude-marketing --skill klaviyo-analystAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 67 |
|---|---|
| repo stars | ★ 93 |
| Last updated | May 14, 2026 |
| Repository | thatrebeccarae/claude-marketing ↗ |
What it does
Helps with ai & agent building tasks.
Files
Klaviyo Marketing Analyst
Expert-level guidance for Klaviyo email and SMS marketing from the marketing operations and analyst perspective — auditing, building, and optimizing flows, segments, campaigns, and integrations.
For API integration, SDK usage, event tracking implementation, webhooks, and developer patterns, see the klaviyo-developer skill.
Install
Step 1 — Install the skill
git clone https://github.com/thatrebeccarae/claude-marketing.git && cp -r claude-marketing/skills/klaviyo-analyst ~/.claude/skills/Step 2 — Connect the Klaviyo MCP (recommended)
This skill is designed around Klaviyo's official MCP server. Connect Claude to your Klaviyo account through OAuth — no local API key, no Python install. The setup path depends on which Claude surface you're using.
For Claude Chat or Claude Cowork — use the Connector Directory
Klaviyo is listed in Claude's Connector Directory (announced as part of the expanded Klaviyo + Anthropic integration on 2026-05-07). Setup takes about two minutes:
1. Open Claude → Settings → Connectors → Browse Connectors 2. Search for Klaviyo 3. Click Connect and authenticate
Plan requirement: Connectors are available on Claude Pro, Max, Team, and Enterprise plans. Free plan users will need to use Claude Code or the local install path below.
For Claude Code — register the remote MCP
claude mcp add klaviyo --transport http https://mcp.klaviyo.com/mcpFor audit-only sessions, append ?read-only=true to disable all write tools at the protocol layer:
claude mcp add klaviyo --transport http "https://mcp.klaviyo.com/mcp?read-only=true"For local install (CI containers, offline development)
claude mcp add klaviyo -e PRIVATE_API_KEY=pk_... -e READ_ONLY=true -- uvx klaviyo-mcp-server@latestKlaviyo-side requirement: Owner, Admin, or Manager role to authorize the connection.
The MCP exposes 40+ tools across Accounts, Campaigns, Catalogs, Events, Flows, Groups, Profiles, Reporting, Templates, and Translations. See REFERENCE.md for the full tool inventory.
Step 3 — Fallback: local CLI scripts (optional)
If you need scripted CLI access for CI, cron, or headless reporting — install the Python requirements and provide a KLAVIYO_API_KEY. See the Scripts (Fallback) section below.
Core Capabilities
Flow Auditing & Optimization
- Audit existing flows against best practices (welcome series, abandoned cart, post-purchase, winback, browse abandonment, sunset)
- Identify revenue leakage from missing or underperforming flows
- Recommend split tests, timing adjustments, and conditional logic improvements
- Review flow filters and trigger conditions for accuracy
Segmentation Strategy
- Build RFM-based segments (Recency, Frequency, Monetary)
- Design engagement tiers: Active (0-30d), Warm (31-90d), At-Risk (91-180d), Lapsed (180d+)
- Create predictive segments using Klaviyo's predictive analytics (CLV, churn risk, next order date)
- Suppress unengaged contacts to protect deliverability
Campaign Strategy
- Plan campaign calendars balancing promotional and value content
- A/B testing frameworks for subject lines, send times, content blocks
- Dynamic content personalization using profile properties and catalog data
- SMS campaign compliance (TCPA, quiet hours, opt-in requirements)
Deliverability Management
- Monitor and diagnose deliverability issues (bounce rates, spam complaints, inbox placement)
- Warm-up strategies for new sending domains/IPs
- Authentication setup: SPF, DKIM, DMARC
- List hygiene practices and sunset flow design
Revenue Attribution & Reporting
- Interpret Klaviyo's attribution model (click-based, 5-day email / 24-hour SMS default windows)
- Build custom dashboards for flow revenue, campaign ROI, list growth
- Benchmark KPIs against industry standards
Event Schema Auditing
- Inventory all tracked metrics by source (built-in integration, custom API, Klaviyo-internal, forms)
- Identify duplicate/redundant events, unused events, and missing standard events
- Assess event property structures for segmentation and personalization accessibility
- Diagnose nested object limitations blocking flow splits and segment conditions
Integration Health Review
- Audit active integrations (e-commerce, review platforms, loyalty, ads)
- Identify stale/dead integrations and orphaned event sources
- Assess flow trigger architecture: direct metric triggers (robust) vs segment-entry triggers via API-synced properties (brittle)
- Evaluate catalog sync health: product coverage, variant handling, freshness
Profile Data Utilization Analysis
- Map profile properties: what's collected vs what's used in segmentation/personalization
- Identify stale calculated properties (set once, never updated)
- Flag properties on events but not synced to profiles (limits segmentation)
- Recommend property flattening strategies for nested data accessibility
Key Benchmarks
| Metric | Good | Great | Warning |
|---|---|---|---|
| Open Rate (email) | 20-25% | 30%+ | <15% |
| Click Rate (email) | 2-3% | 4%+ | <1.5% |
| Unsubscribe Rate | <0.3% | <0.1% | >0.5% |
| Spam Complaint Rate | <0.05% | <0.02% | >0.1% |
| Flow Revenue % of Total | 30-40% | 50%+ | <20% |
| SMS Click Rate | 8-12% | 15%+ | <5% |
| List Growth Rate (monthly) | 3-5% | 8%+ | <1% |
Essential Flows Checklist
1. Welcome Series (3-5 emails + optional SMS) — triggers on list subscribe 2. Abandoned Cart (2-3 emails + 1 SMS) — triggers on Started Checkout 3. Browse Abandonment (1-2 emails) — triggers on Viewed Product, exclude recent purchasers 4. Post-Purchase (2-4 emails) — triggers on Placed Order, split by first-time vs repeat 5. Winback (2-3 emails) — triggers on time since last purchase (60-90 days) 6. Sunset/Re-engagement (2 emails) — targets unengaged 90-180 days, then suppress 7. Review Request — triggers post-delivery, integrates with review platform 8. Replenishment (if applicable) — triggers based on expected repurchase cycle 9. Birthday/Anniversary — triggers on date property 10. VIP/Loyalty — triggers on high-CLV segment entry
Workflow: Full Klaviyo Audit (4-Phase Deep Framework)
Cowork-ready: the 4-phase audit is built for unattended execution. With the Klaviyo connector active in Cowork (read-only mode), describe the outcome — _"Audit my Klaviyo flows and tell me what's missing,"_ _"Pull last week's campaign and flow performance and write me a Monday digest,"_ _"Flag every flow with open rates under 20% and write a prioritized fix-it list"_ — then step away. Cowork pulls the data, runs the analysis, writes the doc, and saves it to the right folder while you're in other meetings.
When asked to audit a Klaviyo account, follow this 4-phase framework:
Phase 1: Status Inventory
1. List/Segment/Flow/Campaign Inventory — Count and categorize all objects by status (live, draft, inactive) 2. Event Schema Inventory — Pull all metrics, categorize by source (built-in, custom API, Klaviyo-internal, forms), flag zero-volume events 3. Integration Inventory — List all active integrations, identify stale connections 4. Custom Profile Property Inventory — Document all custom properties with types and usage
Phase 2: Configuration Audit
5. Per-Flow Configuration Teardown — For each active flow:
- Trigger type and conditions
- Smart Send settings (on/off and implications)
- Exclusion filters (segment membership, event conditions)
- Timing between messages (compare to benchmarks)
- Branching logic (conditional splits, A/B splits)
- Flow duration (first message to last)
6. Campaign Targeting Audit — Segment exclusivity, frequency capping, send-time optimization 7. A/B Testing Methodology Audit — Univariate vs multivariate, KPI alignment, statistical significance practices 8. Deliverability Configuration — Authentication (SPF/DKIM/DMARC), dedicated IP, warmup status
Phase 3: Data Structure Audit
9. Event Schema Health — Check for duplicates, missing standard events, nested objects blocking segmentation 10. Profile Data Utilization — What's collected vs what's used in segmentation/personalization 11. Catalog Sync Health — Product feed freshness, coverage, variant handling
Phase 4: Strategic Recommendations
12. Three-Tier Recommendations — For each finding:
- Finding: What's wrong + evidence from audit data
- Recommendation: What to do (client-facing, plain language)
- Implementation Spec: How to build it (internal SOW with triggers, filters, content brief, timing, testing plan)
13. Sequential Testing Plans — Univariate A/B tests with stat sig framework 14. Quantified ROI — Expected uplift % x current revenue baseline 15. Implementation Roadmap — Phased timeline with dependencies mapped
Industry-Specific Benchmarks
B2B / Wholesale E-Commerce
| Metric | Good | Great | Warning | Notes |
|---|---|---|---|---|
| Open Rate | 25-35% | 40%+ | <20% | Higher than DTC due to professional relevance |
| Click Rate | 3-5% | 6%+ | <2% | Product-focused CTAs perform well |
| CTOR | 10-15% | 18%+ | <8% | Key diagnostic — content relevance signal |
| Flow Revenue % | 25-35% | 40%+ | <15% | Reorder flows are high-value in B2B |
| Avg Order Value | Varies | — | — | Track by segment (industry, company size) |
| Reorder Rate | 60-70% | 80%+ | <50% | Critical for consumable categories |
DTC (Direct-to-Consumer)
| Metric | Good | Great | Warning |
|---|---|---|---|
| Open Rate | 20-25% | 30%+ | <15% |
| Click Rate | 2-3% | 4%+ | <1.5% |
| Flow Revenue % | 30-40% | 50%+ | <20% |
| Welcome Series Conv. | 3-5% | 8%+ | <2% |
| Cart Recovery Rate | 5-10% | 15%+ | <3% |
Subscription / Recurring Revenue
| Metric | Good | Great | Warning |
|---|---|---|---|
| Churn Rate (monthly) | <5% | <3% | >8% |
| Reactivation Rate | 5-10% | 15%+ | <3% |
| Replenishment Flow Conv. | 8-12% | 15%+ | <5% |
| Subscription Upgrade Rate | 3-5% | 8%+ | <1% |
SMS Platform Comparison Framework
When evaluating Klaviyo SMS vs Attentive (common in audits):
| Capability | Klaviyo SMS | Attentive |
|---|---|---|
| Email + SMS unified | Yes (native) | No (separate platform) |
| Shared segments | Yes | Requires sync |
| Unified attribution | Yes | Separate reporting |
| Conversational SMS | Limited | Strong (two-way) |
| Sign-up units | Basic (popup, form) | Advanced (two-tap, link-based) |
| AI features | Predictive analytics, smart send time | AI journeys, smart sending |
| Compliance | Built-in TCPA, quiet hours | Built-in + compliance team |
| Cost | Included in Klaviyo plan (per SMS) | Separate platform fee + per SMS |
| Best for | Unified email+SMS, simplicity | SMS-first strategy, high volume SMS |
Recommendation framework: Use Klaviyo SMS when email is the primary channel and SMS is supplementary (most B2B and mid-market DTC). Consider Attentive when SMS is a primary revenue channel (high-frequency DTC, mobile-first brands).
Repeatable Audit Workflow (Klaviyo MCP Tool Sequence)
When auditing an account via the official Klaviyo MCP server (https://mcp.klaviyo.com/mcp), invoke tools in this order. All tools below are read-only and safe to run against production with the ?read-only=true URL parameter set.
| Step | MCP Tool | Purpose |
|---|---|---|
| 1 | get_account_details | Account config, timezone, integrations |
| 2 | get_metrics | Full event inventory (all metric names and IDs) |
| 3 | get_metric (per metric) | Property structure for key events (Placed Order, Started Checkout, Viewed Product) |
| 4 | get_flows | All flows with status |
| 5 | get_flow (per live flow) | Trigger details, actions, filters |
| 6 | get_flow_report (per flow) | Revenue, conversion, engagement per flow |
| 7 | get_campaigns | Recent campaigns with send dates |
| 8 | get_campaign_report (per campaign) | Open/click/unsub/revenue metrics |
| 9 | get_segments + get_segment | Segment inventory and condition definitions |
| 10 | get_lists + get_list | List inventory |
| 11 | get_catalog_items | Catalog sync health check |
| 12 | query_metric_aggregates | Time-series rollups (mirrors in-app Metric Reporting) for deliverability and revenue trend analysis |
| 13 | get_events (filtered) | Sample recent event payloads to inspect property structure and nested object usage |
After the data pull, analyze using the 4-Phase Deep Framework above.
Note on write tools: the MCP also exposes write tools (create_campaign, create_profile, update_profile, create_email_template, subscribe_profile_to_marketing, assign_template_to_campaign_message, upload_image_from_file, upload_image_from_url, create_event, plus translations). For pure audit work, gate these by connecting with ?read-only=true. Only enable writes if you're acting on a confirmed recommendation (e.g., assigning a template to a campaign you're building together with the user).
Integration Context
E-commerce Platforms
- Shopify: Native integration, syncs orders/products/customers automatically. Use Shopify-specific metrics (Placed Order, Started Checkout, Viewed Product).
- WooCommerce / BigCommerce / Magento: Similar event sync, may need plugin configuration.
- Custom platforms: Coordinate with developers using the klaviyo-developer skill for API event tracking and profile management.
Common Integration Points
- Review platforms (Yotpo, Judge.me, Stamped) for post-purchase review flows
- Loyalty programs (Smile.io, LoyaltyLion) for points-based segmentation
- Subscription platforms (Recharge, Bold) for subscription lifecycle flows
- SMS: Built-in Klaviyo SMS or integration with Attentive/Postscript
How to Use This Skill
Ask me questions like:
- "Audit my Klaviyo flows and identify gaps"
- "Design a welcome series for my DTC brand"
- "My open rates dropped — help me diagnose deliverability issues"
- "Build an RFM segmentation strategy"
- "What A/B tests should I run on my abandoned cart flow?"
- "Help me set up a sunset flow to clean my list"
- "Plan a Black Friday email/SMS campaign calendar"
- "Audit my event schema — are we tracking everything we need?"
- "Give me a three-tier recommendation for fixing our click rates"
Analysis Examples
For complete analysis patterns, worked examples with sample output, and use cases, see EXAMPLES.md.
Scripts (Fallback: Local CLI)
When to use these: scripted CLI access for CI/cron, headless reporting jobs, or environments where you can't connect the Klaviyo MCP (e.g., automated pipelines using KLAVIYO_API_KEY env vars). For interactive analysis in Claude Code/Chat/Cowork, prefer the MCP path above — it's faster, OAuth-authenticated, and exposes the same data.The skill includes utility scripts for data fetching and analysis:
Fetch Klaviyo Data
# List all flows
python scripts/klaviyo_client.py --resource flows
# List campaigns as table
python scripts/klaviyo_client.py --resource campaigns --format table
# Get flow performance report
python scripts/klaviyo_client.py --resource report --report-type flow --id FLOW_ID
# Export metrics to file
python scripts/klaviyo_client.py --resource metrics --output metrics.jsonAnalyze and Generate Reports
# Full account audit
python scripts/analyze.py --analysis-type full-audit
# Flow gap analysis
python scripts/analyze.py --analysis-type flow-audit
# Segment health check
python scripts/analyze.py --analysis-type segment-health
# Campaign performance comparison
python scripts/analyze.py --analysis-type campaign-comparison --days 30
# Deliverability diagnostic
python scripts/analyze.py --analysis-type deliverability
# Revenue attribution
python scripts/analyze.py --analysis-type revenue-attributionThe scripts handle API authentication, data fetching, and analysis. I'll interpret the results and provide actionable recommendations.
Troubleshooting
MCP connection issues
MCP not appearing in Claude: Verify the MCP is configured in your client and authenticated. For Claude Code, check ~/.claude.json or your project-level MCP config. For Claude Chat, check Settings → Connectors.
OAuth permission denied: Your Klaviyo user role must be Owner, Admin, or Manager. Lower-permission users cannot authorize the MCP.
Write tools failing: Confirm the connection URL does not have ?read-only=true set. If it does, write tools will be disabled by design — switch to a read-write URL for write actions.
Multi-account confusion: The MCP supports multi-account setups. If Claude pulls data from the wrong account, re-authenticate or check your active account in Klaviyo's UI.
Script (fallback) errors
Authentication Error: Verify that:
KLAVIYO_API_KEYis set as an environment variable or in a.envfile- The key starts with
pk_(private API key, not public) - The key has read scopes:
profiles:read,flows:read,campaigns:read,segments:read,lists:read,metrics:read
No Data Returned: Check that:
- The API key is associated with the correct Klaviyo account
- The account has active flows, campaigns, or segments to analyze
- Filters are correctly formatted (e.g.,
equals(status,"live"))
Rate Limit Errors: The SDK handles retries automatically (up to 3 retries with 60s max delay). If you still hit limits:
- Reduce concurrent requests
- Add delays between sequential calls
- Check
RateLimit-Remainingheader
Import Errors: Install required packages:
pip install klaviyo-api python-dotenv pandasSecurity Notes
When using the Klaviyo MCP (recommended)
- The MCP uses OAuth — there's no API key to commit, rotate, or leak
- For audit-only work, gate with
?read-only=trueon the connection URL to disable all write tools - Authorization is scoped to your Klaviyo user role (Owner / Admin / Manager) — least-privilege is enforced upstream
- Re-authentication is required if your role changes or the OAuth token expires
- Multi-account setups: confirm the active account before running tools that include write operations
When using the script fallback
- Never hardcode API keys in code or commit them to version control
- Store keys in environment variables or
.envfiles - Add
.envto.gitignore - Use read-only scopes for analyst tasks — no write access needed
- Rotate API keys periodically in Klaviyo Settings
Data Privacy
This skill accesses aggregated marketing data only. It does not:
- Access personally identifiable information (PII) beyond email/profile aggregates
- Store Klaviyo data persistently
- Share data with external services
- Modify your Klaviyo configuration (read-only operations)
All data is processed locally and used only to generate recommendations during the conversation.
For detailed Klaviyo data model, flow builder reference, segmentation conditions, deliverability, and SMS reference, see REFERENCE.md.
For API integration, SDK usage, and developer patterns, use the klaviyo-developer skill.
# Klaviyo API Configuration
# Copy this file to .env and fill in your actual values
# Your Klaviyo Private API Key
# Find this in Klaviyo: Settings > Account > API Keys
# Format: pk_xxxxxxxxxxxxxxxx (starts with "pk_")
# Use a key with READ-ONLY scopes for analyst tasks
KLAVIYO_API_KEY=pk_your-private-api-key-here
# How to set up:
# 1. Log in to Klaviyo (www.klaviyo.com)
# 2. Go to Settings > Account > API Keys
# 3. Click "Create Private API Key"
# 4. Name it (e.g., "Analytics Read-Only")
# 5. Select READ-ONLY scopes: profiles:read, events:read, metrics:read,
# flows:read, campaigns:read, segments:read, lists:read
# 6. Copy the key (starts with "pk_")
# 7. Paste it above and rename this file to .env
# 8. NEVER commit .env to version control!
Klaviyo Analyst Examples
Practical examples of common Klaviyo marketing analysis tasks and optimization patterns.
Example 1: Full Account Health Audit
User Request: "Audit my Klaviyo account and tell me what needs fixing"
Analysis Steps: 1. Inventory all flows and compare against essential checklist 2. Assess segment structure and engagement tiers 3. Review campaign metrics against benchmarks 4. Check deliverability health 5. Analyze revenue attribution (flows vs campaigns)
Script Command:
python scripts/analyze.py --analysis-type full-audit --output audit.jsonSample Output Analysis:
Klaviyo Account Health Audit
=== FLOW AUDIT ===
Coverage: 7/10 essential flows
Essential Flows Checklist:
OK Welcome Series — live
OK Abandoned Cart — live
OK Browse Abandonment — live
OK Post-Purchase — live
XX Winback — MISSING (HIGH priority)
OK Sunset/Re-engagement — live
XX Review Request — MISSING (MEDIUM priority)
OK Replenishment — draft
XX Birthday/Anniversary — MISSING (LOW priority)
OK VIP/Loyalty — live
Active: 6 | Draft: 2 | Inactive: 1
=== SEGMENT HEALTH ===
Total Segments: 14 | Lists: 6
Engagement Tiers: OK Active (0-30d), OK Warm (31-90d), XX At-Risk, OK Lapsed
RFM Segments: XX Not found
Predictive Segments: XX Not found
Suppression Segment: OK Present
=== CAMPAIGN METRICS (Last 30 Days) ===
Campaigns Sent: 8 | Total Recipients: 124,500
Metric Value Benchmark Rating
Open Rate 22.4% 20-25% OK Good
Click Rate 1.8% 2-3% !! Warning
Unsubscribe 0.28% <0.3% OK Good
Spam Complaints 0.03% <0.05% OK Good
=== REVENUE ATTRIBUTION ===
Total Email Revenue: $52,400
Flow Revenue: $21,500 (41.0%) — OK Good
Campaign Revenue: $30,900 (59.0%)
Top Flows by Revenue:
1. Abandoned Cart — $8,750
2. Welcome Series — $4,600
3. Post-Purchase — $4,200
4. Browse Abandonment — $2,350
5. VIP/Loyalty — $1,600Example 2: Flow Gap Analysis
User Request: "What flows am I missing and which should I build first?"
Script Command:
python scripts/analyze.py --analysis-type flow-auditSample Output (abbreviated — see SKILL.md Essential Flows Checklist for full pattern):
Coverage Score: 6/10
Missing: Winback (CRITICAL), Sunset (HIGH), Replenishment (MEDIUM), Birthday (LOW)Example 3: Segment Health Check
User Request: "Are my segments set up correctly?"
Script Command:
python scripts/analyze.py --analysis-type segment-healthFocus on: engagement tier completeness, RFM presence, suppression segment, mutual exclusivity.
Example 4: Campaign Performance Comparison
User Request: "How are my recent campaigns performing?"
Script Command:
python scripts/analyze.py --analysis-type campaign-comparison --days 30Compare: open rate, click rate, CTOR, unsub rate, spam rate, RPR against benchmarks. Flag underperformers.
Example 5: Deliverability Diagnostic
User Request: "My open rates are dropping — deliverability problem?"
Script Command:
python scripts/analyze.py --analysis-type deliverabilityCheck: bounce rate (<2%), spam complaints (<0.05%), delivery rate (>98%), SPF/DKIM/DMARC auth.
Example 6: Revenue Attribution
User Request: "What % of revenue comes from flows vs campaigns?"
Script Command:
python scripts/analyze.py --analysis-type revenue-attributionBenchmark: flow revenue should be 30-50% of total. Below 20% = major flow gap.
---
Example 7: Per-Flow Configuration Audit
User Request: "Do a deep audit of my abandoned cart flow configuration"
This is a Phase 2 (Configuration Audit) analysis — goes beyond flow existence to examine how the flow is built.
Sample Output Analysis:
=== FLOW CONFIGURATION AUDIT: Abandoned Cart ===
Trigger: Segment Entry — "Added to Cart - Active"
Type: Segment-based (not direct metric trigger)
Risk: MEDIUM — If API stops syncing cart data to profiles,
segment stops updating and flow stops triggering.
Recommendation: Consider direct metric trigger on "Started Checkout"
or "Added to Cart" event for resilience.
Messages: 1 email
Email 1: "Complete Your Order" — Sends immediately on segment entry
Issue: Single email only. Best practice is 2-3 emails.
Smart Send: ON (16-hour window)
Issue: Smart Send should be OFF for abandoned cart.
Cart recovery is time-sensitive — delays reduce conversion.
Impact: Estimated 10-20% of recipients delayed past optimal window.
Exclusion Filters:
- "Has Placed Order at least 1 time in the last 4 hours" — OK
- Missing: No exclusion for profiles already in Welcome Series
Issue: New subscribers could receive Welcome Email 1 AND Cart Email 1
on the same day, creating a jarring experience.
Flow Duration: 0 days (single immediate email)
Benchmark: 2-3 days with 3 touchpoints
Issue: Missing follow-up emails means ~60% of recoverable carts are lost.
Timing Analysis:
Email 1 at 0h — OK (immediate is correct for first touch)
No Email 2 at 24h — MISSING (urgency reminder)
No Email 3 at 48-72h — MISSING (last chance + incentive)
A/B Testing: None active
Recommendation: Test subject line first (urgency vs benefit),
then CTA placement, then incentive.
=== THREE-TIER RECOMMENDATION ===
Finding: Abandoned Cart flow has 1 email (should be 3), Smart Send ON
(should be OFF), and no exclusion for Welcome Series overlap. Single
email captures only ~40% of recoverable revenue.
Recommendation: Expand to 3-email series with Smart Send OFF, add
Welcome Series exclusion filter, implement sequential A/B testing
program starting with subject lines.
Implementation Spec: Build steps include detailed trigger configuration,
content briefs per email, timing, conditional splits, and testing plan.
See the implementation spec section for full details.Example 8: Event Schema Audit
User Request: "Audit our event tracking — are we capturing everything we need?"
This is a Phase 3 (Data Structure Audit) analysis using the MCP metrics endpoint.
Sample Output Analysis:
=== EVENT SCHEMA AUDIT ===
Total Metrics: 50 across 4 sources
Source Breakdown:
API (Custom Integration): 10 metrics
Placed Order (AbC12d) — Active, revenue-tracked
Started Checkout (XyZ78e) — Active, used in flows
Viewed Product (Mn34Pq) — Active, NOT used in any flow
Added to Cart (Rs56Tu) — Active, NOT used in any flow
Order Completed (Jk90Wx) — Active, used in post-purchase flow
Cancelled Order — Active, NOT used
Refunded Order — Active, NOT used
Cart Viewed — Active, NOT used
Account Created — Active, used in welcome flow
Subscription Started — Active, NOT used
Klaviyo Internal: 24 metrics
Received Email, Opened Email, Clicked Email, Bounced Email,
Marked Email as Spam, Unsubscribed, Subscribed to List, etc.
Status: All normal — auto-tracked by Klaviyo
Klaviyo Forms: 14 metrics
submitted_back_in_stock_form (Fg45Hj) — Active, potential flow trigger
Various form submissions — Active
Meta Ads: 1 metric
Ads Audience Synced — Active
Issues Identified:
1. UNUSED HIGH-VALUE EVENTS (HIGH)
Viewed Product — Tracked but not used in any flow.
Impact: Missing Browse Abandonment flow ($15K-$30K/year potential).
Action: Build Browse Abandonment flow (see implementation spec).
Added to Cart — Tracked but not used in any flow.
Impact: Could supplement Started Checkout for cart recovery.
Action: Evaluate as backup trigger for Abandoned Cart flow.
2. POTENTIAL DUPLICATE: Cart Viewed vs Added to Cart (MEDIUM)
Both track cart-related activity. Need to verify if they capture
different user actions or are redundant.
Action: Check event properties and volumes. If duplicate, deprecate one.
3. UNUSED EVENTS (LOW)
Cancelled Order, Refunded Order — Tracked but not used.
Opportunity: Use Cancelled Order to suppress from post-purchase flows.
Use Refunded Order to trigger save-the-sale re-engagement.
4. MISSING STANDARD EVENT: Fulfilled Order (MEDIUM)
Not detected via API source. May be tracked under different name
("Order Completed" Jk90Wx may serve this purpose).
Action: Verify Order Completed includes fulfillment/shipping data.
If not, add Fulfilled Order event for review request flow timing.
Event Property Accessibility:
Placed Order Items[] — Nested (templates only, not segments/splits)
Placed Order $value — Top-level (full access)
Placed Order Categories — Top-level array (segmentable via "contains")
Profile company_type — Top-level string (full access)
Profile industry — Top-level array (segmentable via "contains")Example 9: Three-Tier Recommendation Format
Example of the standard recommendation format used in Phase 4 output:
=== FINDING ===
Welcome flow bounce rate is 8.7% (435 bounces out of 5,000 recipients).
Industry benchmark is <2%. This is 4x above acceptable levels.
Root cause: The trigger segment ("New Subscribers - 14 Day") includes
profiles up to 14 days old, many of which have stale/invalid emails from
abandoned registrations.
=== RECOMMENDATION (Client-Facing) ===
Fix the Welcome flow bounce rate by tightening the trigger window and adding
email verification. This protects your sender reputation, which underpins
$750K in annual campaign revenue. Without action, deliverability will degrade
over the next 3-6 months, reducing inbox placement across all sends.
Priority: P1 (Critical)
Timeline: 1-2 weeks
Expected Impact: Protects $1M+ annual email revenue
=== IMPLEMENTATION SPEC (Internal SOW) ===
Prerequisites:
- Access to API/backend for email verification integration
- Klaviyo admin access for segment and flow modifications
Step 1: Tighten trigger segment window from 14 days to 3 days
- Edit segment "New Subscribers - 14 Day"
- Change condition: "created in the last 3 days" (was 14)
- Rationale: 90%+ of valid signups engage within 3 days
Step 2: Integrate email verification at registration
- Add ZeroBounce/NeverBounce API call on account creation
- Set profile property: email_verified = true/false
- Add segment condition: email_verified = true
Step 3: Suppress existing hard-bounced profiles
- Create segment: "Hard Bounced - Never Purchased"
- Suppress via profile suppression bulk job
- Estimated: 500-1,000 profiles
Step 4: Monitor
- Track bounce rate weekly for 30 days
- Target: <1% within 2 weeks of implementation
Testing Plan:
- No A/B test needed (this is a fix, not an optimization)
- Monitor: bounce rate, delivery rate, welcome flow conversion rate
- Success: bounce rate <2% sustained for 30 days
Dependencies: None (this is the first action in the roadmap)---
Common Analysis Patterns
Audit Cadence
- Monthly: Campaign metrics review, segment health check
- Quarterly: Full account audit, flow optimization
- Semi-annual: Deliverability deep-dive, integration review
- Annual: Strategy review, benchmark comparison
Flow Optimization Sequence
1. Check revenue per recipient 2. Compare open/click rates to benchmarks 3. Review timing between messages 4. Check for split test opportunities 5. Verify flow filters are current
Pro Tips
Ask Better Questions
Instead of: "Show me my Klaviyo data" Ask: "What are the top 3 revenue opportunities in my Klaviyo account?"
Request Actionable Insights
Instead of: "What's my open rate?" Ask: "Are my email metrics hitting industry benchmarks, and what should I fix first?"
Focus on Revenue Impact
Instead of: "List all my flows" Ask: "Which flows are missing and how much revenue am I leaving on the table?"
Request Implementation Specs
Instead of: "What should I improve?" Ask: "Give me a three-tier recommendation with an implementation SOW for fixing my click rates"
Compare Periods
Instead of: "How are campaigns doing?" Ask: "How do this month's campaign metrics compare to last month?"
MIT License
Copyright (c) 2026 Rebecca Rae Barton
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Klaviyo Reference
Data Model
Profiles (Contacts)
- Email — Primary identifier
- Phone Number — For SMS, must include country code
- Properties — Custom profile fields (first name, city, loyalty tier, etc.)
- Predictive Analytics — CLV, churn risk, gender, next order date (auto-calculated by Klaviyo)
- Consent — Email subscription status, SMS consent, double opt-in status
Events (Metrics)
Standard e-commerce events synced from integration:
Placed Order— Order completed with line items, total, discountOrdered Product— Individual product from an orderStarted Checkout— Checkout initiatedAdded to Cart— Item added to cartViewed Product— Product detail page viewedActive on Site— Web activity trackedReceived Email,Opened Email,Clicked Email— Engagement eventsReceived SMS,Clicked SMS— SMS engagement
Lists vs Segments
- Lists — Static groups (opt-in forms, imports). Used for sending campaigns.
- Segments — Dynamic, condition-based. Auto-update as profiles match/unmatch criteria.
- Best practice: Use segments for targeting, lists for opt-in tracking.
Flow Builder Reference
Trigger Types
| Trigger | Description | Common Use |
|---|---|---|
| List | Profile added to a list | Welcome series |
| Segment | Profile enters a segment | VIP, winback |
| Metric | Event occurs | Abandoned cart, post-purchase |
| Date | Based on date property | Birthday, anniversary |
| Price Drop | Product price decreases | Price drop alerts |
| Back in Stock | Product becomes available | Restock notifications |
Flow Actions
- Email — Send email with template
- SMS — Send SMS/MMS
- Push Notification — Mobile push
- Webhook — HTTP POST to external service
- Update Profile Property — Set/modify a profile field
- Conditional Split — IF/ELSE based on conditions
- Trigger Split — Branch based on event properties
- A/B Split — Random percentage split for testing
Flow Filters
Applied at the flow level (not individual messages):
- Profile properties (e.g., has placed order = true)
- Segment membership (e.g., is in VIP segment)
- List membership
- Consent status
- Custom properties
Time Delays
- Time Delay — Fixed wait (hours, days)
- Smart Send Time — Optimized per recipient
- Wait until specific day/time — e.g., next Tuesday at 10am
Segmentation Conditions
Behavioral
- Has/has not done [event] in [time period]
- [Event] count >/</= [number] in [time period]
- [Event] property matches [value]
Profile
- Profile property is/is not/contains [value]
- Is in / not in [list]
- Is in / not in [segment]
- Consent status (subscribed, unsubscribed, never subscribed)
Predictive (Klaviyo AI)
- Predicted CLV is above/below [value]
- Predicted churn risk is high/medium/low
- Predicted gender is male/female
- Predicted next order date is within [days]
Engagement
- Has/has not opened email in [days]
- Has/has not clicked email in [days]
- Has/has not opened SMS in [days]
API Reference (v2)
Base URL
https://a.klaviyo.com/api/Authentication
Authorization: Klaviyo-API-Key {private-api-key}Key Endpoints
Profiles
GET /profiles/ # List profiles
POST /profiles/ # Create profile
GET /profiles/{id}/ # Get profile
PATCH /profiles/{id}/ # Update profile
POST /profile-subscription-bulk-create-jobs/ # Subscribe profilesEvents (Metrics)
POST /events/ # Create event
GET /events/ # Query events
GET /metrics/ # List available metrics
POST /metric-aggregates/ # Aggregate metric dataLists & Segments
GET /lists/ # List all lists
POST /lists/ # Create list
GET /segments/ # List all segments
GET /segments/{id}/profiles/ # Get segment membersCampaigns
GET /campaigns/ # List campaigns
POST /campaigns/ # Create campaign
POST /campaign-send-jobs/ # Send campaignFlows
GET /flows/ # List flows
GET /flows/{id}/ # Get flow details
PATCH /flows/{id}/ # Update flow statusCatalogs
POST /catalog-items/ # Create catalog item
GET /catalog-items/ # List catalog items
PATCH /catalog-items/{id}/ # Update catalog itemWebhooks
Klaviyo can send webhooks for:
- Profile subscribed/unsubscribed
- Email bounced/marked spam
- SMS consent changes
- Custom event triggers via flows
Email Deliverability Reference
Authentication Records
# SPF (add to your DNS)
v=spf1 include:_spf.klaviyo.com ~all
# DKIM (Klaviyo provides the records)
# Add CNAME records provided in Klaviyo Settings > Email > Domains
# DMARC (recommended)
v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.comSending Domain Setup
1. Add your domain in Klaviyo Settings > Email > Domains 2. Add the 3 CNAME records (2 DKIM + 1 Return-Path) to your DNS 3. Verify in Klaviyo (can take up to 48 hours) 4. Dedicated sending domain recommended for high-volume senders
IP Warming Schedule (Dedicated IP)
| Day | Daily Volume |
|---|---|
| 1-2 | 500 |
| 3-4 | 1,000 |
| 5-6 | 2,500 |
| 7-8 | 5,000 |
| 9-10 | 10,000 |
| 11-14 | 25,000 |
| 15-21 | 50,000 |
| 22-28 | 100,000 |
| 29+ | Full volume |
During warm-up: Send only to most engaged contacts. Monitor bounce rates and spam complaints daily.
SMS Reference
Compliance Requirements
- TCPA — Express written consent required before sending
- Quiet Hours — Default: No SMS 9pm-9am recipient's local time
- Opt-out — Must honor STOP/UNSUBSCRIBE immediately
- Identification — Business name must be in first message
- Frequency disclosure — "Msg frequency varies. Msg & data rates may apply."
- Short Code vs Toll-Free — Short codes for high-volume, toll-free for smaller senders
SMS Character Limits
- Standard SMS: 160 characters (GSM-7 encoding)
- With special characters/emoji: 70 characters (UCS-2 encoding)
- MMS: Up to 1600 characters + media (image/GIF)
- Klaviyo recommendation: Keep under 160 chars, include clear CTA and opt-out
Integration Setup
Shopify
- Install Klaviyo app from Shopify App Store
- Auto-syncs: customers, orders, products, carts
- Events synced: Placed Order, Started Checkout, Added to Cart, Viewed Product
- Catalog synced for dynamic product recommendations
- Discount codes auto-generated for flows
WooCommerce
- Install Klaviyo plugin
- Syncs orders, customers, cart events
- Requires WooCommerce webhook configuration
Custom (API)
import requests
# Track an event
requests.post(
"https://a.klaviyo.com/api/events/",
headers={
"Authorization": "Klaviyo-API-Key {YOUR_KEY}",
"Content-Type": "application/json",
"revision": "2024-10-15"
},
json={
"data": {
"type": "event",
"attributes": {
"metric": {"data": {"type": "metric", "attributes": {"name": "Placed Order"}}},
"profile": {"data": {"type": "profile", "attributes": {"email": "customer@example.com"}}},
"properties": {"OrderId": "12345", "value": 99.99}
}
}
}
)Reporting Metrics Glossary
| Metric | Definition |
|---|---|
| Revenue per Recipient (RPR) | Total attributed revenue / recipients |
| Attributed Revenue | Revenue within attribution window (default: 5-day email click, 24-hour SMS click) |
| Deliverability Rate | (Sent - Bounced) / Sent |
| Unique Open Rate | Unique opens / delivered (affected by Apple MPP) |
| Unique Click Rate | Unique clicks / delivered |
| Click-to-Open Rate (CTOR) | Unique clicks / unique opens |
| Unsubscribe Rate | Unsubscribes / delivered |
| Spam Complaint Rate | Spam complaints / delivered |
| Bounce Rate | (Hard + Soft bounces) / Sent |
| List Growth Rate | (New subscribers - Unsubscribes) / Total list size |
---
Standard E-Commerce Event Property Schemas
Reference schemas for the standard e-commerce events. Use when auditing event tracking or verifying integration completeness.
Placed Order
{
"$value": 149.99,
"OrderId": "ORD-12345",
"Categories": ["Hand Tools", "Safety"],
"ItemNames": ["Torque Wrench 3/8in", "Safety Goggles (Pack/12)"],
"Brands": ["Stanley", "3M"],
"DiscountCode": "WELCOME10",
"DiscountValue": 15.00,
"Items": [
{
"ProductID": "PROD-001",
"SKU": "STN-TW-38",
"ProductName": "Torque Wrench 3/8in",
"Quantity": 2,
"ItemPrice": 24.99,
"RowTotal": 49.98,
"ProductURL": "https://...",
"ImageURL": "https://...",
"Categories": ["Hand Tools"],
"Brand": "Stanley"
}
],
"ItemCount": 3,
"ShippingCost": 0.00,
"Tax": 12.00
}Critical: $value = revenue property for attribution. Categories (top-level) needed for segmentation since Items[].Categories is nested and not accessible in segments.
Started Checkout
{
"$value": 149.99,
"CheckoutURL": "https://example.com/checkout/abc123",
"ItemNames": ["Torque Wrench 3/8in", "Safety Goggles (Pack/12)"],
"Items": [{ "ProductID": "...", "ProductName": "...", "Quantity": 1, "ItemPrice": 24.99, "ImageURL": "..." }],
"ItemCount": 3
}Critical: CheckoutURL required for abandoned cart email CTAs. $value enables cart value filters.
Viewed Product
{
"ProductName": "Torque Wrench 3/8in",
"ProductID": "PROD-001",
"Categories": ["Hand Tools", "Automotive"],
"Brand": "Stanley",
"Price": 24.99,
"CompareAtPrice": 29.99,
"ImageURL": "https://...",
"URL": "https://..."
}Critical: No $value (not a revenue event). URL and ImageURL required for browse abandonment personalization. Categories at top level enables segment filters.
Added to Cart
{
"$value": 24.99,
"AddedItemProductName": "Torque Wrench 3/8in",
"AddedItemProductID": "PROD-001",
"AddedItemPrice": 24.99,
"AddedItemQuantity": 2,
"ItemNames": ["Torque Wrench 3/8in", "Safety Goggles (Pack/12)"],
"Items": [{ "ProductID": "...", "ProductName": "...", "Quantity": 1, "ItemPrice": 24.99 }]
}Critical: $value = added item price (not total cart). AddedItem* properties are top-level and segmentable. Items[] = full cart for template rendering.
---
Flow Configuration Best Practices
Smart Send Settings
| Setting | When to Enable | When to Disable |
|---|---|---|
| Smart Sending ON (16h window) | Promotional flows (browse abandonment, winback) | Time-sensitive flows (abandoned cart, transactional) |
| Smart Sending OFF | Abandoned cart, order confirmation, shipping | Never for promotional flows |
| Skip recently emailed ON | Browse abandonment, cross-sell | Welcome series (first impression), abandoned cart |
Timing Benchmarks
| Flow | Email 1 | Email 2 | Email 3 | Email 4+ |
|---|---|---|---|---|
| Welcome Series | Immediate | 1 day | 3 days | 5-7 days |
| Abandoned Cart | 1 hour | 24 hours | 48-72 hours | — |
| Browse Abandonment | 2-4 hours | 24 hours | — | — |
| Post-Purchase | Immediate (confirmation) | 3 days (cross-sell) | 7 days (review) | 14+ days (replenish) |
| Winback | 60 days post-purchase | 75 days | 90 days (last chance) | — |
| Sunset | 90 days unengaged | 104 days | Suppress at 120 days | — |
Exclusion Filter Patterns
| Flow | Must Exclude | Why |
|---|---|---|
| Welcome | Existing customers (Placed Order ever) | Different messaging for buyers vs browsers |
| Abandoned Cart | Placed Order in last 4 hours | They already bought |
| Browse Abandonment | Started Checkout in last 1 hour, Placed Order in last 24 hours | Higher-intent flow takes priority |
| Winback | Placed Order in last 60 days | They're still active |
| Sunset | Placed Order in last 90 days | Buyers get different treatment |
---
A/B Testing Framework
Univariate Testing Rules
1. Test ONE variable at a time — Subject line OR send time OR content, never multiple 2. Define the KPI before launching — Open rate for subject lines, click rate for content, RPR for offers 3. Run to statistical significance — Minimum 95% confidence (p < 0.05) 4. Equal random split — 50/50 for flows, 10/10/80 for campaigns (test on 20%, send winner to 80%)
Minimum Sample Sizes
| Baseline Rate | Detectable Lift | Min Sample (per variant) |
|---|---|---|
| 20% open rate | 10% relative (to 22%) | 7,500 |
| 20% open rate | 20% relative (to 24%) | 2,000 |
| 3% click rate | 20% relative (to 3.6%) | 12,000 |
| 3% click rate | 30% relative (to 3.9%) | 5,500 |
| 1% conversion | 30% relative (to 1.3%) | 18,000 |
Sequential Test Plan Template
Test 1 (Week 1-2): Subject Line
Variable: Urgency vs Benefit-led
KPI: Open rate
Duration: 7 days or 5,000 recipients per variant
Winner → Becomes default
Test 2 (Week 3-4): CTA Placement
Variable: Above-fold button vs Below-content button
KPI: Click rate
Duration: 7 days or 5,000 recipients per variant
Winner → Becomes default
Test 3 (Week 5-6): Offer Type
Variable: % discount vs $ discount vs free shipping
KPI: Revenue per recipient
Duration: 14 days or 3,000 recipients per variant
Winner → Becomes default---
Segment Architecture Patterns
Mutually Exclusive Engagement Tiers
Build engagement tiers that don't overlap — each profile falls into exactly one tier:
| Tier | Condition | Sending Strategy |
|---|---|---|
| Active (0-30d) | Opened OR clicked email in last 30 days | Full frequency (3-5x/week) |
| Warm (31-90d) | NOT Active AND (opened OR clicked in last 90 days) | Moderate (2x/week) |
| At-Risk (91-180d) | NOT Active AND NOT Warm AND (opened OR clicked in last 180 days) | Reduced (1x/week), re-engagement content |
| Lapsed (180d+) | NOT Active AND NOT Warm AND NOT At-Risk AND has received email | Sunset flow only, then suppress |
| Never Engaged | Has received email AND has never opened or clicked | Suppress immediately |
RFM Segment Definitions
| Segment | Recency | Frequency | Monetary | Action |
|---|---|---|---|---|
| Champions | Ordered L30d | 4+ orders L12M | Top 20% AOV | VIP treatment, early access |
| Loyal | Ordered L60d | 3+ orders L12M | Above avg AOV | Reward programs, referral asks |
| Promising | Ordered L90d | 1-2 orders L12M | Any | Nurture to loyalty, product education |
| At Risk | No order L90d | 2+ orders ever | Above avg lifetime | Winback flow, incentives |
| Lost | No order L180d | Any | Any | Last-chance offer, then sunset |
Predictive Segment Setup (Klaviyo AI)
| Segment | Condition | Use |
|---|---|---|
| High CLV | Predicted CLV > 75th percentile | VIP flow trigger, premium offers |
| Churn Risk - High | Predicted churn risk = High | Immediate re-engagement, incentive |
| Churn Risk - Medium | Predicted churn risk = Medium | Nurture content, value reminders |
| Next Order < 14 days | Predicted next order date within 14 days | Replenishment reminder timing |
| Gender - Female | Predicted gender = Female | Content personalization (if relevant) |
---
SOW / PRD Template Structure
When producing implementation specs (SOWs) for audit recommendations:
# [Title]
## Summary
- **Priority**: P1/P2/P3/P4
- **Estimated Impact**: $X-$Y/year or % improvement
- **Complexity**: Low/Medium/High
- **Timeline**: X days/weeks
- **Owner**: Client / Agency / Shared
## Problem Statement
[What's wrong, with evidence from audit data]
## Recommendation (Client-Facing)
[What to do and why, in plain language]
## Implementation Spec (Internal)
### Prerequisites
[What must be done first]
### Step-by-Step Build
[Detailed Klaviyo UI steps OR API/code steps]
### Trigger & Filter Configuration
[Exact trigger type, filter conditions, segment criteria]
### Content Brief
[Subject lines, email structure, CTA copy, dynamic content blocks]
### Timing & Logic
[Delays, conditional splits, Smart Send settings]
### Testing Plan
[A/B test variables, sample size, success criteria, stat sig threshold]
### Success Metrics
[KPIs to track, measurement period, benchmarks]
## Dependencies
[What other SOWs must complete first]---
MCP Server Reference
The official Klaviyo MCP server is the recommended way for Claude (Code, Chat, or Cowork) to interact with a Klaviyo account. It wraps the Klaviyo API behind 40+ MCP tools across ten categories.
Connection
| Mode | Setup |
|---|---|
| Remote (recommended) | URL https://mcp.klaviyo.com/mcp · Transport: Streamable HTTP · Auth: OAuth (dynamic client registration) |
| Local | uvx klaviyo-mcp-server@latest |
| Read-only mode | Append ?read-only=true to the remote URL — disables all write tools |
| API revision | Currently pinned to 2026-04-15 |
Required Klaviyo user role
Owner, Admin, or Manager. Lower-permission users cannot authorize the MCP.
Tool Inventory
Accounts (read-only)
get_account_details— account config, timezone, integrations
Campaigns
get_campaigns(read) — list campaigns with filtersget_campaign(read) — fetch a single campaigncreate_campaign(write)assign_template_to_campaign_message(write)
Catalogs (read-only)
get_catalog_items— products synced into Klaviyo's catalog
Events & Metrics
get_events(read) — query event payloads with filters (use for inspecting nested property structure)create_event(write) — track a custom eventget_metrics(read) — list all metric names + IDsget_metric(read) — fetch one metric (including property schema)query_metric_aggregates(read) — time-series rollups (mirrors in-app Metric Reporting)
Flows (read-only)
get_flows— list all flows with statusget_flow— full flow definition (trigger, actions, filters)
Groups (Lists + Segments, read-only)
get_lists/get_listget_segments/get_segment(segment condition definitions)
Images (write-only)
upload_image_from_fileupload_image_from_url
Profiles
get_profiles(read)get_profile(read)create_profile(write)update_profile(write)subscribe_profile_to_marketing(write)unsubscribe_profile_from_marketing(write)
Reporting (read-only)
get_campaign_report— opens, clicks, unsubs, revenue per campaignget_flow_report— revenue, conversion, engagement per flow
Templates
get_email_template(read)create_email_template(write)
Translations (beta)
get_translations/get_translation(read)create_translation/update_translation/delete_translation(write)
When to use the MCP vs. the script fallback
| Scenario | Use |
|---|---|
| Interactive audit in Claude Code/Chat | MCP |
| Unattended audit in Cowork | MCP (with ?read-only=true) |
| Building/editing campaigns or templates with Claude | MCP (write tools, read-write URL) |
| Headless CI/cron data export | Script (KLAVIYO_API_KEY env var) |
| Pipeline integration tests | Script |
| Pulling data for an offline notebook | Either — MCP if available, script otherwise |
# Klaviyo Analyst Skill Dependencies
# Install with: pip install -r requirements.txt
klaviyo-api>=9.0.0,<23.0.0
python-dotenv>=1.0.0,<2.0.0
#!/usr/bin/env python3
"""
Klaviyo Marketing Analysis Tool
Performs higher-level analysis on Klaviyo data including:
- Flow audit against essential flows checklist
- Segment health assessment
- Campaign performance comparison
- Deliverability diagnostics
- Revenue attribution analysis
Usage:
python analyze.py --analysis-type full-audit
python analyze.py --analysis-type flow-audit
python analyze.py --analysis-type segment-health
python analyze.py --analysis-type campaign-comparison --days 30
python analyze.py --analysis-type deliverability
python analyze.py --analysis-type revenue-attribution
"""
import os
import sys
import json
import argparse
import traceback
from typing import Dict, List, Optional
try:
from klaviyo_client import KlaviyoAnalyticsClient
except ImportError:
print("Error: klaviyo_client.py not found in the same directory", file=sys.stderr)
sys.exit(1)
# Per-phase iteration caps. Klaviyo reporting is rate-limited
# (~2/min on standard accounts), so unbounded loops can exhaust the
# SDK retry budget on large accounts. These match the prior implicit
# [:20] cap; raise via code edit only.
MAX_FLOW_REPORTS = 20
MAX_CAMPAIGN_REPORTS = 20
def _safe_output_path(path: str) -> str:
"""Validate output path does not escape working directory."""
resolved = os.path.realpath(path)
cwd = os.path.realpath(os.getcwd())
if not resolved.startswith(cwd + os.sep) and resolved != cwd:
raise ValueError(f"Output path must be within working directory: {cwd}")
return resolved
class KlaviyoAnalyzer:
"""Performs marketing analysis on Klaviyo account data."""
ESSENTIAL_FLOWS = [
{"name": "Welcome Series", "trigger": "List Subscribe", "priority": "CRITICAL"},
{"name": "Abandoned Cart", "trigger": "Started Checkout", "priority": "CRITICAL"},
{"name": "Browse Abandonment", "trigger": "Viewed Product", "priority": "HIGH"},
{"name": "Post-Purchase", "trigger": "Placed Order", "priority": "CRITICAL"},
{"name": "Winback", "trigger": "Time Since Last Purchase", "priority": "HIGH"},
{"name": "Sunset/Re-engagement", "trigger": "Engagement Date", "priority": "HIGH"},
{"name": "Review Request", "trigger": "Fulfilled Order", "priority": "MEDIUM"},
{"name": "Replenishment", "trigger": "Predicted Next Order", "priority": "MEDIUM"},
{"name": "Birthday/Anniversary", "trigger": "Date Property", "priority": "LOW"},
{"name": "VIP/Loyalty", "trigger": "Segment Entry", "priority": "MEDIUM"},
]
BENCHMARKS = {
"open_rate": {"good": 0.20, "great": 0.30, "warning": 0.15},
"click_rate": {"good": 0.02, "great": 0.04, "warning": 0.015},
"unsubscribe_rate": {"good": 0.003, "great": 0.001, "warning": 0.005},
"spam_complaint_rate": {"good": 0.0005, "great": 0.0002, "warning": 0.001},
"flow_revenue_pct": {"good": 0.30, "great": 0.50, "warning": 0.20},
"bounce_rate": {"good": 0.02, "great": 0.01, "warning": 0.05},
"list_growth_rate": {"good": 0.03, "great": 0.08, "warning": 0.01},
}
def __init__(self):
"""Initialize the analyzer with Klaviyo client."""
self.client = KlaviyoAnalyticsClient()
def audit_flows(self) -> Dict:
"""
Audit flows against the essential flows checklist.
Returns:
Dictionary with flow inventory, gaps, and recommendations
"""
flows = self.client.get_flows()
# Map existing flows
flow_names_lower = [f.get("name", "").lower() for f in flows]
checklist = []
found_count = 0
for essential in self.ESSENTIAL_FLOWS:
# Fuzzy match: check if essential flow name keywords appear
keywords = essential["name"].lower().split()
matched = any(
all(kw in flow_name for kw in keywords)
for flow_name in flow_names_lower
)
# Also check common alternate names
alt_names = {
"welcome series": ["welcome", "onboarding"],
"abandoned cart": ["abandon", "cart recovery", "checkout abandon"],
"browse abandonment": ["browse abandon", "viewed product"],
"post-purchase": ["post purchase", "thank you", "order follow"],
"winback": ["win back", "win-back", "lapsed", "re-engage"],
"sunset/re-engagement": ["sunset", "re-engagement", "sunsetting"],
"review request": ["review", "feedback request"],
"replenishment": ["replenish", "reorder", "restock"],
"birthday/anniversary": ["birthday", "anniversary", "bday"],
"vip/loyalty": ["vip", "loyalty", "high value"],
}
if not matched:
alts = alt_names.get(essential["name"].lower(), [])
matched = any(
any(alt in flow_name for alt in alts)
for flow_name in flow_names_lower
)
status = "found" if matched else "missing"
if matched:
found_count += 1
checklist.append({
"flow": essential["name"],
"trigger": essential["trigger"],
"priority": essential["priority"],
"status": status,
})
# Count active vs inactive
active_flows = [f for f in flows if f.get("status") == "live"]
audit = {
"summary": {
"total_flows": len(flows),
"active_flows": len(active_flows),
"inactive_flows": len(flows) - len(active_flows),
"essential_found": found_count,
"essential_missing": len(self.ESSENTIAL_FLOWS) - found_count,
"coverage_score": f"{found_count}/{len(self.ESSENTIAL_FLOWS)}",
},
"checklist": checklist,
"all_flows": [
{
"name": f.get("name"),
"status": f.get("status"),
"id": f.get("id"),
}
for f in flows
],
"recommendations": self._recommend_flow_improvements(checklist),
}
return audit
def analyze_segments(self) -> Dict:
"""
Assess segment health including engagement tiers and structure.
Returns:
Dictionary with segment analysis and recommendations
"""
segments = self.client.get_segments()
lists = self.client.get_lists()
# Categorize segments by engagement keywords
engagement_tiers = {
"active": [],
"warm": [],
"at_risk": [],
"lapsed": [],
"suppression": [],
"other": [],
}
tier_keywords = {
"active": ["active", "engaged", "30 day", "30d", "recent"],
"warm": ["warm", "60 day", "90 day", "60d", "90d"],
"at_risk": ["at risk", "at-risk", "cooling", "fading"],
"lapsed": ["lapsed", "inactive", "unengaged", "180 day", "180d"],
"suppression": ["suppress", "sunset", "never engaged", "do not"],
}
for segment in segments:
seg_name = segment.get("name", "").lower()
categorized = False
for tier, keywords in tier_keywords.items():
if any(kw in seg_name for kw in keywords):
engagement_tiers[tier].append(segment.get("name"))
categorized = True
break
if not categorized:
engagement_tiers["other"].append(segment.get("name"))
# Check for RFM segments
rfm_keywords = ["rfm", "recency", "frequency", "monetary", "clv", "ltv"]
has_rfm = any(
any(kw in seg.get("name", "").lower() for kw in rfm_keywords)
for seg in segments
)
# Check for predictive segments
predictive_keywords = ["predicted", "churn", "next order", "likely to"]
has_predictive = any(
any(kw in seg.get("name", "").lower() for kw in predictive_keywords)
for seg in segments
)
analysis = {
"summary": {
"total_segments": len(segments),
"total_lists": len(lists),
"has_engagement_tiers": bool(
engagement_tiers["active"] or engagement_tiers["warm"]
),
"has_rfm_segments": has_rfm,
"has_predictive_segments": has_predictive,
"has_suppression_segment": bool(engagement_tiers["suppression"]),
},
"engagement_tiers": {
tier: {"count": len(segs), "segments": segs}
for tier, segs in engagement_tiers.items()
},
"lists": [{"name": l.get("name"), "id": l.get("id")} for l in lists],
"recommendations": self._recommend_segment_improvements(
engagement_tiers, has_rfm, has_predictive
),
}
return analysis
def compare_campaigns(self, days: int = 30) -> Dict:
"""
Compare campaign performance against benchmarks.
Args:
days: Number of days to analyze
Returns:
Dictionary with campaign metrics and benchmark comparison
"""
campaigns = self.client.get_campaigns()
# Attempt to get reports for recent campaigns
campaign_data = []
for campaign in campaigns[:MAX_CAMPAIGN_REPORTS]:
campaign_id = campaign.get("id")
if not campaign_id:
continue
try:
report = self.client.get_campaign_report(campaign_id)
if report:
campaign_data.append({
"name": campaign.get("name"),
"id": campaign_id,
"status": campaign.get("status"),
"report": report,
})
except Exception:
campaign_data.append({
"name": campaign.get("name"),
"id": campaign_id,
"status": campaign.get("status"),
"report": None,
})
# Calculate aggregate metrics where reports are available
total_recipients = 0
total_opens = 0
total_clicks = 0
total_bounces = 0
total_unsubscribes = 0
total_complaints = 0
total_revenue = 0.0
campaigns_with_data = 0
for c in campaign_data:
report = c.get("report")
if not report:
continue
stats = report if isinstance(report, dict) else {}
recipients = stats.get("recipients", 0) or 0
if recipients > 0:
campaigns_with_data += 1
total_recipients += recipients
total_opens += stats.get("opens_unique", 0) or 0
total_clicks += stats.get("clicks_unique", 0) or 0
total_bounces += stats.get("bounced", 0) or 0
total_unsubscribes += stats.get("unsubscribes", 0) or 0
total_complaints += stats.get("spam_complaints", 0) or 0
total_revenue += float(stats.get("conversion_value", 0) or 0)
# Calculate rates
if total_recipients > 0:
avg_metrics = {
"open_rate": total_opens / total_recipients,
"click_rate": total_clicks / total_recipients,
"bounce_rate": total_bounces / total_recipients,
"unsubscribe_rate": total_unsubscribes / total_recipients,
"spam_complaint_rate": total_complaints / total_recipients,
}
else:
avg_metrics = {
"open_rate": 0,
"click_rate": 0,
"bounce_rate": 0,
"unsubscribe_rate": 0,
"spam_complaint_rate": 0,
}
# Assess each metric against benchmarks
assessments = {}
for metric, value in avg_metrics.items():
assessments[metric] = self._assess_metric(metric, value)
analysis = {
"period": f"Last {days} days",
"summary": {
"total_campaigns": len(campaigns),
"campaigns_analyzed": campaigns_with_data,
"total_recipients": total_recipients,
"total_revenue": round(total_revenue, 2),
},
"aggregate_metrics": {
k: round(v * 100, 2) for k, v in avg_metrics.items()
},
"benchmark_assessment": assessments,
"campaigns": [
{"name": c["name"], "status": c["status"]} for c in campaign_data
],
"recommendations": self._recommend_campaign_improvements(
avg_metrics, assessments
),
}
return analysis
def check_deliverability(self) -> Dict:
"""
Run deliverability diagnostics on recent sending data.
Returns:
Dictionary with deliverability status and recommendations
"""
campaigns = self.client.get_campaigns()
# Aggregate deliverability metrics
total_sent = 0
total_bounces = 0
total_complaints = 0
total_delivered = 0
campaigns_checked = 0
for campaign in campaigns[:MAX_CAMPAIGN_REPORTS]:
campaign_id = campaign.get("id")
if not campaign_id:
continue
try:
report = self.client.get_campaign_report(campaign_id)
if not report:
continue
stats = report if isinstance(report, dict) else {}
recipients = stats.get("recipients", 0) or 0
if recipients > 0:
campaigns_checked += 1
total_sent += recipients
total_bounces += stats.get("bounced", 0) or 0
total_complaints += stats.get("spam_complaints", 0) or 0
total_delivered += stats.get("delivered", 0) or 0
except Exception:
continue
# Calculate rates
if total_sent > 0:
bounce_rate = total_bounces / total_sent
complaint_rate = total_complaints / total_sent
delivery_rate = total_delivered / total_sent
else:
bounce_rate = 0
complaint_rate = 0
delivery_rate = 0
# Assess
bounce_assessment = self._assess_metric("bounce_rate", bounce_rate)
complaint_assessment = self._assess_metric("spam_complaint_rate", complaint_rate)
# Check for critical thresholds
issues = []
if bounce_rate > 0.05:
issues.append({
"severity": "CRITICAL",
"issue": "High bounce rate",
"detail": f"Bounce rate {bounce_rate * 100:.2f}% exceeds 5% threshold",
"impact": "ISPs may throttle or block your sending",
})
if complaint_rate > 0.001:
issues.append({
"severity": "CRITICAL",
"issue": "High spam complaint rate",
"detail": f"Complaint rate {complaint_rate * 100:.3f}% exceeds 0.1% threshold",
"impact": "Risk of being blacklisted by major ISPs",
})
if bounce_rate > 0.02:
issues.append({
"severity": "HIGH",
"issue": "Elevated bounce rate",
"detail": f"Bounce rate {bounce_rate * 100:.2f}% above 2% benchmark",
"impact": "Sender reputation degradation over time",
})
if complaint_rate > 0.0005:
issues.append({
"severity": "HIGH",
"issue": "Elevated complaint rate",
"detail": f"Complaint rate {complaint_rate * 100:.3f}% above 0.05% benchmark",
"impact": "Gradual inbox placement decline",
})
analysis = {
"summary": {
"campaigns_checked": campaigns_checked,
"total_sent": total_sent,
"total_delivered": total_delivered,
"delivery_rate": round(delivery_rate * 100, 2),
"bounce_rate": round(bounce_rate * 100, 2),
"complaint_rate": round(complaint_rate * 100, 3),
},
"assessments": {
"bounce_rate": bounce_assessment,
"complaint_rate": complaint_assessment,
},
"issues": issues,
"authentication_checklist": [
{"record": "SPF", "note": "Verify include:_spf.klaviyo.com in DNS"},
{"record": "DKIM", "note": "Verify CNAME records from Klaviyo Settings"},
{"record": "DMARC", "note": "Verify DMARC policy (p=quarantine or p=reject)"},
],
"recommendations": self._recommend_deliverability_fixes(
bounce_rate, complaint_rate, issues
),
}
return analysis
def revenue_attribution(self) -> Dict:
"""
Analyze revenue split between flows and campaigns.
Returns:
Dictionary with revenue attribution data and recommendations
"""
# Get flow and campaign revenue. Filter to live flows only —
# paused/archived flows can't generate new revenue and reporting
# on them wastes Klaviyo's rate-limited reporting budget.
flows = self.client.get_flows(filter_str='equals(status,"live")')
campaigns = self.client.get_campaigns()
flow_revenue = 0.0
flow_data = []
for flow in flows[:MAX_FLOW_REPORTS]:
flow_id = flow.get("id")
if not flow_id:
continue
try:
report = self.client.get_flow_report(flow_id)
if report:
stats = report if isinstance(report, dict) else {}
rev = float(stats.get("conversion_value", 0) or 0)
flow_revenue += rev
if rev > 0:
flow_data.append({
"name": flow.get("name"),
"revenue": round(rev, 2),
"status": flow.get("status"),
})
except Exception:
continue
campaign_revenue = 0.0
campaign_data = []
for campaign in campaigns[:MAX_CAMPAIGN_REPORTS]:
campaign_id = campaign.get("id")
if not campaign_id:
continue
try:
report = self.client.get_campaign_report(campaign_id)
if report:
stats = report if isinstance(report, dict) else {}
rev = float(stats.get("conversion_value", 0) or 0)
campaign_revenue += rev
if rev > 0:
campaign_data.append({
"name": campaign.get("name"),
"revenue": round(rev, 2),
})
except Exception:
continue
total_revenue = flow_revenue + campaign_revenue
flow_pct = (flow_revenue / total_revenue * 100) if total_revenue > 0 else 0
campaign_pct = (campaign_revenue / total_revenue * 100) if total_revenue > 0 else 0
# Sort by revenue
flow_data.sort(key=lambda x: x["revenue"], reverse=True)
campaign_data.sort(key=lambda x: x["revenue"], reverse=True)
# Assess flow revenue percentage
flow_rev_assessment = self._assess_metric(
"flow_revenue_pct", flow_pct / 100 if flow_pct else 0
)
analysis = {
"summary": {
"total_revenue": round(total_revenue, 2),
"flow_revenue": round(flow_revenue, 2),
"campaign_revenue": round(campaign_revenue, 2),
"flow_revenue_pct": round(flow_pct, 1),
"campaign_revenue_pct": round(campaign_pct, 1),
},
"assessment": flow_rev_assessment,
"top_flows": flow_data[:10],
"top_campaigns": campaign_data[:10],
"recommendations": self._recommend_revenue_improvements(
flow_pct, flow_data, campaign_data
),
}
return analysis
def full_audit(self) -> Dict:
"""
Run a complete account audit combining all analysis types.
Returns:
Dictionary with all audit sections
"""
return {
"flow_audit": self.audit_flows(),
"segment_health": self.analyze_segments(),
"campaign_performance": self.compare_campaigns(),
"deliverability": self.check_deliverability(),
"revenue_attribution": self.revenue_attribution(),
}
def _assess_metric(self, name: str, value: float) -> Dict:
"""
Compare a metric against benchmarks.
Args:
name: Metric name (must exist in BENCHMARKS)
value: Metric value as decimal (e.g., 0.25 for 25%)
Returns:
Dictionary with rating and context
"""
bench = self.BENCHMARKS.get(name)
if not bench:
return {"rating": "unknown", "value": value}
# For rates where lower is better
lower_is_better = name in [
"unsubscribe_rate",
"spam_complaint_rate",
"bounce_rate",
]
if lower_is_better:
if value <= bench["great"]:
rating = "great"
elif value <= bench["good"]:
rating = "good"
elif value <= bench["warning"]:
rating = "warning"
else:
rating = "critical"
else:
if value >= bench["great"]:
rating = "great"
elif value >= bench["good"]:
rating = "good"
elif value >= bench["warning"]:
rating = "warning"
else:
rating = "critical"
return {
"rating": rating,
"value": round(value * 100, 2),
"benchmark_good": round(bench["good"] * 100, 2),
"benchmark_great": round(bench["great"] * 100, 2),
}
def _recommend_flow_improvements(self, checklist: List[Dict]) -> List[Dict]:
"""Generate recommendations for flow gaps and improvements."""
recommendations = []
missing_flows = [f for f in checklist if f["status"] == "missing"]
for flow in missing_flows:
recommendations.append({
"priority": flow["priority"],
"action": f"Create {flow['flow']} flow",
"reason": f"Missing essential flow (trigger: {flow['trigger']})",
"expected_impact": self._flow_impact_estimate(flow["flow"]),
})
if not missing_flows:
recommendations.append({
"priority": "INFO",
"action": "All essential flows present",
"reason": "Focus on optimizing existing flows",
"expected_impact": "Incremental 10-20% improvement per flow",
})
return recommendations
def _recommend_segment_improvements(
self, tiers: Dict, has_rfm: bool, has_predictive: bool
) -> List[Dict]:
"""Generate recommendations for segment strategy."""
recommendations = []
if not tiers.get("active") and not tiers.get("warm"):
recommendations.append({
"priority": "HIGH",
"action": "Create engagement tier segments",
"reason": "No engagement-based segmentation found",
"expected_impact": "20-30% improvement in campaign targeting",
})
if not tiers.get("suppression"):
recommendations.append({
"priority": "HIGH",
"action": "Create suppression segment",
"reason": "No suppression segment for unengaged contacts",
"expected_impact": "Protect deliverability, reduce costs",
})
if not has_rfm:
recommendations.append({
"priority": "MEDIUM",
"action": "Implement RFM segmentation",
"reason": "No RFM-based segments detected",
"expected_impact": "15-25% lift in campaign revenue",
})
if not has_predictive:
recommendations.append({
"priority": "MEDIUM",
"action": "Enable predictive segments",
"reason": "Not using Klaviyo predictive analytics (CLV, churn risk)",
"expected_impact": "10-20% better targeting for winback and VIP flows",
})
return recommendations
def _recommend_campaign_improvements(
self, metrics: Dict, assessments: Dict
) -> List[Dict]:
"""Generate recommendations for campaign performance."""
recommendations = []
if assessments.get("open_rate", {}).get("rating") in ["warning", "critical"]:
recommendations.append({
"priority": "HIGH",
"action": "Improve subject lines and sender reputation",
"reason": f"Open rate {metrics['open_rate'] * 100:.1f}% below benchmark",
"expected_impact": "20-40% increase in open rates",
})
if assessments.get("click_rate", {}).get("rating") in ["warning", "critical"]:
recommendations.append({
"priority": "HIGH",
"action": "Optimize email content and CTAs",
"reason": f"Click rate {metrics['click_rate'] * 100:.1f}% below benchmark",
"expected_impact": "30-50% increase in click rates",
})
if assessments.get("unsubscribe_rate", {}).get("rating") in [
"warning",
"critical",
]:
recommendations.append({
"priority": "HIGH",
"action": "Reduce send frequency or improve segmentation",
"reason": f"Unsubscribe rate {metrics['unsubscribe_rate'] * 100:.2f}% above threshold",
"expected_impact": "50% reduction in unsubscribes",
})
return recommendations
def _recommend_deliverability_fixes(
self, bounce_rate: float, complaint_rate: float, issues: List[Dict]
) -> List[Dict]:
"""Generate recommendations for deliverability improvement."""
recommendations = []
if bounce_rate > 0.02:
recommendations.append({
"priority": "HIGH",
"action": "Implement list cleaning",
"reason": "Elevated bounce rate indicates stale email addresses",
"expected_impact": "Reduce bounces by 60-80%",
})
if complaint_rate > 0.0005:
recommendations.append({
"priority": "HIGH",
"action": "Review sending frequency and content relevance",
"reason": "Spam complaints above safe threshold",
"expected_impact": "Reduce complaints by 50-70%",
})
recommendations.append({
"priority": "MEDIUM",
"action": "Verify DNS authentication (SPF, DKIM, DMARC)",
"reason": "Authentication protects sender reputation",
"expected_impact": "Improved inbox placement across all ISPs",
})
if not issues:
recommendations.insert(0, {
"priority": "INFO",
"action": "Deliverability metrics within healthy range",
"reason": "No critical issues detected",
"expected_impact": "Continue monitoring",
})
return recommendations
def _recommend_revenue_improvements(
self, flow_pct: float, flow_data: List[Dict], campaign_data: List[Dict]
) -> List[Dict]:
"""Generate recommendations for revenue optimization."""
recommendations = []
if flow_pct < 30:
recommendations.append({
"priority": "HIGH",
"action": "Increase flow automation coverage",
"reason": f"Flow revenue at {flow_pct:.0f}% (benchmark: 30-50%)",
"expected_impact": "Shift 10-20% more revenue to automated flows",
})
if flow_pct > 70:
recommendations.append({
"priority": "MEDIUM",
"action": "Invest in campaign strategy",
"reason": f"Over-reliant on flows ({flow_pct:.0f}% of revenue)",
"expected_impact": "Diversify revenue sources, reduce flow fatigue",
})
if len(flow_data) < 5:
recommendations.append({
"priority": "HIGH",
"action": "Add more revenue-generating flows",
"reason": f"Only {len(flow_data)} flows generating revenue",
"expected_impact": "Each new flow adds 5-15% incremental revenue",
})
return recommendations
def _flow_impact_estimate(self, flow_name: str) -> str:
"""Estimate revenue impact of adding a missing flow."""
impact_map = {
"Welcome Series": "10-15% of total email revenue",
"Abandoned Cart": "15-25% of total email revenue",
"Browse Abandonment": "5-10% of total email revenue",
"Post-Purchase": "8-12% of total email revenue",
"Winback": "5-8% of total email revenue",
"Sunset/Re-engagement": "Protect deliverability, reduce costs",
"Review Request": "Indirect: increases social proof and conversion",
"Replenishment": "5-10% of total email revenue (if applicable)",
"Birthday/Anniversary": "2-5% of total email revenue",
"VIP/Loyalty": "3-8% of total email revenue from high-value segment",
}
return impact_map.get(flow_name, "Incremental revenue improvement")
def main():
parser = argparse.ArgumentParser(
description="Analyze Klaviyo marketing data",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Run full account audit
python analyze.py --analysis-type full-audit
# Audit flows against essential checklist
python analyze.py --analysis-type flow-audit
# Check segment health
python analyze.py --analysis-type segment-health
# Compare campaign performance
python analyze.py --analysis-type campaign-comparison --days 30
# Run deliverability diagnostics
python analyze.py --analysis-type deliverability
# Analyze revenue attribution
python analyze.py --analysis-type revenue-attribution
# Export audit to file
python analyze.py --analysis-type full-audit --output audit.json
""",
)
parser.add_argument(
"--analysis-type",
required=True,
choices=[
"flow-audit",
"segment-health",
"campaign-comparison",
"deliverability",
"revenue-attribution",
"full-audit",
],
help="Type of analysis to perform",
)
parser.add_argument(
"--days",
type=int,
default=30,
help="Number of days to analyze (default: 30)",
)
parser.add_argument(
"--format",
choices=["json", "table"],
default="json",
help="Output format (default: json)",
)
parser.add_argument(
"--output",
help="Output file path (default: stdout)",
)
parser.add_argument(
"--debug",
action="store_true",
help="Re-raise exceptions with full traceback instead of friendly error",
)
args = parser.parse_args()
try:
analyzer = KlaviyoAnalyzer()
# Run analysis
if args.analysis_type == "flow-audit":
result = analyzer.audit_flows()
elif args.analysis_type == "segment-health":
result = analyzer.analyze_segments()
elif args.analysis_type == "campaign-comparison":
result = analyzer.compare_campaigns(days=args.days)
elif args.analysis_type == "deliverability":
result = analyzer.check_deliverability()
elif args.analysis_type == "revenue-attribution":
result = analyzer.revenue_attribution()
elif args.analysis_type == "full-audit":
result = analyzer.full_audit()
# Format output
output = json.dumps(result, indent=2, default=str)
# Write output
if args.output:
safe_path = _safe_output_path(args.output)
with open(safe_path, "w", encoding="utf-8") as f:
f.write(output)
print(f"Analysis saved to {args.output}", file=sys.stderr)
else:
print(output)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
except Exception:
if args.debug:
traceback.print_exc()
else:
print("Error: Analysis failed. Check your API key and network connection.", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Klaviyo Analytics API Client
Read-only SDK wrapper for fetching Klaviyo marketing data including
flows, campaigns, segments, lists, metrics, and performance reports.
Usage:
python klaviyo_client.py --resource flows
python klaviyo_client.py --resource campaigns --format table
python klaviyo_client.py --resource report --report-type flow --id FLOW_ID
python klaviyo_client.py --resource metrics --format json --output metrics.json
Environment Variables:
KLAVIYO_API_KEY: Klaviyo private API key (required, starts with "pk_")
"""
import os
import sys
import json
import argparse
import traceback
from typing import Dict, List, Optional
try:
from klaviyo_api import KlaviyoAPI
from dotenv import load_dotenv
except ImportError:
print("Error: Required packages not installed.", file=sys.stderr)
print("Install with: pip install klaviyo-api python-dotenv", file=sys.stderr)
sys.exit(1)
def _safe_output_path(path: str) -> str:
"""Validate output path does not escape working directory."""
resolved = os.path.realpath(path)
cwd = os.path.realpath(os.getcwd())
if not resolved.startswith(cwd + os.sep) and resolved != cwd:
raise ValueError(f"Output path must be within working directory: {cwd}")
return resolved
class KlaviyoAnalyticsClient:
"""Read-only client for fetching Klaviyo marketing analytics data."""
API_REVISION = "2025-10-15"
def __init__(self):
"""Initialize the client with credentials from environment."""
load_dotenv()
api_key = os.environ.get("KLAVIYO_API_KEY")
if not api_key:
raise ValueError(
"KLAVIYO_API_KEY environment variable not set. "
"Find your API key in Klaviyo: Settings > Account > API Keys"
)
if not api_key.startswith("pk_"):
raise ValueError(
"KLAVIYO_API_KEY should start with 'pk_'. "
"Use a Private API Key, not a Public API Key."
)
try:
self.client = KlaviyoAPI(api_key, max_delay=60, max_retries=3)
except Exception as e:
print(f" caused by: {type(e).__name__}: {e}", file=sys.stderr)
raise RuntimeError("Failed to initialize Klaviyo client. Check your API key.") from e
self._conversion_metric_id: Optional[str] = None
self._flow_report_cache: Dict[str, Dict] = {}
self._campaign_report_cache: Dict[str, Dict] = {}
def _get_conversion_metric_id(self, name: str = "Placed Order") -> str:
"""Resolve a metric name to its Klaviyo metric ID. Cached after first call."""
if self._conversion_metric_id:
return self._conversion_metric_id
metrics = self.get_metrics()
for m in metrics:
if m.get("name") == name:
self._conversion_metric_id = m.get("id")
return self._conversion_metric_id
raise RuntimeError(
f"Conversion metric '{name}' not found in account. "
f"Reporting calls require an alphanumeric metric ID, not a name."
)
def get_flows(self, filter_str: Optional[str] = None) -> List[Dict]:
"""
List all flows with status and trigger information.
Args:
filter_str: Optional filter (e.g., 'equals(status,"live")')
Returns:
List of flow dictionaries with id, name, status, trigger_type
"""
try:
kwargs = {}
if filter_str:
kwargs["filter"] = filter_str
response = self.client.Flows.get_flows(**kwargs)
return self._parse_jsonapi_response(response)
except Exception as e:
print(f" caused by: {type(e).__name__}: {e}", file=sys.stderr)
raise RuntimeError("Failed to fetch flows. Check API key scopes.") from e
def get_campaigns(self, filter_str: Optional[str] = None) -> List[Dict]:
"""
List campaigns with send status and performance summary.
Args:
filter_str: Optional filter (e.g., 'equals(messages.channel,"email")')
Returns:
List of campaign dictionaries
"""
try:
# Klaviyo API requires a filter on messages.channel; default to email.
kwargs = {"filter": filter_str or 'equals(messages.channel,"email")'}
response = self.client.Campaigns.get_campaigns(**kwargs)
return self._parse_jsonapi_response(response)
except Exception as e:
print(f" caused by: {type(e).__name__}: {e}", file=sys.stderr)
raise RuntimeError("Failed to fetch campaigns. Check API key scopes.") from e
def get_segments(self) -> List[Dict]:
"""
List all segments.
Returns:
List of segment dictionaries with id, name, and profile count
"""
try:
response = self.client.Segments.get_segments()
return self._parse_jsonapi_response(response)
except Exception as e:
print(f" caused by: {type(e).__name__}: {e}", file=sys.stderr)
raise RuntimeError("Failed to fetch segments. Check API key scopes.") from e
def get_lists(self) -> List[Dict]:
"""
List all lists.
Returns:
List of list dictionaries with id, name, and profile count
"""
try:
response = self.client.Lists.get_lists()
return self._parse_jsonapi_response(response)
except Exception as e:
print(f" caused by: {type(e).__name__}: {e}", file=sys.stderr)
raise RuntimeError("Failed to fetch lists. Check API key scopes.") from e
def get_flow_report(self, flow_id: str) -> Dict:
"""
Get performance metrics for a specific flow.
Args:
flow_id: The flow ID to get a report for
Returns:
Dictionary with flow performance metrics
"""
if flow_id in self._flow_report_cache:
return self._flow_report_cache[flow_id]
try:
body = {
"data": {
"type": "flow-values-report",
"attributes": {
"statistics": [
"opens",
"opens_unique",
"clicks",
"clicks_unique",
"recipients",
"delivered",
"bounced",
"unsubscribes",
"spam_complaints",
"conversion_value",
],
"timeframe": {"key": "last_365_days"},
"conversion_metric_id": self._get_conversion_metric_id(),
"filter": f"equals(flow_id,\"{flow_id}\")",
},
}
}
response = self.client.Reporting.query_flow_values(body)
parsed = self._parse_jsonapi_response(response)
results = parsed.get("results") if isinstance(parsed, dict) else None
stats = results[0].get("statistics", {}) if results else {}
self._flow_report_cache[flow_id] = stats
return stats
except Exception as e:
print(f" caused by: {type(e).__name__}: {e}", file=sys.stderr)
raise RuntimeError(f"Failed to fetch flow report for {flow_id}.") from e
def get_campaign_report(self, campaign_id: str) -> Dict:
"""
Get performance metrics for a specific campaign.
Args:
campaign_id: The campaign ID to get a report for
Returns:
Dictionary with campaign performance metrics
"""
if campaign_id in self._campaign_report_cache:
return self._campaign_report_cache[campaign_id]
try:
body = {
"data": {
"type": "campaign-values-report",
"attributes": {
"statistics": [
"opens",
"opens_unique",
"clicks",
"clicks_unique",
"recipients",
"delivered",
"bounced",
"unsubscribes",
"spam_complaints",
"conversion_value",
],
"timeframe": {"key": "last_365_days"},
"conversion_metric_id": self._get_conversion_metric_id(),
"filter": f"equals(campaign_id,\"{campaign_id}\")",
},
}
}
response = self.client.Reporting.query_campaign_values(body)
parsed = self._parse_jsonapi_response(response)
results = parsed.get("results") if isinstance(parsed, dict) else None
stats = results[0].get("statistics", {}) if results else {}
self._campaign_report_cache[campaign_id] = stats
return stats
except Exception as e:
print(f" caused by: {type(e).__name__}: {e}", file=sys.stderr)
raise RuntimeError(
f"Failed to fetch campaign report for {campaign_id}."
) from e
def get_metrics(self) -> List[Dict]:
"""
List all available event types (metrics).
Returns:
List of metric dictionaries with id and name
"""
try:
response = self.client.Metrics.get_metrics()
return self._parse_jsonapi_response(response)
except Exception as e:
print(f" caused by: {type(e).__name__}: {e}", file=sys.stderr)
raise RuntimeError("Failed to fetch metrics. Check API key scopes.") from e
def _parse_jsonapi_response(self, response) -> List[Dict]:
"""
Flatten JSON:API envelope into simple dictionaries.
Args:
response: SDK response object
Returns:
List of flattened dictionaries or a single dictionary
"""
if hasattr(response, "to_dict"):
response = response.to_dict()
elif isinstance(response, str):
response = json.loads(response)
data = response.get("data", response)
if isinstance(data, list):
return [self._flatten_resource(item) for item in data]
elif isinstance(data, dict):
return self._flatten_resource(data)
else:
return data
def _flatten_resource(self, resource: Dict) -> Dict:
"""Flatten a single JSON:API resource object."""
flat = {
"id": resource.get("id"),
"type": resource.get("type"),
}
attributes = resource.get("attributes", {})
if attributes:
flat.update(attributes)
return flat
def format_as_table(data, resource_type: str) -> str:
"""Format result as a human-readable table."""
if not data:
return "No data found."
if isinstance(data, dict):
data = [data]
# Select columns based on resource type
column_map = {
"flows": ["id", "name", "status", "trigger_type"],
"campaigns": ["id", "name", "status", "send_strategy"],
"segments": ["id", "name"],
"lists": ["id", "name"],
"metrics": ["id", "name"],
"report": None,
}
columns = column_map.get(resource_type)
if columns is None:
return json.dumps(data, indent=2, default=str)
# Filter to available columns
available_cols = [c for c in columns if any(c in item for item in data)]
if not available_cols:
available_cols = list(data[0].keys())[:5]
# Build header
lines = []
header = " | ".join(col.replace("_", " ").title() for col in available_cols)
lines.append(header)
lines.append("-" * len(header))
# Build rows
for item in data:
values = [str(item.get(col, "")) for col in available_cols]
lines.append(" | ".join(values))
lines.append("")
lines.append(f"Total: {len(data)} {resource_type}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Fetch Klaviyo analytics data",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# List all flows
python klaviyo_client.py --resource flows
# List only live flows
python klaviyo_client.py --resource flows --filter 'equals(status,"live")'
# Get campaign report
python klaviyo_client.py --resource report --report-type campaign --id CAMPAIGN_ID
# List segments as table
python klaviyo_client.py --resource segments --format table
# Export metrics to file
python klaviyo_client.py --resource metrics --output metrics.json
""",
)
parser.add_argument(
"--resource",
required=True,
choices=["flows", "campaigns", "segments", "lists", "metrics", "report"],
help="Resource type to fetch",
)
parser.add_argument(
"--report-type",
choices=["flow", "campaign"],
help="Report type (required when --resource report)",
)
parser.add_argument(
"--id",
help="Resource ID (required for report)",
)
parser.add_argument(
"--filter",
help="API filter expression (e.g., 'equals(status,\"live\")')",
)
parser.add_argument(
"--format",
choices=["json", "table"],
default="json",
help="Output format (default: json)",
)
parser.add_argument(
"--output",
help="Output file path (default: stdout)",
)
parser.add_argument(
"--debug",
action="store_true",
help="Re-raise exceptions with full traceback instead of friendly error",
)
args = parser.parse_args()
# Validate report arguments
if args.resource == "report":
if not args.report_type:
parser.error("--report-type is required when --resource is 'report'")
if not args.id:
parser.error("--id is required when --resource is 'report'")
try:
client = KlaviyoAnalyticsClient()
# Fetch data
if args.resource == "flows":
result = client.get_flows(filter_str=args.filter)
elif args.resource == "campaigns":
result = client.get_campaigns(filter_str=args.filter)
elif args.resource == "segments":
result = client.get_segments()
elif args.resource == "lists":
result = client.get_lists()
elif args.resource == "metrics":
result = client.get_metrics()
elif args.resource == "report":
if args.report_type == "flow":
result = client.get_flow_report(args.id)
else:
result = client.get_campaign_report(args.id)
# Format output
if args.format == "json":
output = json.dumps(result, indent=2, default=str)
else:
output = format_as_table(result, args.resource)
# Write output
if args.output:
safe_path = _safe_output_path(args.output)
with open(safe_path, "w", encoding="utf-8") as f:
f.write(output)
print(f"Data saved to {args.output}", file=sys.stderr)
else:
print(output)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
except Exception:
if args.debug:
traceback.print_exc()
else:
print("Error: Operation failed. Check your API key and network connection.", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()