MetrikStack

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/query and the realtime stream need only read; creating sites/sources needs write; API key and member management need admin.
POST /v1/query HTTP/1.1
Host: api.metrikstack.com
Authorization: Bearer ak_live_...
Content-Type: application/json

Rate 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"]]
}
FieldTypeNotes
site_iduuidrequired
metricsMetric[]required. pageviews, visitors, sessions, bounce_rate, avg_duration
dimensionsDimension[]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
filtersFilter[]{ field, op, value }; op is eq | neq | contains | in; value is a string or array of strings
range{from, to}date range
granularityGranularityauto | hour | day | week | month | none
include_botsbooleandefault false
source_idsuuid[] | nullrestrict to specific sources, e.g. to compare coverage
limitintegercaps 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, with ts, event_name, source_kind, path, referrer_host, country, city, device_type, browser, is_bot, bot_name, traffic_class and channel. 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 individual event messages 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 hours
  • pageviews_24h: pageview + enrichment rows in the last 24 hours
  • is_primary: whether this source is currently the site's primary source under its primary_source_policy
  • last_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}. A POST also creates the site's script source. 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:

TierPageviews / monthEvents / minute / siteErrors / minute / site
10k10,00060060
100k100,00060060
200k200,000600100
500k500,0001,250250
1M1,000,0002,500500
2M2,000,0005,0001,000
5M5,000,00012,5002,500
10M10,000,00025,0003,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-site

Full reference

The complete OpenAPI 3.1 document, with every schema, endpoint and status code, is served at /v1/openapi.json on the API host.

On this page