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

# Conventions

> The cross-cutting contract every authenticated Clickbase endpoint shares — base URL, headers, the response envelope, money, date ranges, filters, and error codes.

The cross-cutting contract every authenticated Clickbase endpoint shares — base URL, headers, the response envelope, money, date ranges, filters, and error codes. Read this once and the per-endpoint pages only need to describe what is unique to them.

## Base URL & headers

All endpoints live on the root domain, under the `/api` prefix:

```text theme={null}
https://clickbase.so/api
```

Send these headers on every authenticated request:

| Header          | Value                                                         |
| --------------- | ------------------------------------------------------------- |
| `Authorization` | `Bearer {token}` — a Passport OAuth bearer token.             |
| `Accept`        | `application/json`                                            |
| `Content-Type`  | `application/json` — on any request that carries a JSON body. |

See [Authentication](/api/authentication) for how tokens are created and scoped to a team.

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

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

  ```python title="Python" theme={null}
  import requests

  requests.get(
      'https://clickbase.so/api/sites',
      headers={'Authorization': f'Bearer {token}', 'Accept': 'application/json'},
  )
  ```
</CodeGroup>

## Response envelope

Responses are **unwrapped**. Clickbase calls `JsonResource::withoutWrapping()` globally in `AppServiceProvider`, so:

* A **single resource** serializes **flat** — its fields are the top-level object, with no `data` envelope.
* A **non-paginated collection** serializes as a flat JSON array.
* Only a **paginated list endpoint** keeps the standard `data` / `meta` / `links` envelope.

The paginated envelope looks like this:

```json theme={null}
{
    "data": [ /* the page of resources */ ],
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "total": 240,
        "last_page": 10,
        "from": 1,
        "to": 25,
        "path": "https://clickbase.so/api/sites/example.com/goals"
    },
    "links": {
        "first": "https://clickbase.so/api/sites/example.com/goals?page=1",
        "last": "https://clickbase.so/api/sites/example.com/goals?page=10",
        "prev": null,
        "next": "https://clickbase.so/api/sites/example.com/goals?page=2"
    }
}
```

### Paginated endpoints

These are the only endpoints that return the `data` / `meta` / `links` envelope. Every other data endpoint returns a flat object or flat array.

| Endpoint                                               | `page` | `per_page`                 |
| ------------------------------------------------------ | :----: | -------------------------- |
| `GET /api/sites/{siteKey}/goals`                       |   Yes  | 1–100, default **25**      |
| `GET /api/sites/{siteKey}/sessions`                    |   Yes  | Fixed page size (internal) |
| `GET /api/sites/{siteKey}/stats/events`                |   Yes  | 1–100, default **50**      |
| `GET /api/sites/{siteKey}/stats/page-titles`           |   Yes  | 1–100, default **25**      |
| `GET /api/sites/{siteKey}/stats/users`                 |   Yes  | 1–100, default **25**      |
| `GET /api/sites/{siteKey}/stats/errors`                |   Yes  | 1–100, default **20**      |
| `GET /api/sites/{siteKey}/stats/errors/{error}/events` |   Yes  | 1–100, default **20**      |
| `GET /api/sites/{siteKey}/replays`                     |   Yes  | Fixed page size (internal) |

Advance through pages with `?page=`. Where `per_page` is accepted it is validated as an integer `1–100`; a value outside that range returns `422`. The sessions and replays lists page at a fixed internal size, so they take `?page=` but not `?per_page=`.

## Money

Money is always an **integer in minor units** (cents) — never a float and never a formatted string. A `revenue` of `4200` means **\$42.00**. This holds everywhere revenue appears (goal revenue, the [Revenue](/api/revenue) reports, session/user values). Format for display on your side by dividing by 100 for a two-decimal currency; the currency itself is reported separately as an ISO 4217 code.

## Date ranges

Every reporting endpoint accepts the same date-range inputs. Pass **either** a preset `range` **or** an explicit `from`/`to` pair (which takes precedence). Boundaries are computed in the **site's own timezone**, then converted to UTC.

**Preset `range` values** (from `App\Actions\Stats\ResolveDateRange::RANGES`):

| Value            | Window                                          |
| ---------------- | ----------------------------------------------- |
| `today`          | The current calendar day at the site (default). |
| `yesterday`      | The previous calendar day.                      |
| `last_7_days`    | The trailing 7 days, ending today.              |
| `last_30_days`   | The trailing 30 days, ending today.             |
| `this_month`     | Month-to-date.                                  |
| `last_month`     | The whole previous calendar month.              |
| `last_12_months` | The trailing 12 months.                         |
| `all`            | Since the beginning of the site's data.         |

