Docs API Reference
FOR DEVELOPERS

API

Version 1.0

Programmatic, read-only access to the same statistics you see in your dashboard — visitor counts, pageviews, and breakdowns by page, country, browser and more, as totals, top-N lists or time series.

Bearer token auth JSON in, JSON out Read-only

BASE URL

Every endpoint lives under /api/v1. JSON in, JSON out. The API is read-only — it never modifies your sites or data.

https://beta.featherweight.eu/api/v1
API access is a paid feature

It’s available to sites on an upgraded plan — not the free plan. Querying a site that isn’t upgraded returns 402 with {"error":"plan_upgrade_required","feature":"api"}, even with a valid token.

GET /sites includes an api_access flag per site, so you can check up front. A key can read stats for any site in your account that has an upgraded plan.

1

CREATE AN API KEY

In the dashboard, open Settings → API keys, give the key a name and click Create. The token is shown once — copy it straight away; we store only a hash, so it can never be displayed again. You can revoke a key from the same screen at any time.

2

AUTHENTICATE

Send the token as a Bearer header, and ask for JSON:

Authorization: Bearer <your-token>
Accept: application/json

ENDPOINTS

METHODPATHPURPOSE
GET /api/v1/sites List the sites your key can read. Each includes its numeric id, domain, name, timezone, and api_access.
POST /api/v1/stats/query Run a stats query (totals, breakdown, or time series). One endpoint covers all stats reads.

GET /sites returns each site’s numeric id (use it as site_id in queries), domain, name, timezone, and api_access.

curl https://beta.featherweight.eu/api/v1/sites \
  -H "Authorization: Bearer <your-token>" \
  -H "Accept: application/json"

THE STATS QUERY

One flexible JSON body sent to POST /api/v1/stats/query covers all stats reads. The query type is derived from which fields you send:

neither dimension nor granularity → Aggregate — totals for the date range
dimension set → Breakdown — one row per dimension value (e.g. top pages)
granularity set → Time series — one row per hour/day/week bucket

Sending both dimension and granularity in one query is rejected with 422 — pick one per request.

FIELDREQUIREDVALUE
site_id yes The site’s numeric id, from GET /sites.
date_range no A preset — 24h, 7d, 30d, 90d, 12m — or a custom range {"from":"YYYY-MM-DD","to":"YYYY-MM-DD"}. Default 7d.
dimension no What to break down by — see Dimensions below.
granularity no Time-series bucket size: hour, day, or week.
metrics no Which metrics to return — see Metrics. Defaults to all metrics valid for the query type.
filters no Narrow the query — see Filters. An array of single-key objects.
limit no Maximum breakdown rows, 1–1000. Default 100.

DIMENSIONS

Set dimension to break a query down into one row per value:

DIMENSIONROWS ARE
page Pages viewed. Values are the page’s normalised identity — see How pages are identified.
entry_page The first page of a visit.
exit_page The last page of a visit.
source Acquisition channel: Direct, Organic search, Referral, Organic social, Paid social, Paid search, Email, AI Chatbot.
referrer Referring website (host, e.g. google.com).
country Country, as an ISO-2 code (e.g. IE).
region Region, as a CC|Name composite (e.g. IE|Leinster).
city City, as a CC|Region|City composite (e.g. IE|Leinster|Dublin). The region part may be empty (US||Atlanta) when no subdivision is known.
browser Browser name (e.g. Chrome).
os Operating system name.
device Device class: desktop, mobile, tablet, console.
event Your custom events, by event name. Returns visitors and completions instead of the usual breakdown metrics.
event_prop Custom event property values.
utm_campaign, utm_source, utm_medium, utm_content, utm_term UTM tag values, verbatim as they appeared in your URLs.

METRICS

Each query type supports a specific set of metrics; request a subset with metrics, or omit it to get all of them.

QUERY TYPEMETRICS
Aggregate visitors, pageviews, views_per_visitor, avg_duration_ms, bounce_rate
Time series visitors, pageviews, bounces, bounce_rate
Breakdown visitors, pageviews (the event dimension returns visitors, completions)

avg_duration_ms is the average visit duration in milliseconds; bounce_rate is a percentage.

NOTE

How visitors are counted: visitors is the sum of daily unique visitors across the range — the same as the dashboard. Someone who visits on two different days counts twice; there is no cross-day deduplication. This is a deliberate part of Featherweight’s privacy model (visitors can’t be tracked across days).

FILTERS PAID

