TEASEDocs
ProductsClubHow-to

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 domain
import 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

FieldTypeDescription
rangestring24h, 7d, 30d, 90d. Missing/unknown → 7d.
lifetimebooleantrue and no complete from_ts/to_ts pair → totals ignore range entirely and use all allowed history. since reads "lifetime".
from_ts / to_tsintegerExplicit unix-second bounds; both must be present to override range.
scope / idstringpage|creator|tenant rollup selector (account switcher). Omit both for your own page.
tzstringIANA 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.

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

On this page