When `range` is missing or unrecognized it falls back to **`today`**.

**Custom range** — supply both `from` and `to` as `Y-m-d` dates (e.g. `from=2026-01-01&to=2026-01-31`). Both must be valid and `from` must be on or before `to`, otherwise the pair is ignored and the preset `range` applies.

| Parameter | Type   | Description                                 |
| --------- | ------ | ------------------------------------------- |
| `range`   | string | One of the preset values above.             |
| `from`    | string | Custom range start, `Y-m-d`. Requires `to`. |
| `to`      | string | Custom range end, `Y-m-d`. Requires `from`. |

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

  ```javascript title="JavaScript" theme={null}
  const params = new URLSearchParams({ range: 'last_7_days' });
  await fetch(`https://clickbase.so/api/sites/example.com/stats/events?${params}`, {
    headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
  });
  ```

  ```python title="Python" theme={null}
  import requests

  requests.get(
      'https://clickbase.so/api/sites/example.com/stats/events',
      params={'range': 'last_7_days'},
      headers={'Authorization': f'Bearer {token}', 'Accept': 'application/json'},
  )
  ```
</CodeGroup>

## Filters

Reporting endpoints accept the same dashboard filter keys as query parameters (allowlisted in `App\Actions\Stats\ResolveFilters`). Any key not in this list is ignored.

| Group       | Keys                                                                  |
| ----------- | --------------------------------------------------------------------- |
| Page        | `path`, `entry_path`, `title`, `hostname`, `event_name`               |
| Visitor     | `user_id`, `visit_count`                                              |
| Acquisition | `referrer`, `channel`, `source`, `campaign`                           |
| UTM         | `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term` |
| Tech        | `browser`, `os`, `size`                                               |
| Location    | `country`, `region`, `city`                                           |
| Goal        | `goal`                                                                |
| Property    | `prop_key` + `prop_value` (both required together)                    |

Each dimension takes **a single value or a list**. Repeat the key with `[]` to match any of several values (`WHERE IN`):

```text theme={null}
?country=BR
?path[]=/pricing&path[]=/signup
```

`visit_count` is single-value and uses an operator prefix — all-time tracked session count per visitor:

```text theme={null}
?visit_count=is:1
?visit_count=gte:2
?visit_count=lte:5
```

Operators: `is`, `gte`, `lte`.
The `prop_key` / `prop_value` pair must be supplied together — either both are present (non-empty) or neither is applied.

<CodeGroup>
  ```bash title="curl" theme={null}
  curl "https://clickbase.so/api/sites/example.com/stats/events?country=BR&path[]=/pricing&path[]=/signup" \
    -H "Authorization: Bearer {token}" \
    -H "Accept: application/json"
  ```

  ```javascript title="JavaScript" theme={null}
  const params = new URLSearchParams({ country: 'BR' });
  params.append('path[]', '/pricing');
  params.append('path[]', '/signup');
  await fetch(`https://clickbase.so/api/sites/example.com/stats/events?${params}`, {
    headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
  });
  ```

  ```python title="Python" theme={null}
  import requests

  requests.get(
      'https://clickbase.so/api/sites/example.com/stats/events',
      params={'country': 'BR', 'path[]': ['/pricing', '/signup']},
      headers={'Authorization': f'Bearer {token}', 'Accept': 'application/json'},
  )
  ```
</CodeGroup>

## Errors & status codes

| Status                     | Meaning                                                                                                                                               |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401 Unauthorized`         | Missing or invalid bearer token (the `auth:api` guard rejected it).                                                                                   |
| `402 Payment Required`     | The token's team has no active subscription. See [Authentication](/api/authentication).                                                               |
| `403 Forbidden`            | The token is bound to a team you no longer belong to, or your team role lacks the permission for a write (e.g. deleting a site requires Owner/Admin). |
| `404 Not Found`            | The site / goal / funnel / resource is unknown or not in your team.                                                                                   |
| `422 Unprocessable Entity` | Validation failed, or your account has no active team.                                                                                                |
| `429 Too Many Requests`    | Rate limit exceeded (the authenticated group is `60` requests/minute).                                                                                |

A validation error (`422`) carries a `message` and an `errors` map keyed by field name:

```json theme={null}
{
    "message": "The range field must be a string.",
    "errors": {
        "range": ["The range field must be a string."]
    }
}
```

Other errors carry a plain `message`:

```json theme={null}
{
    "message": "An active subscription is required to access this team."
}
```
