REST API

Revenue

The read-only revenue-reporting suite for one of the current team's sites — the HTTP twin of the web /revenue page. Every endpoint is thin: it resolves the site, resolves the range/filters, and calls the SAME App\Actions\* Action the page calls with the SAME arguments, so the API can never disagree with the dashboard.

A site is addressed by {siteKey} (UUID or domain). All endpoints require Authorization: Bearer {token} and honour the shared range and filter parameters — see Conventions.

Money is integer minor units

Every money field on this page is an integer in the currency's minor units (cents). 1999 means $19.99. Refund and dispute amounts are already signed — a refunded payment reports a NEGATIVE amount — so summing a list of amounts nets refunds automatically. Currencies are ISO 4217 strings.

Verified revenue only

This page reports VERIFIED revenue exclusively: money confirmed by Stripe — the Stripe webhook plus the authenticated Payments API — written to the events store as rows where event_name = 'payment' (App\Actions\Stats\Support\EventScope::PAYMENT_CONDITION = isNotNull(revenue_reporting_amount) AND event_name = 'payment'). It NEVER includes client-reported, browser-side revenue. Revenue you send from the tracker attaches to goals and is deliberately kept out of these numbers — see Track revenue.

Two consequences that appear throughout:

  • The MRR headline and the subscriber counts read durable subscription STATE from Postgres (site_subscriptions), which carries no attribution columns. So mrr, subscriber-growth, ltv (recurring mode), payments and subscriptions are unfiltered — a channel/campaign filter has nothing to match and is not threaded through.
  • The three breakdowns (channels/sources/campaigns) DO accept filters, because revenue there is attributed to the paying visitor's first-touch dimension.

MRR summary

GET /api/sites/{siteKey}/revenue/mrr

The MRR card envelope: live Instant MRR / subscriber stock, percent change vs the previous equal-length period (movement-based for MRR — Instant baseline + SumMrrMovements over the window, so upgrades count), Instant chart samples per bucket with signed ΔMRR splits (New / Expansion / Contraction / Churn), and currencies excluded from the money figures. Unfiltered. Backed by App\Actions\Integrations\GetMrrSummary.

The headline (current.mrr) is LIST price from live subscription state. change_percent is movement-aware (not Instant end − Instant start). Each months[] point is an Instant stock sample for that bucket plus the bucket's movement splits. Both money numbers are in the site's single reporting currency; excluded_currencies names every currency left out (a foreign-currency subscription carries no settled amount and cannot be converted). Subscriber COUNTS are never currency-filtered.

Response

Field Type Description
kpi object MRR descriptor: metric (recurring_revenue), label (MRR), currency.
connected bool Whether the site has a Stripe integration at all. false → the card prompts to connect.
current.mrr int Live MRR, integer minor units (list price).
current.subscribers int Live active-subscriber count.
change_percent float|null Percent MRR change vs the previous equal-length period (movement-based). null when not connected or baseline is 0.
subscribers_change_percent float|null Percent subscriber change vs the previous period.
group string Chart bucket size (hour / day / week / month).
months array Instant series for the range. Each: month (bucket label), mrr, subscribers, mrr_change, mrr_change_new / _expansion / _contraction / _churn, customers_new, customers_churned.
excluded_currencies string[] ISO 4217 currencies omitted from the money figures on a multi-currency account.

Status: 200 OK.

curl "https://clickbase.so/api/sites/example.com/revenue/mrr?range=last_30_days" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
await fetch('https://clickbase.so/api/sites/example.com/revenue/mrr?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
requests.get('https://clickbase.so/api/sites/example.com/revenue/mrr', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})

Subscriber growth

GET /api/sites/{siteKey}/revenue/subscriber-growth

Per-bucket New / Churn headcounts for the Subscriber growth chart. Unfiltered. Backed by App\Actions\Integrations\GetSubscriberGrowthTimeseries.

Response

Field Type Description
labels string[] Bucket labels (same grouping as the date range).
started int[] New subscriptions per bucket.
ended int[] Churned subscriptions per bucket.
group string Bucket size (hour / day / week / month).

Status: 200 OK.

