Skip to main content
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

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 Status: 200 OK.

Subscriber growth

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

Customer Lifetime Value

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 Status: 200 OK.

Customer 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 Status: 200 OK.

Revenue by channel

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: Status: 200 OK.

Revenue by source

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: Status: 200 OK.

Revenue by campaign

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: Status: 200 OK.

Recent 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: Status: 200 OK.

Recent 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: Status: 200 OK.