REST API
Authentication
The authenticated data endpoints use Passport OAuth bearer tokens (the auth:api guard). Send your token on every request:
Authorization: Bearer {token}
The same tokens authenticate both this REST API and the MCP connectors — one token, two surfaces.
Creating a token
Create a personal access token either way — both call the same Action:
- In the app, on the Developers settings page.
- Over the API, with
POST /api/tokens.
The plaintext token is shown once, at creation time, and is never stored or shown again. Copy it immediately.
curl -X POST https://clickbase.so/api/tokens \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"name": "Reporting script"}'
await fetch('https://clickbase.so/api/tokens', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ name: 'Reporting script' }),
});
import requests
requests.post(
'https://clickbase.so/api/tokens',
json={'name': 'Reporting script'},
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json',
'Accept': 'application/json',
},
)
Team binding
A token is scoped to a single team. Tenancy is resolved by the LoadTeamFromToken middleware on every authenticated request:
- Personal access tokens (created on the Developers page or via
POST /api/tokens) are pinned to the team they were minted for. That binding is stable — it never follows the creating user's current-team switches. A token created for Team A always acts on Team A. last_used_atis stamped on the token on every authenticated request.- Fail-closed on lost membership: the team is resolved through your team memberships. If you no longer belong to the token's team, the request is rejected with
403 Forbidden("This token is bound to a team you no longer belong to.").
Because the resolved team is written onto the acting user's currentTeam, every endpoint scopes its reads and writes through $team->sites(). A token can never reach another team's data by passing a foreign id or domain.
Subscription requirement
The authenticated management and reporting endpoints require the token's team to have an active subscription. After team binding, the EnsureTokenTeamHasActiveSubscription middleware (team.subscription.token on the group in routes/api.php) re-checks Team::subscriptionAccess()->allowsProductAccess() on every request — the same rule the web app uses.
A team whose trial has expired or that has no subscription is cut off from the whole dashboard, reads and writes alike, with a plain 402 Payment Required:
{
"message": "An active subscription is required to access this team."
}
The public tracking ingestion beacons (/api/collect, /api/event, /api/identify, /api/engage, /api/session-replay/record, /api/tracking-config/{trackingKey}) and the Stripe webhook are exempt — they authenticate per-site (or not at all) and never pass through this middleware, so data keeps flowing even while billing lapses. The Payments API is not exempt: it requires an active subscription like every other authenticated endpoint.
Site allowlist
When you create a personal access token — on the Developers settings page or via POST /api/tokens — you can optionally limit it to specific sites with site_ids (an array of site UUIDs). Omit the field, pass null, or pass ["*"] to allow all sites in the workspace. A token with a non-null allowlist can only reach those sites; a foreign site returns 404 Not Found, same as a site outside your workspace. See API tokens for the create payload.
Failure modes
| Condition | Status | Detail |
|---|---|---|
| Missing / invalid token | 401 Unauthorized |
The auth:api guard rejects the request. |
| Token bound to a team you left | 403 Forbidden |
Membership is re-checked on every request ("This token is bound to a team you no longer belong to."). |
| Team without an active subscription | 402 Payment Required |
"An active subscription is required to access this team." |
| Your account has no active team | 422 Unprocessable Entity |
"Your account has no active team. Create a team before using the API." |
| Site not in your team (foreign or unknown id/domain) | 404 Not Found |
Sites are looked up via $team->sites(). |
Rate limiting
The whole authenticated data group is throttled at 60 requests per minute (throttle:60,1) per token. Exceeding it returns 429 Too Many Requests. A few reads carry an additional dedicated bucket on top of the group limit (search-console keyword lookups isolate Google's quota). The tracking ingestion and Stripe webhook endpoints have their own, separate limits.
Example
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'},
)