> ## Documentation Index
> Fetch the complete documentation index at: https://docs.clickbase.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Revenue

> The read-only revenue-reporting suite for one of the current team's sites — the HTTP twin of the web /revenue page.

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](/api/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](/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

```http theme={null}
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`.

<CodeGroup>
  ```bash title="curl" theme={null}
  curl "https://clickbase.so/api/sites/example.com/revenue/mrr?range=last_30_days" \
    -H "Authorization: Bearer {token}" \
    -H "Accept: application/json"
  ```

  ```javascript title="JavaScript" theme={null}
  await fetch('https://clickbase.so/api/sites/example.com/revenue/mrr?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
  ```

  ```python title="Python" theme={null}
  requests.get('https://clickbase.so/api/sites/example.com/revenue/mrr', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})
  ```
</CodeGroup>

***

### Subscriber growth

```http theme={null}
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`.

<CodeGroup>
  ```bash title="curl" theme={null}
  curl "https://clickbase.so/api/sites/example.com/revenue/subscriber-growth?range=last_30_days" \
    -H "Authorization: Bearer {token}" \
    -H "Accept: application/json"
  ```

  ```javascript title="JavaScript" theme={null}
  await fetch('https://clickbase.so/api/sites/example.com/revenue/subscriber-growth?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
  ```

  ```python title="Python" theme={null}
  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'})
  ```
</CodeGroup>

***

### Customer Lifetime Value

```http theme={null}
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`.

<CodeGroup>
  ```bash title="curl" theme={null}
  curl "https://clickbase.so/api/sites/example.com/revenue/ltv?range=last_30_days" \
    -H "Authorization: Bearer {token}" \
    -H "Accept: application/json"
  ```

  ```javascript title="JavaScript" theme={null}
  await fetch('https://clickbase.so/api/sites/example.com/revenue/ltv?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
  ```

  ```python title="Python" theme={null}
  requests.get('https://clickbase.so/api/sites/example.com/revenue/ltv', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})
  ```
</CodeGroup>

***

### Customer Churn Rate

```http theme={null}
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`.

<CodeGroup>
  ```bash title="curl" theme={null}
  curl "https://clickbase.so/api/sites/example.com/revenue/churn-rate?range=last_30_days" \
    -H "Authorization: Bearer {token}" \
    -H "Accept: application/json"
  ```

  ```javascript title="JavaScript" theme={null}
  await fetch('https://clickbase.so/api/sites/example.com/revenue/churn-rate?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
  ```

  ```python title="Python" theme={null}
  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'})
  ```
</CodeGroup>

***

### Revenue by channel

```http theme={null}
GET /api/sites/{siteKey}/revenue/channels
```

Verified revenue attributed to each acquisition CHANNEL, by the paying visitor's first-touch. Accepts the shared [filters](/api/conventions). 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.

**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`.

<CodeGroup>
  ```bash title="curl" theme={null}
  curl "https://clickbase.so/api/sites/example.com/revenue/channels?range=last_30_days" \
    -H "Authorization: Bearer {token}" \
    -H "Accept: application/json"
  ```

  ```javascript title="JavaScript" theme={null}
  await fetch('https://clickbase.so/api/sites/example.com/revenue/channels?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
  ```

  ```python title="Python" theme={null}
  requests.get('https://clickbase.so/api/sites/example.com/revenue/channels', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})
  ```
</CodeGroup>

***

### Revenue by source

```http theme={null}
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`.

<CodeGroup>
  ```bash title="curl" theme={null}
  curl "https://clickbase.so/api/sites/example.com/revenue/sources?range=last_30_days" \
    -H "Authorization: Bearer {token}" \
    -H "Accept: application/json"
  ```

  ```javascript title="JavaScript" theme={null}
  await fetch('https://clickbase.so/api/sites/example.com/revenue/sources?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
  ```

  ```python title="Python" theme={null}
  requests.get('https://clickbase.so/api/sites/example.com/revenue/sources', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})
  ```
</CodeGroup>

***

### Revenue by campaign

```http theme={null}
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`.

<CodeGroup>
  ```bash title="curl" theme={null}
  curl "https://clickbase.so/api/sites/example.com/revenue/campaigns?range=last_30_days" \
    -H "Authorization: Bearer {token}" \
    -H "Accept: application/json"
  ```

  ```javascript title="JavaScript" theme={null}
  await fetch('https://clickbase.so/api/sites/example.com/revenue/campaigns?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
  ```

  ```python title="Python" theme={null}
  requests.get('https://clickbase.so/api/sites/example.com/revenue/campaigns', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})
  ```
</CodeGroup>

***

### Recent payments

```http theme={null}
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`.

<CodeGroup>
  ```bash title="curl" theme={null}
  curl "https://clickbase.so/api/sites/example.com/revenue/payments?range=last_30_days" \
    -H "Authorization: Bearer {token}" \
    -H "Accept: application/json"
  ```

  ```javascript title="JavaScript" theme={null}
  await fetch('https://clickbase.so/api/sites/example.com/revenue/payments?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
  ```

  ```python title="Python" theme={null}
  requests.get('https://clickbase.so/api/sites/example.com/revenue/payments', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})
  ```
</CodeGroup>

***

### Recent subscriptions

```http theme={null}
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`.

<CodeGroup>
  ```bash title="curl" theme={null}
  curl "https://clickbase.so/api/sites/example.com/revenue/subscriptions?range=last_30_days" \
    -H "Authorization: Bearer {token}" \
    -H "Accept: application/json"
  ```

  ```javascript title="JavaScript" theme={null}
  await fetch('https://clickbase.so/api/sites/example.com/revenue/subscriptions?range=last_30_days', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } })
  ```

  ```python title="Python" theme={null}
  requests.get('https://clickbase.so/api/sites/example.com/revenue/subscriptions', params={'range':'last_30_days'}, headers={'Authorization': f'Bearer {token}', 'Accept':'application/json'})
  ```
</CodeGroup>
