REST API
Conventions
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:
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 for how tokens are created and scoped to a team.
curl https://clickbase.so/api/sites \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"
await fetch('https://clickbase.so/api/sites', {
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
});
import requests
requests.get(
'https://clickbase.so/api/sites',
headers={'Authorization': f'Bearer {token}', 'Accept': 'application/json'},
)
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
dataenvelope. - A non-paginated collection serializes as a flat JSON array.
- Only a paginated list endpoint keeps the standard
data/meta/linksenvelope.
The paginated envelope looks like this:
{
"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 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. |
curl "https://clickbase.so/api/sites/example.com/stats/events?range=last_7_days" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"
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' },
});
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'},
)
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):
?country=BR
?path[]=/pricing&path[]=/signup
visit_count is single-value and uses an operator prefix — all-time tracked session count per visitor:
?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.
curl "https://clickbase.so/api/sites/example.com/stats/events?country=BR&path[]=/pricing&path[]=/signup" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"
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' },
});
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'},
)
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. |
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:
{
"message": "The range field must be a string.",
"errors": {
"range": ["The range field must be a string."]
}
}
Other errors carry a plain message:
{
"message": "An active subscription is required to access this team."
}