/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 whereevent_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
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
Status:
200 OK.
Subscriber growth
App\Actions\Integrations\GetSubscriberGrowthTimeseries.
Response
Status:
200 OK.
Customer Lifetime Value
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
Status:
200 OK.
Customer Churn Rate
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
Status:
200 OK.
Revenue by channel
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.
kpi (as above) plus rows, each:
Status:
200 OK.
Revenue by source
includeUnattributed: true, onlyWithPayments: true flags as the channel breakdown. Backed by App\Actions\Stats\GetSourceBreakdown.
Response — kpi plus rows, each:
Status:
200 OK.
Revenue by campaign
includeUnattributed: true, onlyWithPayments: true flags. Backed by App\Actions\Stats\GetCampaignBreakdown.
Response — kpi plus rows, each:
Status:
200 OK.
Recent payments
occurred_at). Unfiltered, capped at 5 rows. Backed by App\Actions\Payments\ListPayments.
Response — a flat JSON array of:
Status:
200 OK.
Recent subscriptions
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:
Status:
200 OK.

