REST API

Read your analytics and manage your account programmatically. The read API is the HTTP twin of the MCP server — same data, plain HTTP, JSON responses — and the management API gives you full CRUD over sites, keys, team, and workspace settings.

Authentication

All requests use a bearer token. There are two kinds of key, each with its own scope:

Key Prefix Scope Created from
Site keyabner_Read-only analytics for one siteSite → Settings → API key
Management keyabner_ws_Full CRUD across one workspaceAccount → Settings → Management API Keys
Authorization: Bearer abner_xxxx…        # read endpoints
Authorization: Bearer abner_ws_xxxx…     # management endpoints

The full key is shown once at creation; only a hash is stored, so save it somewhere safe. Treat keys like passwords; never embed them in client-side code (responses are not CORS-enabled, so the API is intended for server-side use). A missing or wrong-type key returns 401 Unauthorized — read endpoints require a site key, management endpoints require a management key.

Base URL

https://www.abner.app/api/v1

Vocabulary

The query endpoints share one vocabulary. GET /api/v1/meta returns the live, authoritative lists; the current values are:

  • Periods: 24h, 7d, 14d, 30d, month, year, all
  • Metrics: visitors, pageviews, sessions, pageviews_per_visitor, events
  • Dimensions: pathname, referrer_host, country, region, city, browser, device_type, utm_source, utm_medium, utm_campaign, event_name (plus time_day / time_hour for time series)
  • Filters (as filter.<key>=value): pathname, referrer_host, country, browser, device_type, utm_source, utm_medium, utm_campaign, event_name

Read API

Authenticated with a site key (abner_…). All read endpoints are GET and scoped to that key's site.

GET /site

Returns the site the key is scoped to. Useful as a "whoami" / key-check.

curl https://www.abner.app/api/v1/site \
  -H "Authorization: Bearer $ABNER_API_KEY"

{
  "site_id": "9f3c…",
  "name": "Example",
  "domain": "example.com",
  "timezone": "UTC",
  "workspace_id": "1a2b…"
}

GET /stats

A single overview row of aggregate metrics for the period. Query params: period (default 7d), any filter.<key>.

curl "https://www.abner.app/api/v1/stats?period=30d&filter.country=US" \
  -H "Authorization: Bearer $ABNER_API_KEY"

{
  "site_id": "9f3c…",
  "period": "30d",
  "stats": { "visitors": 1240, "pageviews": 3877, "sessions": 1502, "pageviews_per_visitor": 3.12 }
}

GET /timeseries

Metrics bucketed over time. Query params: period (default 30d), interval = day | hour (default day), metrics (comma-separated, default visitors,pageviews), filters.

curl "https://www.abner.app/api/v1/timeseries?period=7d&interval=day&metrics=visitors" \
  -H "Authorization: Bearer $ABNER_API_KEY"

{
  "site_id": "9f3c…",
  "period": "7d",
  "interval": "day",
  "points": [
    { "time": "2026-05-19T00:00:00Z", "metrics": { "visitors": 180 } },
    { "time": "2026-05-20T00:00:00Z", "metrics": { "visitors": 205 } }
  ]
}

GET /breakdown/{dimension}

Top values for a single dimension (e.g. top pages, referrers, countries). Path: any dimension except the time buckets. Query params: period (default 7d), metrics (default visitors,pageviews), limit (default 50, max 1000), filters.

curl "https://www.abner.app/api/v1/breakdown/pathname?period=30d&limit=5" \
  -H "Authorization: Bearer $ABNER_API_KEY"

{
  "site_id": "9f3c…",
  "period": "30d",
  "dimension": "pathname",
  "results": [
    { "value": "/", "metrics": { "visitors": 820, "pageviews": 1610 } },
    { "value": "/pricing", "metrics": { "visitors": 210, "pageviews": 340 } }
  ]
}

GET /metrics

The swiss-army query — combine any metrics, dimensions, filters, and period. This is the direct mirror of the MCP query_metrics tool. Query params: metrics (required, comma-separated), dimensions (optional, comma-separated), period, limit, filter.<key>.

curl "https://www.abner.app/api/v1/metrics?metrics=visitors,pageviews&dimensions=country,browser&period=7d&limit=10" \
  -H "Authorization: Bearer $ABNER_API_KEY"

{
  "site_id": "9f3c…",
  "period": "7d",
  "rows": [
    { "dimensions": { "country": "US", "browser": "Chrome" }, "metrics": { "visitors": 412, "pageviews": 980 } }
  ]
}

With no dimensions you get a single overview row (same as /stats). Add time_day or time_hour as a dimension to get a time series, where each row carries a time field.

GET /realtime

Unique visitors active in the last 5 minutes plus the most recent events. Query param: limit (default 50, max 200).

