Region API
Query your data directly in its region with a short-lived region token.
Query results never pass through the control plane. You ask the control plane for a region token, then
call your project's region API directly: https://eu.api.analytics.atrix.dev for EU projects.
Get a region token
curl -X POST "https://app.analytics.atrix.dev/v1/projects/$PROJECT_ID/region-token" \
-H "Authorization: Bearer $ATRIX_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "environment": "production", "scopes": ["query:read"] }'{
"token": "eyJhbGciOiJFZERTQSIs…",
"region_api_url": "https://eu.api.analytics.atrix.dev",
"expires_at": "2026-09-25T10:10:00Z",
"region": "eu",
"environment": "production",
"scopes": ["query:read"]
}The token is an EdDSA (Ed25519) JWT that lives 10 minutes by default and never more than 15. It carries the
project, environment and scopes; the region reads them only from the token. Its scopes are your token's
regional scopes intersected with your role, optionally narrowed further with scopes.
Run a query
The shortcut POST /v1/projects/{project_id}/query validates a Query IR document and returns a query:read
region token together with the exact region request to send:
curl -X POST "https://eu.api.analytics.atrix.dev/query" \
-H "Authorization: Bearer $REGION_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"query": {
"ir_version": 1,
"date_range": { "from": "-30d" },
"insight": {
"kind": "trends",
"interval": "day",
"series": [{ "event": "signed_up", "math": "unique_persons" }],
"breakdown": { "type": "event", "property": "$os" }
}
},
"include_sql": true
}'Insight kinds: trends, funnel, retention, paths, lifecycle, stickiness and revenue.
- Math:
total,unique_persons,unique_sessions,unique_groups,sum,avg,min,max,p50,p90,p99. - Filter operators:
eq,neq,in,not_in,contains,not_contains,regex,not_regex,gt,gte,lt,lte,is_set,is_not_set. - Dates:
now,today,yesterday, relative (-7d,-12w,-3m,-1y,-24h), a date or an ISO 8601 timestamp. - Defaults: funnel window 14 days, retention 7 periods, paths depth 5 with 50 edges.
include_sql: true returns the compiled, parameterised SQL next to the result, the same SQL Ask cites as
evidence. Queries time out after 30 seconds and read at most 500 million rows. Results are cached for 60
seconds for recent data and an hour for historical ranges; refresh: true bypasses the cache. Each project
runs at most two queries at once (429 too_many_concurrent_queries beyond that).
Routes
| Route | Scope |
|---|---|
POST /query | query:read |
POST /sql | sql.raw; see SQL |
GET /schema/events, GET /schema/properties | query:read |
GET /events/live | query:read |
GET /persons, GET /persons/{id}, GET /persons/{id}/events, GET /persons/{id}/identity | persons:read (personal data masked unless persons.pii) |
POST /persons/{id}/unmerge | persons:write |
GET /groups, GET /groups/types, GET /groups/{index}/{key} | persons:read |
POST /cohorts/preview, POST /cohorts/{id}/materialize, POST /cohorts/{id}/static | cohorts:* |
GET /catalog/discovery | catalog:read |
GET /replays, GET /replays/{id}, GET /replays/{id}/blocks/{name} | replays:read |
POST /ask, GET /ask/conversations, GET /ask/conversations/{id} | ai:read |
POST /experiments/stats | experiments:read |
Errors use the same { "error": { "code", "message" } } shape as the control plane, with codes
unauthorized, forbidden, invalid_query, bad_request, not_found, conflict, query_too_large (422),
too_many_concurrent_queries (429), unavailable and internal.