> ## 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.

# Sources API

> Sessions and attributed revenue per traffic source, per day.

## Base URL and authentication

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

`GET /v1/sources/breakdown` takes `Authorization: Bearer <YOUR_API_KEY>` ([details](/api/authentication)) and the `stats:read` permission. Its `site_id` is the site's **snippet key**, the `ttid_` value your install snippet carries as `data-site`, which you can read from [`GET /v1/sites`](/api/sites).

[`GET /v1/sources`](#get-v1sources), at the bottom of this page, is different on both counts: a dashboard session only, and a UUID.

## `GET /v1/sources/breakdown`

One row per day per source, with attributed revenue joined on. This is the data behind the Sources page in the dashboard.

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

```json theme={null}
{
  "breakdown": [
    {
      "date": "2026-08-01",
      "ai_source": "chatgpt",
      "sessions": 120,
      "bounces": 34,
      "avg_time_on_page": 48.2,
      "revenue_usd_cents": 41900,
      "revenue_display": 419.0,
      "conversion_count": 7
    }
  ],
  "total_sessions": 420,
  "currency": "USD",
  "converted": false
}
```

### Choosing the window

Send either `from` and `to` together, or `days`. If you send neither, the window is the last 7 days.

| 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". |

Two limits apply to `from` and `to`:

* The range may not span more than 730 days.
* `from` may not be more than 730 days ago.

The second one is not arbitrary. Bounce rate is derived from session-level records, which are kept for at least 730 days and at most 760, while the session counts beside them are kept for the life of your account. Because the guarantee for any individual session is the 730-day end of that range, an older window could report a confident `0.0%` bounce rate instead of telling you the data is gone. It is refused instead.

### Response fields

| Field                | Type    | Notes                                                        |
| -------------------- | ------- | ------------------------------------------------------------ |
| `breakdown`          | array   | One row per (`date`, `ai_source`) pair.                      |
| `total_sessions`     | integer | All sessions for the site in the window, AI and non-AI.      |
| `currency`           | string  | Your site's reporting currency.                              |
| `converted`          | boolean | `false` when figures are already in your reporting currency. |
| `missing_rate_dates` | array   | Only present when some date had no exchange rate. See below. |

Each `breakdown` row:

| Field               | Type             | Notes                                                                       |
| ------------------- | ---------------- | --------------------------------------------------------------------------- |
| `date`              | string           | `YYYY-MM-DD`.                                                               |
| `ai_source`         | string           | `chatgpt`, `perplexity`, `direct`, `organic_search` and so on.              |
| `sessions`          | integer          | Sessions attributed to this source on this date.                            |
| `bounces`           | integer          | Sessions with a single interaction.                                         |
| `avg_time_on_page`  | number           | Seconds.                                                                    |
| `revenue_usd_cents` | integer          | Attributed revenue in US cents. Always USD, always an integer.              |
| `revenue_display`   | number \| `null` | The same money in your reporting currency. `null` when it cannot be stated. |
| `conversion_count`  | integer          | Revenue-generating conversions.                                             |

### About the two revenue fields

`revenue_usd_cents` is how revenue is stored: integer US cents, so it never drifts through rounding.

`revenue_display` is the same amount in your site's reporting currency. Each day converts at that day's exchange rate, so a figure you read last month does not change when rates move.

If we hold no rate for one of the dates in your window, that row's `revenue_display` is `null` and the date is listed in `missing_rate_dates`. It is never the US dollar figure relabelled with your currency.

If your reporting currency is USD, no conversion happens: `converted` is `false` and `revenue_display` equals `revenue_usd_cents / 100`.

### `ai_source` is a source label, not an AI flag

`ai_source` also carries non-AI values (`direct`, `organic_search`, `social`, `email`, `paid`, `referral`), so summing every row gives you total traffic rather than AI traffic. `total_sessions` is that same total, computed independently.

### Errors

| Status | Cause                                                                                                                                        |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, malformed, revoked or expired key.                                                                                                  |
| `403`  | The key is valid but lacks `stats:read`. The body names the permission.                                                                      |
| `404`  | No such site, or the key does not cover it.                                                                                                  |
| `422`  | `site_id` is missing or malformed, `from` without `to`, a range over 730 days, or a `from` past the 730 day floor. The body names the field. |
| `429`  | Rate limited. Back off and retry.                                                                                                            |

## `GET /v1/sources`

<Warning>
  **Dashboard session only.** Unlike `/breakdown` above, this endpoint does not accept a `tk_live_`
  API key. A key gets `401`, whatever its permissions. It is documented because it exists, not
  because you can call it with a key today.
</Warning>

The distinct source keys seen for a site: the `ai_source` values actually observed in the window,
plus the fixed list of non-AI channels. The dashboard uses it to populate its source filter.

Two things differ from every other read on this page. `site_id` is the site's Postgres id (a UUID),
not the `ttid_` snippet key, and the window is `days` rather than `from` and `to`.

```bash theme={null}
curl "https://api.traceten.com/v1/sources?site_id=018f0000-0000-7000-8000-000000000000&days=30"
```

| Parameter | Type    | Required | Notes                                              |
| --------- | ------- | -------- | -------------------------------------------------- |
| `site_id` | string  | yes      | `sites.id`, a UUID.                                |
| `days`    | integer | no       | 1 to 90, counting back from today. Defaults to 90. |

```json theme={null}
{
  "ai": ["chatgpt", "perplexity", "claude"],
  "non_ai": ["direct", "organic_search", "social", "email", "paid", "referral"]
}
```

`ai` depends on the window. `non_ai` is a fixed list and does not. This is one of the endpoints that
predates the `{ status, data }` envelope, so the object above is the whole body.

If the underlying query fails, `ai` comes back empty with a `200` and no caching headers, rather
than an error. Treat an empty `ai` as "no answer right now", not as "no AI traffic".

## Next

* [Recent events](/api/events) for individual events rather than daily totals
* [Goals](/api/goals) and [funnels](/api/funnels) for conversion reporting
