I Built A Script To Split Klaviyo Flow Revenue From Campaign Revenue

Published

One question kept coming up on a client account: is the revenue actually coming from flows, or from the campaign calendar. Klaviyo shows both numbers, just not side by side.

The Screen-Switching Problem

Answering it meant opening two separate report screens, writing the numbers down somewhere, and doing the split by hand. I did that three times before I decided it wasn't worth doing by hand a fourth time.

So I wrote a small script. It pulls flow revenue and campaign revenue for a given date range straight from Klaviyo's API and prints one table. Nothing fancier than that.

Klaviyo's flow report and campaign report live in separate views. Each one is fine on its own, clear totals, clear date filters.

The problem is comparing them. There's no single screen that shows flow revenue and campaign revenue for the same date range without switching tabs and writing both numbers down yourself.

For a one-off check, that's a two minute inconvenience. For a client I check weekly, it adds up to a repeated manual task that a script can just do instead.

What The Script Actually Pulls

It hits Klaviyo's API for two things: flow-level revenue attribution and campaign-level revenue attribution, both scoped to the same date range. These are two separate endpoints under Klaviyo's Reporting API, each one built for its own screen in the dashboard, which is exactly why they never show up side by side on their own.

The flow side is a POST request with a timeframe and a conversion metric (I use Placed Order). Klaviyo returns one row per flow, each with a conversion_value.

import requests

BASE = "https://a.klaviyo.com/api"
HEADERS = {
    "Authorization": f"Klaviyo-API-Key {API_KEY}",
    "revision": "2024-10-15",
    "Content-Type": "application/json",
}

def get_flow_revenue(start, end, conversion_metric_id):
    body = {
        "data": {
            "type": "flow-values-report",
            "attributes": {
                "timeframe": {"start": start, "end": end},
                "conversion_metric_id": conversion_metric_id,
                "statistics": ["conversion_value"],
            },
        }
    }
    resp = requests.post(f"{BASE}/flow-values-reports/", json=body, headers=HEADERS)
    return resp.json()["data"]["attributes"]["results"]

The campaign side is the same shape, just a different report type and a different endpoint. Same timeframe, same conversion metric, so the two numbers are actually comparable.

def get_campaign_revenue(start, end, conversion_metric_id):
    body = {
        "data": {
            "type": "campaign-values-report",
            "attributes": {
                "timeframe": {"start": start, "end": end},
                "conversion_metric_id": conversion_metric_id,
                "statistics": ["conversion_value"],
            },
        }
    }
    resp = requests.post(f"{BASE}/campaign-values-reports/", json=body, headers=HEADERS)
    return resp.json()["data"]["attributes"]["results"]

Both functions return a list of rows, one per flow or per campaign. The last step just sums each list down to one number and prints the split, which is the entire point of writing this in the first place.

flows = get_flow_revenue(start, end, metric_id)
campaigns = get_campaign_revenue(start, end, metric_id)

flow_total = sum(r["statistics"]["conversion_value"] for r in flows)
campaign_total = sum(r["statistics"]["conversion_value"] for r in campaigns)
total = flow_total + campaign_total

print(f"Flow revenue:     {flow_total:,.2f}  ({flow_total / total:.0%})")
print(f"Campaign revenue: {campaign_total:,.2f}  ({campaign_total / total:.0%})")

That's the core of it, not the whole file. My actual copy has argument parsing for the date range and a try/except around the request for when a token expires mid-week. Neither of those teaches you anything about the Klaviyo API, so I left them out here.

What It Actually Showed

Running it on the client account that started this: flows were carrying a noticeably larger share of revenue than the campaign calendar's own reporting made it look like, once the two were actually compared side by side.

Nobody had done anything wrong. The campaigns were performing fine on their own numbers. The gap was purely a visibility problem: nobody had put the two totals next to each other before, so the real balance between them stayed a guess until the script ran.

That's the same root pattern behind Klaviyo flow and campaign overlap: flows and campaigns get planned, reported on, and reviewed as if they're separate systems, when the account only has one revenue number that matters.

When A Script Beats A Dashboard

A dashboard makes sense when a specific view needs to be checked constantly, by more than one person, on an ongoing basis.

This was one specific comparison, checked periodically, by me. Building a full dashboard for that would have taken a lot longer than the problem justified.

If the same question ever needs answering across many accounts on a recurring basis, that calculus changes. Until then, a 40-line script that runs in a few seconds beats a maintained piece of software for a question this narrow.

I set up this same account's flows from scratch, so having an easy way to check whether flows are actually carrying their share of revenue matters to me directly, not just as a reporting nicety.

Want a clear read on what's actually driving your Klaviyo revenue?

I manage full Klaviyo accounts, flows, campaigns, segmentation, and the reporting question of which one is actually working.