Read revenue by traffic source
See how much money each acquisition source produced — split by country, link, and fan-kind, reconciled to your account net.
Everything lives inside GET /api/admin/stats, under revenue.sources plus its sibling
buckets. There is no standalone /attribution endpoint.
curl "https://app.tease.link/api/admin/stats?range=30d" \
-H "Authorization: Bearer $TEASE_API_KEY"const res = await fetch(
'https://app.tease.link/api/admin/stats?range=30d',
{ headers: { Authorization: `Bearer ${process.env.TEASE_API_KEY}` } },
);
const { revenue } = await res.json();
const leaderboard = revenue.sources; // array — one row per resolved domainimport os, requests
res = requests.get(
"https://app.tease.link/api/admin/stats",
headers={"Authorization": f"Bearer {os.environ['TEASE_API_KEY']}"},
params={"range": "30d"},
)
leaderboard = res.json()["revenue"]["sources"]Query parameters that affect the leaderboard
| Field | Type | Description |
|---|---|---|
range | string | 24h, 7d, 30d, 90d. Missing/unknown → 7d. |
lifetime | boolean | true and no complete from_ts/to_ts pair → totals ignore range entirely and use all allowed history. since reads "lifetime". |
from_ts / to_ts | integer | Explicit unix-second bounds; both must be present to override range. |
scope / id | string | page|creator|tenant rollup selector (account switcher). Omit both for your own page. |
tz | string | IANA zone for calendar-day buckets, e.g. Europe/Berlin. Invalid/missing → UTC. |
domain and source narrow the click/funnel telemetry elsewhere in the same response, but
not revenue.sources or its reconciliation — attribution is a property of the fan, not of
one page's traffic.
What comes back
revenue.sources is an array of rows sorted by revenue_net_cents descending, each shaped:
{
"domain_source": "ig-main",
"channel": "instagram",
"subs": 312,
"spenders": 188,
"revenue_net_cents": 421550,
"revenue_net_hard_cents": 398200,
"revenue_net_llm_cents": 23350,
"evidence_level": "mixed",
"pct": 0.41,
"new_fans": 240,
"returning_fans": 72,
"trial_subs": 96,
"trial_to_paid": 51,
"by_kind": [{ "kind": "subscription", "net_cents": 184200, "count": 240 }],
"countries": [{ "country": "US", "subs": 140, "spenders": 92, "revenue_net_cents": 210400 }],
"links": [{ "link_ref": "lnk_8fq2", "subs": 201, "spenders": 130, "revenue_net_cents": 290300 }]
}Three sibling rows sit next to sources, same shape, always present:
organic— paid with no signal at all.pending— traffic known, source not mapped yet.checking— a brand-new payer the engine hasn't finished checking yet.
See Source attribution for what decides
which bucket a fan lands in, and why reconciliation.balanced alone doesn't prove the split is
correct — check reconciliation.fans_disjoint for that.
Money-by-type and per-country/per-link drills
by_kind (global and per-row) always uses the same five keys — subscription, rebill,
ppv, tips, other — and sums to the parent's revenue_net_cents. Each source row's
countries and links arrays are pre-computed drills; for the individual fans behind either
one, see Drill a source to paying fans.
What's next
- Drill a source down to paying fans
- Confirm the source for a newly scanned campaign
- Source attribution — the bucket model this page's numbers come from.
Review recent chargebacks
See which payments were recently flipped to refunded/undo status, sorted by when Club noticed — not when the payment happened.
Confirm the source for a scanned campaign
Bind a newly scanned OnlyFans tracking link to one of your registered source domains, one at a time or in bulk.