Compare Channel Performance
Rank your acquisition channels by how well they convert — not just how much traffic they send — by combining the metric, overview, and goals endpoints with channel filters.
The channel that sends the most visitors is rarely the channel that sends the most customers. This guide ranks each acquisition channel by conversion rate so you can see which sources punch above their weight — and which ones are vanity traffic.
Endpoints used:
GET /metric— sessions broken down by channelGET /goals— conversion metrics, scoped with a channel filter
The key technique here is the filters parameter. Almost every endpoint accepts it, so you can take any site-wide metric and recompute it for a single segment — here, one channel at a time.
The workflow
List your channels and their session counts with metric?parameter=channel.
For each channel, fetch your goal's conversion rate by calling /goals with a channel filter applied.
Rank by conversion rate to reveal which channels actually drive outcomes.
Full script
This script ranks every channel by the conversion rate of a chosen goal (here, goal 1 — your signup goal). Swap in whichever goal matters to your business.
const API = 'https://app.rybbit.io';
const SITE = '123';
const API_KEY = process.env.RYBBIT_API_KEY;
const PRIMARY_GOAL_ID = 1; // the goal you care about
const range = { start_date: '2024-01-01', end_date: '2024-01-31', time_zone: 'America/New_York' };
async function get(path, params) {
const url = new URL(`${API}/api/sites/${SITE}${path}`);
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
const res = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` } });
if (!res.ok) throw new Error(`${path} failed: ${res.status}`);
return res.json();
}
// 1. All channels with their session counts
async function getChannels() {
const { data } = await get('/metric', { parameter: 'channel', limit: 20, ...range });
return data.data; // [{ value: 'Organic Search', count: 5400, ... }]
}
// 2. Conversion rate for one channel, scoped with a filter
async function goalForChannel(channel) {
const filters = JSON.stringify([{ parameter: 'channel', type: 'equals', value: [channel] }]);
const { data } = await get('/goals', { filters, ...range });
return data.find((g) => g.goalId === PRIMARY_GOAL_ID);
}
async function rankChannels() {
const channels = await getChannels();
const rows = await Promise.all(
channels.map(async (c) => {
const goal = await goalForChannel(c.value);
return {
channel: c.value || 'Direct',
sessions: c.count,
conversions: goal?.total_conversions ?? 0,
// conversion_rate comes back as a 0–1 fraction
conversionRate: (goal?.conversion_rate ?? 0) * 100,
};
})
);
return rows.sort((a, b) => b.conversionRate - a.conversionRate);
}
rankChannels().then((rows) => {
console.log('Channel'.padEnd(18), 'Sessions'.padStart(10), 'Conv.'.padStart(8), 'Rate'.padStart(8));
for (const r of rows) {
console.log(
r.channel.padEnd(18),
r.sessions.toLocaleString().padStart(10),
String(r.conversions).padStart(8),
`${r.conversionRate.toFixed(2)}%`.padStart(8)
);
}
});import os, json, requests
API = "https://app.rybbit.io"
SITE = "123"
API_KEY = os.environ["RYBBIT_API_KEY"]
PRIMARY_GOAL_ID = 1 # the goal you care about
RANGE = {"start_date": "2024-01-01", "end_date": "2024-01-31", "time_zone": "America/New_York"}
session = requests.Session()
session.headers["Authorization"] = f"Bearer {API_KEY}"
def get(path, **params):
res = session.get(f"{API}/api/sites/{SITE}{path}", params=params)
res.raise_for_status()
return res.json()
def get_channels():
# 1. All channels with their session counts
return get("/metric", parameter="channel", limit=20, **RANGE)["data"]["data"]
def goal_for_channel(channel):
# 2. Conversion rate for one channel, scoped with a filter
filters = json.dumps([{"parameter": "channel", "type": "equals", "value": [channel]}])
goals = get("/goals", filters=filters, **RANGE)["data"]
return next((g for g in goals if g["goalId"] == PRIMARY_GOAL_ID), None)
def rank_channels():
rows = []
for c in get_channels():
goal = goal_for_channel(c["value"])
rows.append({
"channel": c["value"] or "Direct",
"sessions": c["count"],
"conversions": goal["total_conversions"] if goal else 0,
# conversion_rate comes back as a 0–1 fraction
"conversion_rate": (goal["conversion_rate"] if goal else 0) * 100,
})
return sorted(rows, key=lambda r: r["conversion_rate"], reverse=True)
print(f"{'Channel':<18}{'Sessions':>10}{'Conv.':>8}{'Rate':>8}")
for r in rank_channels():
print(f"{r['channel']:<18}{r['sessions']:>10,}{r['conversions']:>8}{r['conversion_rate']:>7.2f}%")Reading the result
Channel Sessions Conv. Rate
Referral 1,820 310 17.03%
Organic Search 5,400 642 11.89%
Direct 4,220 451 10.69%
Email 980 88 8.98%
Paid Search 2,310 142 6.15%
Social 6,150 119 1.93%Social sends the most traffic (6,150 sessions) but converts the worst (1.93%). Referral sends a fraction of the volume but converts nine times better. The takeaway writes itself: the referral partnerships deserve more investment, and the social strategy needs a hard look before more budget goes into it.
Conversion rate alone can mislead on tiny samples — Email looks healthy at 9% but it's only 88 conversions. Always show the raw sessions and conversions columns next to the rate so a channel with 12 sessions and one fluke conversion doesn't top your list.
Variations
- By UTM campaign instead of channel: swap
parameter: 'channel'forparameter: 'utm_campaign', and filter onutm_campaignin the goals call. Same code, campaign-level granularity. - Engagement instead of conversion: call
/overviewwith the channel filter to comparebounce_rateandsession_durationper channel — useful when you don't have a goal defined yet. - Full-funnel by channel: run
/funnels/analyzewith a channel filter to see where in the funnel each channel's visitors fall out, not just the final rate.
Next steps
- Find Where Users Drop Off — once you know your best channels, fix the funnel they pour into.
- Automated Weekly Report — add a channel league table to a scheduled digest.
Find Where Users Drop Off
Chain the funnel analysis and journeys endpoints to locate the biggest leak in a conversion flow, then discover where the users who abandon actually go instead.
Build a Live Visitor Feed
Poll the live visitor count and the events endpoint to build a real-time activity stream — the foundation for a live dashboard, an internal "who's online" widget, or an alert when a key page gets traffic.