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

# Insights

> The publisher insights endpoint — query parameters, the bare list response, and the error catalog.

One read-only endpoint returns a bare list of metric rows for one publisher. With a publisher key that is the key's own publisher; with an organization key you choose it with `publisher_id`.

Money is **US dollars** (floats): `revenue`, `ecpm`, and `cpc` (e.g. `5.0` = \$5.00); `ctr` is a fraction (`0.01` = 1%). `revenue` is your **net** share.

How `revenue` accrues depends on your account's **payment model**, configured by Thrad: **CPM** accounts (the default) earn a share of advertiser spend on viewable (confirmed on-screen) impressions; **CPC** accounts earn a fixed per-click rate × your revenue share on measured clicks. Clicks are measured on DSP-linked impressions, so CPC revenue is always consistent with the `clicks` metric shown.

## Authorizations

**thrad-api-key** `string` *required*

One of two keys. `Authorization: Bearer <key>` is accepted as an alternative. A missing key returns `401 auth_required`; an unknown, expired, or revoked key returns `401 invalid_api_key`.

* **Publisher key** (`sk_...`) — the same key you use to serve ads. It reads only its own publisher. Find it in the dashboard under **Publisher → Configuration → Keys**.
* **Organization key** (`sko_...`) — one key for your whole organization that reads any of its publishers; name the one you want with `publisher_id`. An organization owner or admin creates, rotates, and revokes it under **Settings → API key**. It is read-only and **can't serve ads**. Rotating keeps the old key working for a short grace window (30 minutes by default) so you can switch over; revoking stops it immediately. The full key is shown once, when it is created.

## Choosing a publisher

**publisher\_id** `string`