curl "https://clickbase.so/api/sites/example.com/revenue/subscriber-growth?range=last_30_days" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
await fetch('https://clickbase.so/api/sites/example.com/revenue/subscriber-growth?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
requests.get('https://clickbase.so/api/sites/example.com/revenue/subscriber-growth', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})

Customer Lifetime Value

GET /api/sites/{siteKey}/revenue/ltv

Customer Lifetime Value for the selected range. Unfiltered. Backed by App\Actions\Integrations\GetLtvSummary.

LTV = ARPA ÷ trailing 6-month average of monthly customer churn (ChartMogul). ARPA is Instant MRR ÷ Instant subscribers for the report interval. The churn average uses the previous six calendar months excluding the current month (missing months count as 0%). Monthly churn excludes subscribers who joined and canceled in the same month. When that average is zero, current / point ltv is 0.

Response

Field Type Description
kpi object Charges descriptor (metric, label, currency).
connected bool Stripe connected.
current int|null Current LTV in minor units, or null when undefined.
change_percent float|null Growth vs the previous equal-length period.
group string Bucket size (hour / day / week / month).
points array Chart series: label, ltv, optional arpu / churn_rate.

Status: 200 OK.

curl "https://clickbase.so/api/sites/example.com/revenue/ltv?range=last_30_days" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
await fetch('https://clickbase.so/api/sites/example.com/revenue/ltv?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
requests.get('https://clickbase.so/api/sites/example.com/revenue/ltv', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})

Customer Churn Rate

GET /api/sites/{siteKey}/revenue/churn-rate

Classic paid-subscriber (logo) churn rate for the selected range. Unfiltered. Backed by App\Actions\Integrations\GetChurnRateSummary.

Per calendar month: churn_rate = net_churned ÷ Instant subscribers at month start (ChartMogul Paid Subscriber / B2B). Net churn excludes subscribers who joined and canceled in the same month. Incomplete months use actual churn to date (no forward extrapolation). current is the last chart point.

Response

Field Type Description
connected bool Stripe connected.
current float|null Latest monthly churn rate (e.g. 0.1667 = 16.67%).
change_percent float|null Growth vs the previous equal-length period.
group string Always month.
points array Chart series: label, churn_rate, churned, start_subscribers.

Status: 200 OK.

curl "https://clickbase.so/api/sites/example.com/revenue/churn-rate?range=last_30_days" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
await fetch('https://clickbase.so/api/sites/example.com/revenue/churn-rate?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
requests.get('https://clickbase.so/api/sites/example.com/revenue/churn-rate', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})

Revenue by channel

GET /api/sites/{siteKey}/revenue/channels

Verified revenue attributed to each acquisition CHANNEL, by the paying visitor's first-touch. Accepts the shared filters. Backed by App\Actions\Stats\GetChannelBreakdown, called with includeUnattributed: true and onlyWithPayments: true — so:

  • A synthetic Unattributed row (unattributed: true, count: 0) is appended, carrying revenue that resolves to no channel.
  • Only channels with at least one payment row in range survive; a channel that brought sessions but no money is dropped. The presence gate keys on a payments COUNT, not revenue > 0, so a channel whose money nets to zero via a refund KEEPS its row.

Responsekpi (as above) plus rows, each:

Field Type Description
label string Channel name (Unattributed for the synthetic bucket).
count int Visitors attributed to the channel (0 on the Unattributed row).
percent float Share of visitors.
revenue int Verified revenue, integer minor units, signed. Present only when the channel has a payment row.
recurring_revenue int The recurring subset of revenue. Present under the same gate.
unattributed bool Present and true only on the synthetic bucket.
sources array Top referring sources for the channel: label, domain, count, percent, and (when the source itself has a payment) revenue, recurring_revenue.

Status: 200 OK.

curl "https://clickbase.so/api/sites/example.com/revenue/channels?range=last_30_days" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
await fetch('https://clickbase.so/api/sites/example.com/revenue/channels?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
requests.get('https://clickbase.so/api/sites/example.com/revenue/channels', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})

Revenue by source

GET /api/sites/{siteKey}/revenue/sources

Verified revenue attributed to each acquisition SOURCE. Same filters and same includeUnattributed: true, onlyWithPayments: true flags as the channel breakdown. Backed by App\Actions\Stats\GetSourceBreakdown.

