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
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.
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.
AUTHENTICATE
Send the token as a Bearer header, and ask for JSON:
Authorization: Bearer <your-token>
Accept: application/json
ENDPOINTS
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:
Sending both dimension and granularity in one query is rejected with 422 — pick one per request.
DIMENSIONS
Set dimension to break a query down into one row per value:
METRICS
Each query type supports a specific set of metrics; request a subset with metrics, or omit it to get all of them.
avg_duration_ms is the average visit duration in milliseconds; bounce_rate is a percentage.
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"}]
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/helpanddocs.example.com/helpare 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 apagebreakdown 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.
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.