The id of the publisher to read (a UUID — list yours with [`GET /v1/publisher/publishers`](https://docs.thrad.ai/insights/publishers)).

* **Organization key:** required. Without it you get `400 missing_required_parameter`. A publisher outside your organization, or one that was deleted, returns `404 not_found`.
* **Publisher key:** optional. When sent, it must be the key's own publisher; any other id returns `404 not_found`.

A value that isn't a UUID returns `400 invalid_value`.

## Query parameters

Repeated parameters use the `name[]` convention (e.g. `fields[]=impressions&fields[]=revenue`).

**time\_granularity** `string` (default: daily)

How rows are bucketed over time: `hourly`, `daily` (default), `monthly`, or `none`. With `none` a single row covers the whole requested range and has no `readable_time`. Any other value returns `invalid_value`. Not applicable in breakdown mode (see `segments[]`).

**time\_ranges\[]** `array`

One or more JSON window objects; each may span **at most 366 days** (`range_too_wide`). When omitted, a trailing 30-day window is used.

**time\_ranges\[] object**

**unix\_range** `object`

`{"type":"unix_range","start":<unix>,"end":<unix>}` — Unix seconds (UTC).

**date\_range** `object`

`{"type":"date_range","since":"YYYY-MM-DD","until":"YYYY-MM-DD","timezone":"UTC"}` — calendar dates; `timezone` is an optional IANA name (defaults UTC; unknown → `invalid_time_range`).

**hour\_range** `object`

`{"type":"hour_range","since":"YYYY-MM-DDTHH","until":"YYYY-MM-DDTHH"}` — hour-precision bounds, pairs with `time_granularity=hourly`.

> [!NOTE]
>
> `end` must be after `start` (`invalid_time_range`). With `time_granularity=hourly`, each window must be **25 hours or less** (`hourly_range_too_wide`).

**fields\[]** `array`

Which metrics to project onto each row: `impressions`, `clicks`, `revenue`, `ctr`, `ecpm`, `cpc`, `fill_nominal`, `fill_real`, `fill_requests`, `fill_rate`, `nominal_fill_rate`. Defaults to `impressions`, `clicks`, `revenue` (fill metrics are opt-in — the default response is unchanged). An unknown metric returns `unknown_field`.

**segments\[]** `array`

Roll the whole range up by one dimension — `country`, `intent`, or `device` — returning one row per value (ordered by impressions). **At most one** (`too_many_segments`); an unrecognized value returns `invalid_segment`.

> [!NOTE]
>
> A breakdown spans the whole range, so it **can't be combined with an explicit `time_granularity`** (`segment_time_conflict`). On breakdown rows, `clicks`, `ctr`, and `cpc` are **populated for CPC-model accounts** (per-dimension revenue rides those clicks) and **`null` for CPM-model accounts** (clicks aren't broken out per dimension there). Fill metrics are always `null` on breakdown rows.

**sort\[]** `array`

One or more JSON `{ "field", "direction" }` objects, applied in order. `field` is a metric; `direction` is `asc` or `desc`. A non-metric field or bad direction returns `invalid_value`. Row `id` is always the final deterministic tiebreak.

**limit** `integer` (default: 20)

Maximum rows in the page, `1`–`2000` (default `20`). Out of range returns `invalid_value`.

**before** `string`

Cursor for the previous page — pass a prior response's `first_id`. Mutually exclusive with `after` (`invalid_cursor` if both). An unknown cursor returns `invalid_cursor`.

**after** `string`

Cursor for the next page — pass a prior response's `last_id`. Mutually exclusive with `before`.

> [!NOTE]
>
> The advertiser-only parameters `aggregation_level`, `source`, and `filters[]` are **not supported** here and return `400 unsupported_parameter` — they are never silently ignored.

```bash cURL — daily revenue theme={null}
curl -G https://api.thrad.ai/v1/publisher/insights \
  -H "thrad-api-key: $THRAD_API_KEY" \
  --data-urlencode 'time_granularity=daily' \
  --data-urlencode 'fields[]=impressions' \
  --data-urlencode 'fields[]=revenue' \
  --data-urlencode 'fields[]=ecpm' \
  --data-urlencode 'time_ranges[]={"type":"date_range","since":"2026-04-01","until":"2026-04-08"}'
```

```bash cURL — revenue by country theme={null}
curl -G https://api.thrad.ai/v1/publisher/insights \
  -H "thrad-api-key: $THRAD_API_KEY" \
  --data-urlencode 'segments[]=country' \
  --data-urlencode 'fields[]=impressions' \
  --data-urlencode 'fields[]=revenue' \
  --data-urlencode 'time_ranges[]={"type":"date_range","since":"2026-04-01","until":"2026-04-30"}'
```

```bash cURL — fill rate theme={null}
curl -G https://api.thrad.ai/v1/publisher/insights \
  -H "thrad-api-key: $THRAD_API_KEY" \
  --data-urlencode 'time_granularity=none' \
  --data-urlencode 'fields[]=impressions' \
  --data-urlencode 'fields[]=fill_requests' \
  --data-urlencode 'fields[]=fill_rate' \
  --data-urlencode 'fields[]=nominal_fill_rate' \
  --data-urlencode 'time_ranges[]={"type":"date_range","since":"2026-04-01","until":"2026-04-30"}'
```

```bash cURL — organization key theme={null}
curl -G https://api.thrad.ai/v1/publisher/insights \
  -H "thrad-api-key: $THRAD_ORG_KEY" \
  --data-urlencode 'publisher_id=6f1c2b8e-1d2a-4c3b-9e8f-0a1b2c3d4e5f' \
  --data-urlencode 'fields[]=impressions' \
  --data-urlencode 'fields[]=revenue' \
  --data-urlencode 'time_ranges[]={"type":"date_range","since":"2026-04-01","until":"2026-04-08"}'
```

```python Python theme={null}
import os, json, requests

resp = requests.get(
    "https://api.thrad.ai/v1/publisher/insights",
    headers={"thrad-api-key": os.environ["THRAD_API_KEY"]},
    params={
        "time_granularity": "daily",
        "fields[]": ["impressions", "clicks", "revenue", "ecpm"],
        "time_ranges[]": json.dumps(
            {"type": "date_range", "since": "2026-04-01", "until": "2026-04-08"}
        ),
    },
)
for row in resp.json()["data"]:
    print(row["readable_time"], row["revenue"], row["ecpm"])
```

```json 200 - Daily rows theme={null}
{
  "object": "list",
  "data": [
    {
      "id": "start=1775347200:end=1775433600",
      "start_time": 1775347200,
      "end_time": 1775433600,
      "readable_time": "2026-04-02",
      "timezone": "UTC",
      "impressions": 1000,
      "revenue": 5.0,
      "ecpm": 5.0
    }
  ],
  "count": 1,
  "first_id": "start=1775347200:end=1775433600",
  "last_id": "start=1775347200:end=1775433600",
  "has_more": false
}
```

```json 200 - By country (segments[]=country) theme={null}
{
  "object": "list",
  "data": [
    {
      "id": "start=1775347200:end=1775433600:country=US",
      "start_time": 1775347200,
      "end_time": 1775433600,
      "timezone": "UTC",
      "country": "US",
      "impressions": 800,
      "revenue": 4.0,
      "clicks": null,
      "ctr": null
    }
  ],
  "count": 1,
  "first_id": "start=1775347200:end=1775433600:country=US",
  "last_id": "start=1775347200:end=1775433600:country=US",
  "has_more": false
}
```

```json 200 - Fill metrics (fields[]=fill_* / nominal_fill_rate) theme={null}
{
  "object": "list",
  "data": [
    {
      "id": "start=1775347200:end=1777852800",
      "start_time": 1775347200,
      "end_time": 1777852800,
      "timezone": "UTC",
      "impressions": 108,
      "fill_requests": 404,
      "fill_rate": 0.25,
      "nominal_fill_rate": 0.5
    }
  ],
  "count": 1,
  "first_id": "start=1775347200:end=1777852800",
  "last_id": "start=1775347200:end=1777852800",
  "has_more": false
}
```

```json 401 - Missing key theme={null}
{
  "error": {
    "message": "Authentication credentials were not provided.",
    "type": "authentication_error",
    "param": null,
    "code": "auth_required"
  }
}
```

## Response

The endpoint returns a bare list object — **not** the platform `{ success, data, error, meta }` envelope.

**object** `string`

Always `"list"`.

**data** `array`

The metric rows for this page. Each row is a flat object.

**row properties**

**id** `string`

The row's opaque id, used as a pagination cursor. Composed as `start=<unix>:end=<unix>`, with `:<segment>=<value>` appended in breakdown mode.

**start\_time** `integer`

Bucket start as Unix seconds (UTC).

**end\_time** `integer`

Bucket end as Unix seconds (UTC), exclusive.

**readable\_time** `string`

Human-readable bucket label — `2026-04-02` (daily), `2026-04` (monthly), or `2026-04-02T13` (hourly). **Omitted** when `time_granularity=none` and in breakdown mode.

**timezone** `string`

Always `"UTC"`.

**country / intent / device** `string`

The dimension value for this sub-row. Present only with the matching `segments[]` value.

**impressions** `integer`

Impression count. Present when requested in `fields[]` (default).

**clicks** `integer`

Click count. Present when requested in `fields[]` (default). On breakdown rows: populated for CPC-model accounts, `null` for CPM-model accounts (not broken out per dimension there).

**revenue** `float`

Your **net** revenue in **dollars** (e.g. `5.0` = \$5.00). Present when requested in `fields[]` (default). CPM accounts: share of advertiser spend on viewable (confirmed on-screen) impressions. CPC accounts: clicks × your per-click rate × revenue share.

**ctr** `float`

Click-through rate as a fraction (clicks ÷ impressions). `null` when impressions are zero, and on breakdown rows of CPM-model accounts. Present only when requested in `fields[]`.

**ecpm** `float`

Effective revenue per thousand impressions in **dollars** (revenue ÷ impressions × 1000). `null` when impressions are zero. Present only when requested in `fields[]`.

**cpc** `float`

Revenue per click in **dollars** (revenue ÷ clicks). `null` when clicks are zero, and on breakdown rows of CPM-model accounts. Present only when requested in `fields[]`.

**fill\_nominal** `integer`

All ad impressions delivered to your placement in the bucket, whether or not the render was confirmed. Per-bucket on time-series rows; `null` on breakdown rows. Present only when requested in `fields[]`.

**fill\_real** `integer`

The confirmed-rendered subset of `fill_nominal` — impressions your placement actually displayed. `null` on breakdown rows. Present only when requested in `fields[]`.

**fill\_requests** `integer`

Production ad requests received for your placement in the bucket — the denominator of both fill rates. `null` on breakdown rows. Present only when requested in `fields[]`.

**fill\_rate** `float`

The **real** fill rate: `fill_real ÷ fill_requests` as a fraction (`0.25` = 25%) — confirmed-rendered impressions over ad requests. `null` when `fill_requests` is zero or on breakdown rows. Present only when requested in `fields[]`.

**nominal\_fill\_rate** `float`

The nominal (served) fill rate: `fill_nominal ÷ fill_requests` — all delivered impressions over ad requests, before render confirmation. `null` when `fill_requests` is zero or on breakdown rows. Present only when requested in `fields[]`.

**count** `integer`

Number of rows in `data` on this page.

**first\_id** `string`

Id of the first row — pass as `before` for the previous page. `null` for an empty page.

**last\_id** `string`

Id of the last row — pass as `after` for the next page. `null` for an empty page.

**has\_more** `boolean`

`true` if more rows exist beyond this page.

## Errors

Errors use the bare OpenAI shape — `{ "error": { "message", "type", "param", "code" } }`. `type` is one of `invalid_request_error`, `authentication_error`, `rate_limit_error`, or `server_error`.

| Status | Type | Code | When |
| - | - | - | - |
| `400` | `invalid_request_error` | `invalid_value` | A parameter has a bad value (`time_granularity`, `sort`, `limit`, `publisher_id`, …). |
| `400` | `invalid_request_error` | `missing_required_parameter` | An organization key was sent without `publisher_id`. |
| `400` | `invalid_request_error` | `invalid_time_range` | A `time_ranges[]` entry is malformed, has `end` ≤ `start`, or an unknown timezone. |
| `400` | `invalid_request_error` | `range_too_wide` | A window exceeds the 366-day maximum. |
| `400` | `invalid_request_error` | `hourly_range_too_wide` | An `hourly` window exceeds 25 hours. |
| `400` | `invalid_request_error` | `unknown_field` | A `fields[]` value is not a known metric. |
| `400` | `invalid_request_error` | `invalid_segment` | An unrecognized `segments[]` value. |
| `400` | `invalid_request_error` | `too_many_segments` | More than one `segments[]` value. |
| `400` | `invalid_request_error` | `segment_time_conflict` | `segments[]` passed with an explicit `time_granularity`. |
| `400` | `invalid_request_error` | `invalid_cursor` | `before` and `after` both passed, or an unknown cursor. |
| `400` | `invalid_request_error` | `unsupported_parameter` | An advertiser-only param (`aggregation_level`, `source`, `filters`) was passed. |
| `401` | `authentication_error` | `auth_required` | No API key was sent. |
| `401` | `authentication_error` | `invalid_api_key` | The key is malformed, unknown, expired, or revoked. |
| `404` | `invalid_request_error` | `not_found` | `publisher_id` names a publisher this key can't read: another organization's, a deleted one, or (with a publisher key) any publisher but its own. |
| `429` | `rate_limit_error` | `rate_limit_exceeded` | Exceeded the per-key limit of 1000 requests/hour. An organization key has one limit shared across all its publishers. |
| `504` | `server_error` | `clickhouse_timeout` | The analytics query timed out. Narrow the range or granularity and retry. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.