API
Reference for the MetrikStack web analytics API, covering API keys and scopes, the query endpoint, realtime events, sources, plans and usage.
The dashboard, the CLI and the MCP server all use this public API; there
are no dashboard-only endpoints. It is versioned under /v1, takes and
returns JSON, and serves its OpenAPI spec at /v1/openapi.json on the API
host (https://api.metrikstack.com).
Authentication
Every authenticated endpoint accepts two schemes:
- Session cookie (
metrikstack_session,httpOnly, same-site), used by the dashboard after sign-in. - API key:
Authorization: Bearer ak_.... Keys are created from the dashboard (session auth only; keys can't mint other keys) and carry scopes:read,write,admin.POST /v1/queryand the realtime stream need onlyread; creating sites/sources needswrite; API key and member management needadmin.
POST /v1/query HTTP/1.1
Host: api.metrikstack.com
Authorization: Bearer ak_live_...
Content-Type: application/jsonRate limits
Endpoints that read analytics data (POST /v1/query, the realtime stream,
coverage, errors, sources and usage) allow 600 requests an hour per API key
and per connected app such as the MCP server, refilled continuously, with
bursts of up to 120 at once. A signed-in dashboard user gets 6,000 an hour.
Over the limit the API answers 429 with a Retry-After header that says
how many seconds to wait. Other endpoints are not counted.
POST /v1/query
The analytics query endpoint. Every report in the dashboard (the main chart, top pages, referrers, countries, devices, custom events) is a variation of this request.
{
"site_id": "…",
"metrics": ["pageviews", "visitors", "sessions", "bounce_rate", "avg_duration"],
"dimensions": ["path"],
"filters": [{ "field": "country", "op": "eq", "value": "NL" }],
"range": { "from": "2026-09-01", "to": "2026-09-11" },
"granularity": "auto",
"include_bots": false,
"source_ids": null,
"limit": 50,
"order": [["pageviews", "desc"]]
}| Field | Type | Notes |
|---|---|---|
site_id | uuid | required |
metrics | Metric[] | required. pageviews, visitors, sessions, bounce_rate, avg_duration |
dimensions | Dimension[] | path, hostname, referrer_host, utm_source, utm_medium, utm_campaign, country, region, city, device_type, browser, os, language, event_name, bot_name, bot_category, source_kind, cache_status, status_code, traffic_class, channel, bot_verification, render_evidence |
filters | Filter[] | { field, op, value }; op is eq | neq | contains | in; value is a string or array of strings |
range | {from, to} | date range |
granularity | Granularity | auto | hour | day | week | month | none |
include_bots | boolean | default false |
source_ids | uuid[] | null | restrict to specific sources, e.g. to compare coverage |
limit | integer | caps returned rows |
order | [field, "asc"|"desc"][] |
Who made a request is in traffic_class: human (people), ai_assistant
(an AI assistant opened the page because someone asked it something),
ai_crawler (AI training and AI search indexing), search (search
engines) and automation (other bots). Set include_bots: true to see
more than human. How people arrived is in channel: direct, search,
social, ai, email, paid, campaign or referral. Channel, referrer
and UTM tags describe how a visit started, so the later pages of a visit
keep the source it came from.
Response (QueryResult):
{
"granularity": "day",
"dimensions": ["path"],
"metrics": ["pageviews"],
"rows": [
{ "bucket": 1756684800, "dimensions": ["/blog/hello"], "metrics": [128] }
]
}bucket is Unix seconds of the bucket start, or null for an un-bucketed
(no time dimension) query. dimensions and metrics on each row line up
positionally with the arrays in the request.
Example 1: time series
The main dashboard chart: daily pageviews and visitors over a range, no dimension breakdown.
{
"site_id": "b6c4...",
"metrics": ["pageviews", "visitors"],
"range": { "from": "2026-08-01", "to": "2026-09-11" },
"granularity": "auto"
}{
"granularity": "day",
"dimensions": [],
"metrics": ["pageviews", "visitors"],
"rows": [
{ "bucket": 1754006400, "dimensions": [], "metrics": [812, 640] },
{ "bucket": 1754092800, "dimensions": [], "metrics": [790, 615] }
]
}Example 2: top pages
A ranked breakdown by path, no time bucketing (granularity: "none"):
{
"site_id": "b6c4...",
"metrics": ["pageviews", "visitors"],
"dimensions": ["path"],
"range": { "from": "2026-09-01", "to": "2026-09-11" },
"granularity": "none",
"limit": 10,
"order": [["pageviews", "desc"]]
}{
"granularity": "none",
"dimensions": ["path"],
"metrics": ["pageviews", "visitors"],
"rows": [
{ "bucket": null, "dimensions": ["/"], "metrics": [4210, 3190] },
{ "bucket": null, "dimensions": ["/blog/hello"], "metrics": [1284, 1100] }
]
}Example 3: filtered breakdown
Countries for traffic from one campaign, people only (the default), restricted to one source, for example to check what the edge integration sees on its own:
{
"site_id": "b6c4...",
"metrics": ["pageviews", "visitors", "bounce_rate"],
"dimensions": ["country"],
"filters": [{ "field": "utm_campaign", "op": "eq", "value": "launch-2026" }],
"range": { "from": "2026-09-01", "to": "2026-09-11" },
"granularity": "none",
"include_bots": false,
"source_ids": ["a1e2..."]
}Granularity rules and point capping
granularity: "auto" picks a bucket size from the range:
- range ≤ 2 days →
hour - range ≤ 90 days →
day - longer →
week
Whatever the granularity, the number of returned points is capped. A year of data comes back in weekly buckets, for example, not as hourly points.
Query performance
Queries over ranges longer than 2 days may be served from pre-aggregated
data. The exception is bounce_rate broken down by a dimension, which is
always computed from individual events because bounce rates can't be added
up across slices.
The query shape is the same either way. It does mean that a
bounce_rate-by-path query over a long range can be slower than the same
query for pageviews.
Realtime: GET /v1/sites/{id}/realtime
A Server-Sent Events stream of a site's live traffic. Each data: line is
a JSON RealtimeMessage, tagged by type:
visitors:{ "type": "visitors", "current": 42, "ai_requests": 17 }. People seen in the last 5 minutes, and the pages AI assistants and AI crawlers read in the same window. Sent on connect and every 10 seconds.event:{ "type": "event", "event": LiveEvent }, a reduced copy of one event without hashes, withts,event_name,source_kind,path,referrer_host,country,city,device_type,browser,is_bot,bot_name,traffic_classandchannel. Sent for each event as it arrives, at most 10 per second per stream.burst:{ "type": "burst", "count": 37, "pageviews": 30, "bots": 7 }. Sent instead of individualeventmessages when more than 10 events arrive within one second. It carries only counts.
Which sources saw the people: GET /v1/sites/{id}/coverage
?from=&to=&granularity=&path=. People counted by which sources saw
them, over the whole range (total) and per bucket (series), in the
site's timezone:
both: seen by the script and by a server, edge or log source.source_only: the script never ran for them (ad blockers, no JavaScript, left before it loaded).script_only: seen by the script only. With a log source that sees every request, these are nearly always people whose browser reached the script over a different network than your site (IPv4 against IPv6).
The dashboard's "missed by the script" is (source_only - script_only) / (both + max(source_only, script_only)).
Errors over time: GET /v1/sites/{id}/errors/timeline
?from=&to=&granularity=. Error occurrences and affected visitors per
bucket (people only), and the releases first seen inside the range. A
release is known once an error event reports it (data-release), so a
release without errors does not show up. The dashboard marks new releases
and error spikes on its traffic chart with this.
Sources status: GET /v1/sites/{id}/sources
Returns each source with:
events_24h: events received in the last 24 hourspageviews_24h: pageview + enrichment rows in the last 24 hoursis_primary: whether this source is currently the site's primary source under itsprimary_source_policylast_event_at,status,kind,name
Coverage of a secondary source relative to the primary is
pageviews_24h / primary.pageviews_24h. The dashboard's Sources page uses
it to show, for example, that the edge integration sees 94% of what the
script sees.
POST /v1/sites/{id}/sources creates a source and returns its
ingest_token. The token is shown only in this response. script sources
have no token.
Members and orgs
GET /v1/me: the current session/key's identity, orgs and scopes.GET/POST /v1/orgs/{org_id}/members,PATCH/DELETE /v1/orgs/{org_id}/members/{user_id}: invite, list, change role (owner \| admin \| member), remove.GET/POST /v1/api-keys,DELETE /v1/api-keys/{id}: API key management. Minting a key requires session auth (keys can't create keys).
Sites
GET/POST /v1/sites,GET/PATCH/DELETE /v1/sites/{id}. APOSTalso creates the site'sscriptsource. Deleting a site removes it and its sources; event data already ingested is kept until the end of its normal retention period.
Plans, limits and usage
There is one plan with every source and feature, sold in volume tiers:
10k, 100k, 200k, 500k, 1M, 2M, 5M and 10M pageviews a month. New
organisations start with a 14-day trial without a card (plan trial, with
the limits of the 1M tier).
GET /v1/orgs/{org_id}/usage returns the plan, its limits and what the org is
currently using:
{
"org_id": "0f8e...",
"plan": "100k",
"limits": {
"sites": 50,
"events_per_month": 100000,
"site_events_per_min": 600,
"visitor_events_per_min": 240,
"errors_per_min": 60,
"api_keys": 25,
"members": 25
},
"events_this_month": 21877,
"sites": 2,
"api_keys": 1,
"members": 1
}Every tier allows 50 sites, 25 API keys and 25 members, and 240 events per minute per visitor. What grows with the tier:
| Tier | Pageviews / month | Events / minute / site | Errors / minute / site |
|---|---|---|---|
| 10k | 10,000 | 600 | 60 |
| 100k | 100,000 | 600 | 60 |
| 200k | 200,000 | 600 | 100 |
| 500k | 500,000 | 1,250 | 250 |
| 1M | 1,000,000 | 2,500 | 500 |
| 2M | 2,000,000 | 5,000 | 1,000 |
| 5M | 5,000,000 | 12,500 | 2,500 |
| 10M | 10,000,000 | 25,000 | 3,000 |
events_this_month counts pageviews and custom events by people on
the org's sites since the start of the calendar month (UTC). Bots, AI
assistants and AI crawlers, page-leave events and a second source's copy of
the same pageview are not counted, so connecting more sources doesn't use
more of your volume. Error reports have their own per-minute budget and are
not counted either. The number is refreshed every few minutes, so it can lag
a little.
How limits are enforced. Sites, API keys and members are checked when
you create one: POST /v1/sites, POST /v1/api-keys and
POST /v1/orgs/{org_id}/members return 402 with a body like
{ "error": "plan limit reached: the 10k plan allows 50 sites" }. Ingest
enforces the rate and volume limits and returns 429 once a site exceeds
its per-minute rate, a visitor exceeds theirs, or the org is over its
monthly volume ({ "error": "monthly quota exceeded" }). When the trial or
a paid period has ended without a plan, ingest returns 402
({ "error": "subscription inactive" }) and stops storing new data. Data
collected before that is kept.
Choose or change the plan in the dashboard under Settings → Billing. Prices are on the pricing page.
Rotating a source token
POST /v1/sites/{id}/sources/{source_id}/rotate issues a new ingest token for
a source and returns it once, in the same shape as source creation. Script
sources have no token, so rotating one is a 400.
curl -X POST https://api.metrikstack.com/v1/sites/$SITE/sources/$SOURCE/rotate \
-H "authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{"grace_hours": 24}'grace_hours (0-168, default 24) is how long the previous token keeps
working, so a fleet of collectors can be redeployed without dropping events.
Pass 0 to revoke the old token immediately. During the grace window ingest
accepts either token. A revocation can take up to a minute to take effect.
The source's token_rotated_at and previous_token_expires_at appear in
GET /v1/sites/{id}/sources. The CLI (not published yet) surfaces the same
thing as a warning:
metrikstack sources rotate my-site edge --grace-hours 2
metrikstack sources status my-siteFull reference
The complete OpenAPI 3.1 document, with every schema, endpoint and status
code, is served at /v1/openapi.json on the API host.
Errors
Lightweight error tracking for the browser and Node that groups uncaught exceptions into issues, without session replay, cookies or request bodies.
MCP server & CLI
Connect Claude, ChatGPT, Cursor or n8n to your web analytics with the MetrikStack MCP server, or query it from the terminal with the CLI.