filters is an array of single-key objects. Multiple filters combine with AND; give a key a list of values to match any of them (an OR within that key):

"filters": [{"country": ["IE", "GB"]}, {"device": "mobile"}]
KEYMATCHES
path A page, by its normalised identity — exactly as the page breakdown returns it (e.g. example.com/pricing). See How pages are identified.
source A channel label, e.g. Organic search (same values as the source dimension).
referrer A referring host, e.g. google.com.
country An ISO-2 country code, e.g. IE.
region, city The composite value the matching breakdown returns: region = CC|Name, city = CC|Region|City. Copy values from a breakdown response rather than constructing them by hand.
browser, os The name as reported, e.g. Chrome.
device Lowercase: desktop, mobile, tablet, console.
utm_source, utm_medium, utm_campaign, utm_content, utm_term The verbatim UTM value.

Filters work on aggregate and time-series queries, and on every breakdown except event_prop (filtering that dimension returns 422). Not yet available through the API: the dashboard’s goal, visit-duration and entry-page segment filters, and exclude (“is not”) filters.

EXAMPLES

Aggregate totals for the last 7 days

curl -X POST https://beta.featherweight.eu/api/v1/stats/query \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"site_id":7,"date_range":"7d","metrics":["visitors","pageviews"]}'
{
  "results": { "visitors": 1280, "pageviews": 3410 },
  "meta": {
    "site": 7,
    "date_range": { "from": "2026-06-12", "to": "2026-06-19", "preset": "7d", "granularity": "day" },
    "dimension": null,
    "metrics": ["visitors","pageviews"]
  }
}

Top pages (breakdown)

curl -X POST https://beta.featherweight.eu/api/v1/stats/query \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"site_id":7,"dimension":"page","limit":10}'
{
  "results": [
    { "page": "example.com/pricing", "visitors": 240, "pageviews": 305 },
    { "page": "example.com/blog", "visitors": 185, "pageviews": 220 }
  ],
  "meta": { "...": "..." }
}

Daily time series, filtered to one country

curl -X POST https://beta.featherweight.eu/api/v1/stats/query \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"site_id":7,"granularity":"day","date_range":"30d","filters":[{"country":"IE"}]}'
{
  "results": [
    { "bucket": "2026-05-20 00:00:00", "visitors": 41, "pageviews": 96, "bounces": 18, "bounce_rate": 43.9 }
  ],
  "meta": { "...": "..." }
}

Custom date range, broken down by channel

curl -X POST https://beta.featherweight.eu/api/v1/stats/query \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"site_id":7,"date_range":{"from":"2026-06-01","to":"2026-06-30"},"dimension":"source"}'

HOW PAGES ARE IDENTIFIED

A page’s identity is its host + path, normalised: lowercased, with the scheme, query string, fragment, www., port and trailing slash removed. So https://www.example.com/Pricing/?utm_source=x is the page example.com/pricing. Two things follow:

  • Subdomains are distinct pages. example.com/help and docs.example.com/help are different pages with separate numbers — merging them would conflate their stats.
  • Path filters must use the full identity. Filter with example.com/pricing, not /pricing — the safest approach is to copy values straight from a page breakdown response.

ERRORS

Errors are JSON. Upgrade-related errors carry an error of plan_upgrade_required and a feature field; validation errors carry details in errors.

CODEMEANING
200 OK.
401 Missing or invalid token ({"message":"Unauthenticated."}).
402 The site’s plan doesn’t include API access ({"error":"plan_upgrade_required","feature":"api"}).
403 Your token doesn’t have stats-read permission.
404 site_id doesn’t exist or isn’t in your account.
422 Invalid query — unknown dimension or metric, dimension and granularity together, a filter on a non-filterable dimension, or a bad date. See errors for specifics.
429 Rate limited — see Rate limits.

RATE LIMITS

Each API key may make 120 requests per minute (there is also a broader per-IP limit of 300 requests per minute). Limits use fixed one-minute windows.

Every response includes X-RateLimit-Limit and X-RateLimit-Remaining headers so you can pace your client. Exceeding a limit returns 429 with a Retry-After header telling you how many seconds to wait — honour it and retry.

GOOD TO KNOW

  • Keys are shown once. If a token is lost, revoke it and create a new one.
  • Keep tokens secret. A key can read the stats of every site in your account — treat it like a password. Don’t embed it in client-side code or public repositories.
  • The API and the dashboard always agree. Both are views over the same data with the same definitions, so numbers match for the same site, range and filters.
BACK TO Documentation home