curl "https://www.abner.app/api/v1/realtime?limit=20" \
  -H "Authorization: Bearer $ABNER_API_KEY"

{
  "site_id": "9f3c…",
  "active_visitors": 7,
  "recent_events": [
    { "event_type": "pageview", "url": "https://example.com/x", "pathname": "/x", "country": "US", "browser": "Chrome", "timestamp": "2026-05-25T11:34:00Z" }
  ]
}

GET /vitals

p75 Core Web Vitals over the period. Query param: period (default 30d).

curl "https://www.abner.app/api/v1/vitals?period=30d" \
  -H "Authorization: Bearer $ABNER_API_KEY"

{
  "site_id": "9f3c…",
  "period": "30d",
  "vitals": { "lcp_p75": 1.8, "fid_p75": 12, "cls_p75": 0.04, "fcp_p75": 1.1, "ttfb_p75": 0.6, "inp_p75": 180 }
}

GET /funnel

A stateless, ordered per-session conversion funnel over inline steps — nothing is stored server-side; pass the steps you want each time. This is the direct mirror of the MCP query_funnel tool. Query params: steps (required, repeatable, 2-8 ordered step specs), period (default 7d), window_seconds (default 86400, max 604800).

Each step is path:<pathname> (e.g. path:/pricing) or event:<name> (e.g. event:signed_up). Steps must occur in order within one session, within window_seconds of the first step.

curl "https://www.abner.app/api/v1/funnel?steps=path:/pricing&steps=event:signed_up&period=30d" \
  -H "Authorization: Bearer $ABNER_API_KEY"

{
  "site_id": "9f3c…",
  "period": "30d",
  "steps": [
    { "step": 1, "label": "path:/pricing", "sessions": 820, "conversion_rate": 100, "dropoff_rate": 0 },
    { "step": 2, "label": "event:signed_up", "sessions": 164, "conversion_rate": 20, "dropoff_rate": 80 }
  ]
}

Management API

Authenticated with a management key (abner_ws_…), these endpoints manage everything in the workspace the key belongs to. Reads return 200, creates 201, deletes 204. PATCH updates only the fields you send.

Sites

Method & path Description
GET /api/v1/sitesList all sites in the workspace
POST /api/v1/sitesCreate a site — body { "name", "domain", "timezone"? }
GET /api/v1/sites/{site_id}Fetch one site
PATCH /api/v1/sites/{site_id}Update — body any of { "name", "domain", "timezone" }
DELETE /api/v1/sites/{site_id}Delete a site and its related Postgres plus ClickHouse data
curl -X POST https://www.abner.app/api/v1/sites \
  -H "Authorization: Bearer $ABNER_WS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Marketing Site","domain":"example.com"}'

{ "site_id": "…", "name": "Marketing Site", "domain": "example.com", "public_id": "…", "timezone": "UTC", "workspace_id": "…", "created_at": "…" }

Site read keys

Mint and revoke the per-site read keys used by the read API. The raw key is returned once on creation.

GET /api/v1/sites/{site_id}/keysList a site's read keys (metadata only)
POST /api/v1/sites/{site_id}/keysCreate — body { "label"? }; response includes key once
DELETE /api/v1/sites/{site_id}/keys/{key_id}Revoke a read key

Workspace

GET /api/v1/workspaceFetch the current workspace
PATCH /api/v1/workspaceUpdate — body { "name" }

Team members

The invitee must already have an Abner account; they're added to the workspace by email.

GET /api/v1/workspace/membersList members (membership_id, user_id, email, name, role)
POST /api/v1/workspace/membersAdd — body { "email", "role"? } (role: owner, admin, member)
DELETE /api/v1/workspace/members/{membership_id}Remove a member

Account profile and billing are managed in the web UI and the Stripe billing portal, not the API.

Sending events

To record events server-side, use the public ingest endpoint, which is authenticated by your site's public ID (not an API key). See Installation for details.

POST /api/ingest/
Content-Type: application/json

{ "site_id": "your_public_id", "url": "https://example.com/page", "referrer": "https://google.com" }

Errors

Errors return a JSON envelope with a stable machine-readable code and a human-readable message:

{
  "error": { "code": "invalid_metric", "message": "Unknown metric 'foo'. See GET /api/v1/meta for the supported list." }
}

Status codes:

  • 200 / 201 / 204 — Success (read / created / deleted)
  • 400 — Invalid parameter or body (invalid_period, invalid_metric, invalid_dimension, invalid_filter, invalid_interval, missing_metrics, missing_steps, invalid_steps, invalid_body, invalid_role)
  • 401 — Missing or wrong-type API key (unauthorized)
  • 404 — Resource not found in this workspace (not_found, user_not_found)
  • 409 — Conflict, e.g. user already a member (already_member)
  • 503 — Analytics backend temporarily unavailable (analytics_unavailable)