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

# Revenue API

> Attributed revenue by source, over time, by product, and by lifetime value.

## Base URL and authentication

```
https://api.traceten.com/v1/revenue
```

Takes `Authorization: Bearer <YOUR_API_KEY>` ([details](/api/authentication)) and the `stats:read` permission.

<Note>
  Every endpoint on this page rejects a parameter it does not recognise with a `422`, rather than
  ignoring it. A filter we cannot apply would otherwise come back as a `200` carrying a number that
  answers a different question, and you would have no way to tell.
</Note>

`site_id` is the site's **snippet key**, the `ttid_` value your install snippet carries as `data-site`. You can read it from [`GET /v1/sites`](/api/sites).

## Currency

Every amount is stored in USD. `amount_usd`, `revenue_usd` and `ltv_usd` are always USD, whatever your site's display currency is.

Where a display currency is set, the `_display` field beside each amount carries the same money converted at the European Central Bank reference rate **for that row's own date**, so a figure for last Tuesday does not move when today's rate does. If any date in the window has no published rate, the `_display` field is `null`. It is never the USD number relabelled.

The response tells you what happened:

| Field                | Meaning                                                                   |
| -------------------- | ------------------------------------------------------------------------- |
| `currency`           | Your display currency.                                                    |
| `converted`          | `false` when nothing was converted, because the site already reports USD. |
| `missing_rate_dates` | Present only when at least one date had no rate. Omitted otherwise.       |

## Refunds

Every amount on this page is **net of refunds**. A refund is subtracted from whichever AI source the original payment was attributed to, on all six revenue paths: [Stripe](/integrations/stripe), [Shopify](/integrations/shopify), [Lemon Squeezy](/integrations/lemonsqueezy), [Polar](/integrations/polar), [Paddle](/integrations/paddle), and anything you post to the [Payment API](/api/payments).

Four things follow, and they are worth knowing before you build a report on these numbers:

* **A refund is dated on the day it was issued**, not the day of the original payment. A window you queried last week returns the same figures today. Refunds land in the window they happened in.
* **An amount can be negative.** A day, or a source, that gave back more than it took in during your window is a negative number. Do not clamp it at zero: the negative is the answer.
* **Conversion counts stay gross.** `attributed_sessions` on `/breakdown` and `payments` on `/ltv` count the purchase that happened. A refund does not erase it. So a source can show conversions and negative revenue in the same window, and both are correct.
* **A cancelled subscription is not a refund.** Cancelling stops future charges; the charges already paid stay in `ltv_usd` and in every window that contains them.

There is no separate gross figure on this API. If you need "what did this source bill before returns", that is your payment processor's own reporting, not Traceten's.

## `GET /v1/revenue/breakdown`

Which AI sources produced revenue in the window. Top 5 by revenue, highest first.

```bash theme={null}
curl "https://api.traceten.com/v1/revenue/breakdown?site_id=ttid_7Rb4TrC1dTbnD8w3s1TS12&from=2026-08-01&to=2026-08-07" \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

```json theme={null}
{
  "breakdown": [
    {
      "ai_source": "chatgpt",
      "source_kind": "ai",
      "source_name": "chatgpt.com",
      "amount_usd": 4190.0,
      "amount_display": 3902.11,
      "attributed_sessions": 7
    }
  ],
  "currency": "EUR",
  "converted": true
}
```

Ranking uses the stored USD figure, so the order of the rows cannot change when the exchange rate does.

### Query parameters

| Parameter           | Type    | Required | Notes                                                                |
| ------------------- | ------- | -------- | -------------------------------------------------------------------- |
| `site_id`           | string  | yes      | The snippet key.                                                     |
| `from`              | string  | no       | `YYYY-MM-DD`, inclusive. Must be sent with `to`.                     |
| `to`                | string  | no       | `YYYY-MM-DD`, inclusive. Must be sent with `from`.                   |
| `days`              | integer | no       | 1 to 90. An alias for "the last N days ending today". Defaults to 7. |
| `attribution_model` | string  | no       | `first_touch`, `last_touch`, `linear` or `time_decay`.               |
| `source_kind`       | string  | no       | `ai`, `non_ai`, `unmatched` or `unknown`.                            |

Sending `from` without `to` (or the reverse) is a `422`.

When you omit `attribution_model`, every model is summed together. Pass the model you actually report on if you want a single view.

`source_kind: "unmatched"` is revenue we could not tie back to any session. It is worth reading: it is the gap between what your payment provider recorded and what Traceten could attribute.

### Response fields

| Field                 | Type    | Notes                                                                                                                                                                                                                         |
| --------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ai_source`           | string  | The detected source, for example `chatgpt`, `perplexity`, `direct`.                                                                                                                                                           |
| `source_kind`         | string  | `ai`, `non_ai`, `unmatched` or `unknown`. Empty when unresolved.                                                                                                                                                              |
| `source_name`         | string  | The specific source: a referring domain such as `google.com`, or an AI provider's host such as `chatgpt.com`. When there is no specific name it repeats `ai_source`, so direct traffic reads `direct`. Empty for `unmatched`. |
| `amount_usd`          | number  | Attributed revenue in USD, net of refunds. Negative when refunds outran orders.                                                                                                                                               |
| `amount_display`      | number  | The same money in your display currency, or `null` if any date had no rate.                                                                                                                                                   |
| `attributed_sessions` | integer | Revenue-generating conversion events credited to this source. Gross: a refunded conversion still counts.                                                                                                                      |

## `GET /v1/revenue/timeseries`

Daily revenue per AI source, ordered by date and then by source. This is the data behind the revenue chart in the dashboard.