Responsekpi plus rows, each:

Field Type Description
label string Source name (Unattributed for the synthetic bucket).
domain string Representative referrer domain for the source (favicon host).
count int Visitors attributed to the source.
percent float Share of visitors.
revenue int Verified revenue, integer minor units, signed. Present when the source has a payment row.
recurring_revenue int Recurring subset, present under the same gate.
unattributed bool Present and true only on the synthetic bucket.

Status: 200 OK.

curl "https://clickbase.so/api/sites/example.com/revenue/sources?range=last_30_days" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
await fetch('https://clickbase.so/api/sites/example.com/revenue/sources?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
requests.get('https://clickbase.so/api/sites/example.com/revenue/sources', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})

Revenue by campaign

GET /api/sites/{siteKey}/revenue/campaigns

Verified revenue attributed to each UTM CAMPAIGN. Same filters and same includeUnattributed: true, onlyWithPayments: true flags. Backed by App\Actions\Stats\GetCampaignBreakdown.

Responsekpi plus rows, each:

Field Type Description
label string Campaign name (Unattributed for the synthetic bucket).
count int Visitors attributed to the campaign.
percent float Share of visitors.
revenue int Verified revenue, integer minor units, signed. Present when the campaign has a payment row.
recurring_revenue int Recurring subset, present under the same gate.
unattributed bool Present and true only on the synthetic bucket.

Status: 200 OK.

curl "https://clickbase.so/api/sites/example.com/revenue/campaigns?range=last_30_days" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
await fetch('https://clickbase.so/api/sites/example.com/revenue/campaigns?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
requests.get('https://clickbase.so/api/sites/example.com/revenue/campaigns', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})

Recent payments

GET /api/sites/{siteKey}/revenue/payments

The site's most recent verified payments over the range, newest first (ranged on occurred_at). Unfiltered, capped at 5 rows. Backed by App\Actions\Payments\ListPayments.

Response — a flat JSON array of:

Field Type Description
id string Payment id.
transaction_id string Provider transaction id.
amount int Integer minor units. Signed: a refunded payment is negative.
currency string ISO 4217.
recurring bool Whether the payment is recurring.
refunded bool Whether the payment was refunded.
renewal bool Whether the payment is a renewal.
is_free_trial bool Whether the payment covers a free trial.
provider string Payment provider (e.g. stripe).
customer_email string | null Customer email, when known.
customer_name string | null Customer name, when known.
visitor_id string | null Attributed visitor id, when resolved.
occurred_at string (ISO 8601) When the payment occurred.

Status: 200 OK.

curl "https://clickbase.so/api/sites/example.com/revenue/payments?range=last_30_days" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
await fetch('https://clickbase.so/api/sites/example.com/revenue/payments?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
requests.get('https://clickbase.so/api/sites/example.com/revenue/payments', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})

Recent subscriptions

GET /api/sites/{siteKey}/revenue/subscriptions

The site's most recently started subscriptions over the range, newest first (ranged on started_at). Every status is included — canceled ones too — because this is a ledger of what happened, not the live-MRR sum. Unfiltered, capped at 5 rows. Backed by App\Actions\Integrations\ListSiteSubscriptions.

Response — a flat JSON array of:

Field Type Description
id string Subscription record id.
subscription_id string Provider subscription id.
customer_id string | null Provider customer id.
customer_name string | null Name, recovered from the latest matching payment.
customer_email string | null Email, recovered from the latest matching payment.
status string Subscription status (e.g. active, canceled, past_due).
mrr_amount int Monthly-normalized recurring amount, integer minor units.
currency string ISO 4217.
provider string Subscription provider (e.g. stripe).
started_at string (ISO 8601) When the subscription started.
canceled_at string (ISO 8601) | null When the subscription was canceled, if it was.
current_period_end string (ISO 8601) | null End of the current billing period, when known.

Status: 200 OK.

curl "https://clickbase.so/api/sites/example.com/revenue/subscriptions?range=last_30_days" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
await fetch('https://clickbase.so/api/sites/example.com/revenue/subscriptions?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
requests.get('https://clickbase.so/api/sites/example.com/revenue/subscriptions', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})