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. Somrr,subscriber-growth,ltv(recurring mode),paymentsandsubscriptionsare 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
Unattributedrow (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.
Response — kpi (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.
Response — kpi 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.
Response — kpi 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'})