```bash theme={null}
curl "https://api.traceten.com/v1/revenue/timeseries?site_id=ttid_7Rb4TrC1dTbnD8w3s1TS12&from=2026-08-01&to=2026-08-02" \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

```json theme={null}
{
  "timeseries": [
    { "date": "2026-08-01", "ai_source": "chatgpt", "amount_usd": 1200.0, "amount_display": null },
    { "date": "2026-08-02", "ai_source": "chatgpt", "amount_usd": 990.5, "amount_display": null }
  ],
  "currency": "USD",
  "converted": false
}
```

Takes the same parameters as `/breakdown`, plus `interval`. Only `interval=day` is supported today.

## `GET /v1/revenue/sku-breakdown`

Which products the AI-referred traffic actually bought. Shopify only.

```bash theme={null}
curl "https://api.traceten.com/v1/revenue/sku-breakdown?site_id=ttid_7Rb4TrC1dTbnD8w3s1TS12&days=30" \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

```json theme={null}
{
  "rows": [
    {
      "ai_source": "chatgpt",
      "product_name": "Trail Runner GTX",
      "sku": "TR-GTX-42",
      "units_sold": 18,
      "revenue_usd": 2340.0,
      "revenue_display": 2340.0
    }
  ],
  "currency": "USD",
  "converted": false
}
```

The default window is 30 days here, wider than the other revenue endpoints, because product-level data is sparser. The response is capped at 200 rows.

`units_sold` and `revenue_usd` are both net of returns. A partial refund takes back only the line items that came back, so a returned SKU loses its units and its revenue while the rest of the order keeps both. Either figure can be negative in a window where returns outran sales.

### Query parameters

This endpoint takes a **narrower** set than the other revenue routes. `attribution_model` and `source_kind` are not supported here, because the underlying product-level rollup has no column for either. Sending one is a `422`, not a silent no-op.

| Parameter   | Type    | Required | Notes                                              |
| ----------- | ------- | -------- | -------------------------------------------------- |
| `site_id`   | string  | yes      | The snippet key.                                   |
| `from`      | string  | no       | `YYYY-MM-DD`, inclusive. Must be sent with `to`.   |
| `to`        | string  | no       | `YYYY-MM-DD`, inclusive. Must be sent with `from`. |
| `days`      | integer | no       | 1 to 90. Defaults to 30.                           |
| `ai_source` | string  | no       | Restrict to one source.                            |

Any other parameter is rejected.

### If you have not connected Shopify

This endpoint returns `403` when the account has no Shopify connection, and `404` when the site is not one your key can reach. The two are distinct on purpose, and they answer different questions: by the time you can see the `403` you have already proved you own the site, so it tells you only that the connection is missing.

Connect Shopify in the dashboard, then retry.

## `GET /v1/revenue/ltv`

Cumulative attributed recurring revenue per subscription. This is the SaaS number: Claude referred this customer, and they have now paid you this much.

```bash theme={null}
curl "https://api.traceten.com/v1/revenue/ltv?site_id=ttid_7Rb4TrC1dTbnD8w3s1TS12&page=1&page_size=100" \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

```json theme={null}
{
  "rows": [
    {
      "subscription_id": "sub_1PqR2s3T4u5V6w",
      "ai_source": "claude",
      "ltv_usd": 1188.0,
      "payments": 12,
      "first_payment_at": "2025-09-04 11:22:31",
      "last_payment_at": "2026-08-04 11:22:31"
    }
  ],
  "page": 1,
  "page_size": 100,
  "has_more": false,
  "total_count": 1
}
```

### This endpoint takes no date range

Lifetime value is cumulative and all-time. A window would contradict the number it reports, so `from`, `to` and `days` are **rejected with a `422`**, not ignored. Use `/breakdown` or `/timeseries` when you want a period.

### Query parameters

| Parameter   | Type    | Required | Notes                                                     |
| ----------- | ------- | -------- | --------------------------------------------------------- |
| `site_id`   | string  | yes      | The snippet key.                                          |
| `page`      | integer | no       | 1-based. `(page - 1) * page_size` may not exceed 100,000. |
| `page_size` | integer | no       | 1 to 200. Defaults to 100. Highest LTV first.             |

### Reading the response

`ai_source` is the source that **acquired** the customer, not the source on the most recent payment. That is the whole point of the metric.

`ltv_usd` is always USD, and it is net of refunds. This endpoint does not convert to a display currency. `payments` stays gross, so a subscription that was billed twice and refunded once reads two payments and one payment's worth of value.

`first_payment_at` and `last_payment_at` are the first and last **payments**. A refund never becomes a subscription's last payment.

`total_count` counts `(subscription_id, ai_source)` pairs, because that is the unit `rows` lists. A subscription credited to two sources is two rows and two counts.

`has_more` is derived from the page coming back full, so it can be `true` on an exact multiple of `page_size` with an empty next page. `total_count` is exact if you would rather do the arithmetic.

## Errors

Standard [error shapes](/api/errors) apply. The ones specific to these endpoints:

| Status | When                                                                                                                                                                 |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `403`  | Shopify is not connected (`/sku-breakdown` only).                                                                                                                    |
| `404`  | The site is not on your account, or your key is scoped to a different site.                                                                                          |
| `422`  | `from` sent without `to`, `page_size` over 200, `days` over 90, a page past the depth limit, a malformed `site_id`, or an unsupported parameter on `/sku-breakdown`. |

## Next

* [Sessions API](/api/sessions) to drill from a revenue number into the sessions behind it.
* [Sources API](/api/sources) for session counts per source.
