# Revenue

> The API operations about revenue, each with its input, its output and an example.

The operations about revenue. Each one is a `POST` with a JSON object as its body: [Making a request](/docs/api#start) has the rules they share.

## Revenue totals

`POST /api/v1/revenue.kpis` · Read

The revenue totals of one site for a period: money taken (gross), money refunded, the difference (net) and the number of orders. Get the siteId from sites.list first; sites.get says in hasRevenue whether the site ever recorded an order. Amounts are whole minor units (cents) of one currency and are never converted or added across currencies. The answer names its currency and lists the others in otherCurrencies; send currency to choose one. Orders count on the day they were paid and refunds on the day they were refunded. Filters change what this answers: with any filter set, it is the revenue credited to the visits that match (label attributed_revenue, attributedOnly true), refunds are not taken off, and orders counts the orders with a matching credited visit. It carries no comparison and no period fields: compare, cfrom and cto are accepted and ignored. For the same totals with a comparison call revenue.report and read summary. Answers 402 workspace_locked while the workspace's dashboards are locked.

Needs a token with the Read permission. Answers 402 while the dashboards of the workspace are locked.

### Input

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `siteId` | string | required | The site id, from sites.list. |
| `range` | "today" \| "yesterday" \| "30m" \| "12h" \| "24h" \| "week" \| "7d" \| "30d" \| "60d" \| "90d" \| "mtd" \| "ytd" \| "12mo" \| "all" \| "custom" | optional | The period to report on: one of today, yesterday, 30m, 12h, 24h, week, 7d, 30d, 60d, 90d, mtd, ytd, 12mo, all. Left out, the workspace's default range is used (30d unless it was changed). For exact dates send from and to and leave range out (custom is accepted with them and means the same). A preset together with dates answers 400. |
| `from` | string | optional | First day of a custom period, YYYY-MM-DD in the site's time zone. Send it together with to: one without the other answers 400, and so does a day that does not exist or a from after to. A day after today is read as today; window in the answer shows the period that was used. |
| `to` | string | optional | Last day of a custom period, inclusive, YYYY-MM-DD in the site's time zone. Send it together with from. |
| `compare` | "prev" \| "week" \| "month" \| "quarter" \| "year" \| "custom" \| "off" | optional | Accepted and ignored: this report has no comparison. |
| `cfrom` | string | optional | Accepted and ignored: this report has no comparison. |
| `cto` | string | optional | Accepted and ignored: this report has no comparison. |
| `filters` | object | optional | Narrow the numbers to matching traffic. An object such as { "country": "us", "path.has": "pricing" }. A plain key means "is"; a key ending in .not means "is not" and a key ending in .has means "contains". Filters are joined with AND. At most 10 keys, of which at most 3 may end in .has. Default: `{}`. [The filter keys are listed under Filters.](/docs/api#filters) |
| `currency` | string | optional | The currency to report, a three-letter ISO 4217 code in upper case such as USD. Left out, the currency with the most orders in the period is used. |

### Output

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `currency` | string | always | The currency of every amount in this answer, an ISO 4217 code such as USD. Amounts are whole minor units (cents). |
| `multiCurrency` | boolean | always | True when the period also holds orders in other currencies. They are listed in otherCurrencies and never added in. |
| `otherCurrencies` | array of object | always | The other currencies of the period: code, the number of orders and their gross amount in minor units of that currency. Pass a code as currency to get its numbers. |
| `grossMinor` | number | always | Money taken in the period, before refunds. With a filter: the part credited to the matching visits. |
| `refundsMinor` | number | always | Money refunded in the period, counted by the day of the refund. 0 with a filter, where refunds are not applied. |
| `refundCount` | number | always | The number of refunds in the period. |
| `netMinor` | number | always | grossMinor minus refundsMinor. |
| `orders` | number | always | The number of orders paid in the period. With a filter: the orders with a credited visit that matches. |
| `attributedOnly` | boolean | always | True when a filter was set, so the amounts are revenue credited to matching visits and not the whole takings. |
| `label` | "revenue" \| "net_revenue" \| "attributed_revenue" | always | What the dashboard calls the main number: revenue (no refunds in the period), net_revenue (refunds were taken off) or attributed_revenue (a filter was set). |
| `hasOrdersEver` | boolean | always | False when the site never recorded an order, in which case every number is 0. |

### Example

**curl**

```bash
curl -s -X POST https://clearsaga.com/api/v1/revenue.kpis \
  -H "Authorization: Bearer $CLEARSAGA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"siteId":"site_abc123def456"}'
```

**CLI**

```bash
clearsaga revenue kpis --input '{"siteId":"site_abc123def456"}'
```

**MCP**

```text
Tool: revenue_kpis
Arguments: {"siteId":"site_abc123def456"}
```

Run `clearsaga help revenue kpis` for one flag per field.

## Refunds list

`POST /api/v1/revenue.refunds` · Read

The refunds of one site in a period and one currency, newest first: when each happened, the product, the order number at the payment provider and the amount. Get the siteId from sites.list first. currency is required here; revenue.kpis names the currencies of the period. Amounts are whole minor units (cents). It pages by cursor: the answer holds limit rows (20 unless set, 100 at most) and nextCursor; send that as cursor for the next, older page, until it is null. A cursor this list did not issue answers an empty last page. There is no total. Only the period and the currency are used: filters and the comparison are accepted and ignored, and the answer carries no period fields. Answers 402 workspace_locked while the workspace's dashboards are locked.

Needs a token with the Read permission. Answers 402 while the dashboards of the workspace are locked.

### Input

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `siteId` | string | required | The site id, from sites.list. |
| `range` | "today" \| "yesterday" \| "30m" \| "12h" \| "24h" \| "week" \| "7d" \| "30d" \| "60d" \| "90d" \| "mtd" \| "ytd" \| "12mo" \| "all" \| "custom" | optional | The period to report on: one of today, yesterday, 30m, 12h, 24h, week, 7d, 30d, 60d, 90d, mtd, ytd, 12mo, all. Left out, the workspace's default range is used (30d unless it was changed). For exact dates send from and to and leave range out (custom is accepted with them and means the same). A preset together with dates answers 400. |
| `from` | string | optional | First day of a custom period, YYYY-MM-DD in the site's time zone. Send it together with to: one without the other answers 400, and so does a day that does not exist or a from after to. A day after today is read as today; window in the answer shows the period that was used. |
| `to` | string | optional | Last day of a custom period, inclusive, YYYY-MM-DD in the site's time zone. Send it together with from. |
| `compare` | "prev" \| "week" \| "month" \| "quarter" \| "year" \| "custom" \| "off" | optional | Accepted and ignored: this list has no comparison. |
| `cfrom` | string | optional | Accepted and ignored: this list has no comparison. |
| `cto` | string | optional | Accepted and ignored: this list has no comparison. |
| `filters` | object | optional | Accepted and ignored: this report applies no filter. Default: `{}`. [The filter keys are listed under Filters.](/docs/api#filters) |
| `currency` | string | required | The currency of the refunds to list, a three-letter ISO 4217 code in upper case such as USD. Required. |
| `cursor` | string, at most 200 characters | optional | Where to continue: the nextCursor of the page before. Leave it out for the newest refunds. |
| `limit` | integer, 1 to 100 | optional | Rows per page, 1 to 100. 20 when left out. |

### Output

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `rows` | array of object | always | The refunds of the period in the chosen currency, newest first: occurredAt is the time of the refund in milliseconds since 1970 UTC, productName the product of the refunded order when it is known, providerOrderId the order number at the payment provider, and amountMinor the refunded amount in minor units (cents) of currency. |
| `nextCursor` | string \| null | always | Send this as cursor to get the next, older page. Null on the last page. |

### Example

**curl**

```bash
curl -s -X POST https://clearsaga.com/api/v1/revenue.refunds \
  -H "Authorization: Bearer $CLEARSAGA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"siteId":"site_abc123def456","currency":"USD"}'
```

**CLI**

```bash
clearsaga revenue refunds --input '{"siteId":"site_abc123def456","currency":"USD"}'
```

**MCP**

```text
Tool: revenue_refunds
Arguments: {"siteId":"site_abc123def456","currency":"USD"}
```

Run `clearsaga help revenue refunds` for one flag per field.

## Revenue attribution report

`POST /api/v1/revenue.report` · Read

Where the revenue of one site came from in a period: the totals with the comparison period, how many orders could be tied to a visit, and credited revenue by landing page, by the page before the order, by the source of the first and of the last visit, by product, by the number of visits before buying and by page sequence. Get the siteId from sites.list first. An order is tied to a visit through an id the website passes to its payment provider; orders without one are counted in coverage as no_id. Amounts are whole minor units (cents) of one currency and are never converted or added across currencies. Send currency to choose one; currencies lists those with orders in the period. group chooses whether sources are referring sites or channels. Credit follows the site's attribution settings, which are in settings (lookback days and model). This report ignores every filter: the filters that were sent change nothing and are listed in ignoredFilters, each once by its plain key (path for path.has). Nothing pages. Answers 402 workspace_locked while the workspace's dashboards are locked.

Needs a token with the Read permission. Answers 402 while the dashboards of the workspace are locked.

### Input

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `siteId` | string | required | The site id, from sites.list. |
| `range` | "today" \| "yesterday" \| "30m" \| "12h" \| "24h" \| "week" \| "7d" \| "30d" \| "60d" \| "90d" \| "mtd" \| "ytd" \| "12mo" \| "all" \| "custom" | optional | The period to report on: one of today, yesterday, 30m, 12h, 24h, week, 7d, 30d, 60d, 90d, mtd, ytd, 12mo, all. Left out, the workspace's default range is used (30d unless it was changed). For exact dates send from and to and leave range out (custom is accepted with them and means the same). A preset together with dates answers 400. |
| `from` | string | optional | First day of a custom period, YYYY-MM-DD in the site's time zone. Send it together with to: one without the other answers 400, and so does a day that does not exist or a from after to. A day after today is read as today; window in the answer shows the period that was used. |
| `to` | string | optional | Last day of a custom period, inclusive, YYYY-MM-DD in the site's time zone. Send it together with from. |
| `compare` | "prev" \| "week" \| "month" \| "quarter" \| "year" \| "custom" \| "off" | optional | What the period is compared with: prev (the period just before, the default), week, month, quarter or year earlier, custom (cfrom and cto are then required), or off for no comparison. The range all never has a comparison. |
| `cfrom` | string | optional | First day of a custom comparison period, YYYY-MM-DD. Required with compare set to custom, where a day that does not exist or a cfrom after cto answers 400. |
| `cto` | string | optional | Last day of a custom comparison period, inclusive, YYYY-MM-DD. Required with compare set to custom. |
| `filters` | object | optional | Accepted and ignored: this report applies no filter. The filters that were sent come back in ignoredFilters, each named once by its plain key. Default: `{}`. [The filter keys are listed under Filters.](/docs/api#filters) |
| `currency` | string | optional | The currency to report, a three-letter ISO 4217 code in upper case such as USD. Left out, the currency with the most orders in the period is used. |
| `group` | "referrer" \| "channel" | optional | How firstSource and lastSource are grouped: referrer (by referring site, the default) or channel. |

### Output

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `serverNow` | number | always | The clock the period was resolved with: the start of the next whole minute, milliseconds since 1970 UTC. |
| `serverTime` | number | always | When the answer was built, milliseconds since 1970 UTC. |
| `window` | object | always | The period the numbers cover: from and to in milliseconds since 1970 UTC (to is exclusive), the time zone, a label and the bucket size. |
| `compare` | object \| null | always | The comparison period (from, to, tz, label, granularity, token) with the same totals for it as summary has. Null when there is no comparison. |
| `rawFrom` | number \| null | always | The earliest time detailed data is kept from, or null when nothing was trimmed. |
| `truncated` | boolean | always | True when a filtered answer could only cover the part of the period that still has detailed data. |
| `currency` | string | always | The currency of every amount in this answer, an ISO 4217 code. Amounts are whole minor units (cents). |
| `currencies` | array of object | always | Every currency with orders in the period: code, the number of orders and their gross amount in minor units of that currency. Pass a code as currency to get its report. |
| `summary` | object | always | The totals of the period. grossMinor is the money taken, refundsMinor the money refunded (refundCount refunds) and netMinor the difference; orders is the number of orders. attributedMinor and attributedOrders are the part that could be tied to a recorded visit, attributionRate is attributedOrders divided by orders (a fraction from 0 to 1, null without orders), and medianTimeToBuyMs is the median time from a buyer's first visit to the order in milliseconds (null when no order could be tied to a visit). |
| `coverage` | array of object | always | How many orders could be tied to a visit. attributed: tied to a recorded visit. id_no_visit: the order carried a visitor id but no visit was found in the lookback window. no_id: the order carried no id. share is the part of all orders, a fraction from 0 to 1. |
| `breakdown` | object | always | Credited revenue split four ways, each a list of { key, label, orders, revenueMinor }: by the first page of the buyer's journey (landingPages), by the page viewed last before the order (convertingPages), and by the source of the first and of the last visit (firstSource, lastSource). Sources are referring sites, or channels when group is channel. |
| `products` | array of object | always | Revenue per product: its name, whether it is a subscription or a one-time purchase, orders, revenueMinor, share of the period's revenue and attributedShare, the part of its revenue tied to a visit. Shares are fractions from 0 to 1. |
| `touches` | array of object | always | Orders grouped by how many visits the buyer made before buying: 1, 2-3, 4-6, 7-10 or 11+. |
| `paths` | array of object | always | The page sequences that most often led to an order: the pages in order, truncated true when the sequence was cut short, and the orders and revenue behind it. |
| `settings` | object | always | The attribution settings the report was computed with: how many days back a visit can earn credit, and the model (last_touch, first_touch or linear). |
| `ignoredFilters` | array of string | always | The filters of the input. This report applies no filter, so every filter that was sent is listed here. A filter is named once by its plain key, whichever form was sent: path.has and path.not are both listed as path. |

### Example

**curl**

```bash
curl -s -X POST https://clearsaga.com/api/v1/revenue.report \
  -H "Authorization: Bearer $CLEARSAGA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"siteId":"site_abc123def456"}'
```

**CLI**

```bash
clearsaga revenue report --input '{"siteId":"site_abc123def456"}'
```

**MCP**

```text
Tool: revenue_report
Arguments: {"siteId":"site_abc123def456"}
```

Run `clearsaga help revenue report` for one flag per field.

## Revenue over time

`POST /api/v1/revenue.timeseries` · Read

Money taken, orders and money refunded per bucket of a period, for one site and one currency. Get the siteId from sites.list first. Amounts are whole minor units (cents) of one currency and are never converted or added across currencies. Send currency to choose one. The bucket size follows the period and is named in window.granularity. Orders count in the bucket they were paid in and refunds in the bucket they were refunded in. This report ignores every filter: the filters object is accepted and changes nothing. It has no comparison series either. Answers 402 workspace_locked while the workspace's dashboards are locked.

Needs a token with the Read permission. Answers 402 while the dashboards of the workspace are locked.

### Input

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `siteId` | string | required | The site id, from sites.list. |
| `range` | "today" \| "yesterday" \| "30m" \| "12h" \| "24h" \| "week" \| "7d" \| "30d" \| "60d" \| "90d" \| "mtd" \| "ytd" \| "12mo" \| "all" \| "custom" | optional | The period to report on: one of today, yesterday, 30m, 12h, 24h, week, 7d, 30d, 60d, 90d, mtd, ytd, 12mo, all. Left out, the workspace's default range is used (30d unless it was changed). For exact dates send from and to and leave range out (custom is accepted with them and means the same). A preset together with dates answers 400. |
| `from` | string | optional | First day of a custom period, YYYY-MM-DD in the site's time zone. Send it together with to: one without the other answers 400, and so does a day that does not exist or a from after to. A day after today is read as today; window in the answer shows the period that was used. |
| `to` | string | optional | Last day of a custom period, inclusive, YYYY-MM-DD in the site's time zone. Send it together with from. |
| `compare` | "prev" \| "week" \| "month" \| "quarter" \| "year" \| "custom" \| "off" | optional | What the period is compared with: prev (the period just before, the default), week, month, quarter or year earlier, custom (cfrom and cto are then required), or off for no comparison. The range all never has a comparison. |
| `cfrom` | string | optional | First day of a custom comparison period, YYYY-MM-DD. Required with compare set to custom, where a day that does not exist or a cfrom after cto answers 400. |
| `cto` | string | optional | Last day of a custom comparison period, inclusive, YYYY-MM-DD. Required with compare set to custom. |
| `filters` | object | optional | Accepted and ignored: this report applies no filter. Default: `{}`. [The filter keys are listed under Filters.](/docs/api#filters) |
| `currency` | string | optional | The currency to report, a three-letter ISO 4217 code in upper case such as USD. Left out, the currency with the most orders in the period is used. |

### Output

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `serverNow` | number | always | The clock the period was resolved with: the start of the next whole minute, milliseconds since 1970 UTC. |
| `serverTime` | number | always | When the answer was built, milliseconds since 1970 UTC. |
| `window` | object | always | The period the numbers cover: from and to in milliseconds since 1970 UTC (to is exclusive), the time zone, a label and the bucket size. |
| `compare` | object \| null | always | The comparison period in the same form, or null when there is none. |
| `rawFrom` | number \| null | always | The earliest time detailed data is kept from, or null when nothing was trimmed. |
| `truncated` | boolean | always | True when a filtered answer could only cover the part of the period that still has detailed data. |
| `currency` | string | always | The currency of the amounts, an ISO 4217 code. Amounts are whole minor units (cents). |
| `points` | array of object | always | One point per bucket of the period, oldest first: start and end in milliseconds since 1970 UTC (end exclusive), partial true while the bucket is still running, the money taken (grossMinor), the number of orders and the money refunded (refundsMinor) in it. |

### Example

**curl**

```bash
curl -s -X POST https://clearsaga.com/api/v1/revenue.timeseries \
  -H "Authorization: Bearer $CLEARSAGA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"siteId":"site_abc123def456"}'
```

**CLI**

```bash
clearsaga revenue timeseries --input '{"siteId":"site_abc123def456"}'
```

**MCP**

```text
Tool: revenue_timeseries
Arguments: {"siteId":"site_abc123def456"}
```

Run `clearsaga help revenue timeseries` for one flag per field.
