# Stats API

Every figure on a site’s dashboard can also be read as JSON, from an address under /api/ and the site’s domain.

Use it to put your figures where you already look, such as a team dashboard, a spreadsheet or a report your own script sends. It reads what the dashboard shows by the same code, so the two agree, and takes the same ranges and filters, so a view set up on the dashboard is a short step from a request. A script needs only a token made in Settings.

## The addresses

Each site’s API is under `https://fiveb.ar/api/` and its domain, written as in its dashboard’s address:

| Address | Returns |
| --- | --- |
| `/api/example.com/stats` | Everything the dashboard shows over a range, beside the period before |
| `/api/example.com/sources` | One pane in full, by its name, a page of rows at a time |
| `/api/example.com/realtime` | How many are on the site now |

Each answers a GET, and nothing else. They take the same ranges and filters as the dashboard, so an address copied from it asks for the same view.

## Who it answers

The API answers:

- **An API token**, for a script: all your sites, or the ones you made it for, while they are on your Sites page.
- **Your browser**, while you’re signed in to fivebar: the sites on your Sites page.
- **Anyone at all**, for a site whose stats are public: see [Public stats](https://fiveb.ar/docs/public-stats.md).

Any other site is not found. The API sends no CORS headers, so a page on another site can’t read it from its visitors’ browsers.

To reach your stats from an app such as Claude or an editor, connect it over MCP instead: see [Connecting apps](https://fiveb.ar/docs/mcp.md). The API doesn’t take an app’s MCP token.

### Making a token

In [Settings](https://fiveb.ar/settings), under API tokens, make one. It reads all your sites, including ones you add later, unless you choose only some of them: a box to find a site helps when you have many. It takes a name of up to 50 characters if you like, such as what will use it, to tell your tokens apart. The token is shown once, as you make it: copy it then, and keep it as you would a password. You can have up to 20.

### Sending a token

Send it in the `Authorization` header of every request, over https:

```
Authorization: Bearer fivebar_…
```

It’s never read from the address, so it stays out of logs and browser histories. When the header is there, it alone decides: a browser’s sign-in beside it isn’t read. A request over plain http is sent on to https, but a token sent with it has already crossed the network readable, so if that ever happens, revoke it and make another.

### What a token reads

A token reads all your sites, or the ones you chose for it, while they are on your Sites page, and any site whose stats are public, as anyone can. A site you delete is no longer read. Your tokens together can make 60 requests a minute.

### Revoking a token

A token lasts until you revoke it in Settings, and anything using it stops at once. Settings shows when each was made and last used, so one nothing uses any more is easy to spot. Deleting your account revokes every token you made.

## Parameters

Each address but realtime takes the dashboard’s own parameters, read by the same code:

- **A date range**, such as `range=last-month` or `from=2026-09-01&to=2026-09-14`, in the site’s own days. The last 7 days if left out. See [Date ranges](https://fiveb.ar/docs/date-ranges.md).
- `f`, a filter: a dimension, a colon and a value, such as `f=source:Google`. Repeat it for up to 6, and every one must hold. See [Filters](https://fiveb.ar/docs/filters.md).
- `dir`, the folder the content drilldown opens, such as `dir=/blog/`.

### Filters

A filter’s dimension is one of `path`, `source`, `referrer`, `country`, `region`, `city`, `device`, `browser`, `os`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `asn`, `entry`, `exit`, `goal` and `channel`, or `prop:` and a custom property’s name, as in `f=prop:plan:pro`. A value is matched exactly: a country is its two-letter code, such as `f=country:GB`, a region its name, a network its AS number alone, such as `f=asn:2856`, and a channel its name as the Channels tab spells it, such as `f=channel:AI+Assistants`. The simplest way to get one right is to copy it, from the dashboard’s address once you have clicked it, or from the `filter` of a pane’s row.

### Parameters for a pane

A pane’s address also takes:

- `q`, a search: only the rows whose label holds it, ignoring case, up to 100 characters. A country is found by its name or its code, a network by its name or its number, and a search term by its page too.
- `limit`, how many rows, 1 to 500, 100 if left out.
- `offset`, how many rows to skip, up to 10,000, for the pages after the first.
- `trend=0`, to leave out each row’s counts across the range.
- `status`, for `errors` alone, a status from 400 to 599, such as `status=404`: only the errors answered with it, as the Errors page shows them filtered by it. The answer says which as `status`, or null for every one.

### What it cannot read

A parameter the API can’t read is never an error. A range it does not know is the last 7 days, a filter by a dimension it does not know, or past the sixth, is left out, and a number too big or too small is the nearest it takes.

## What each returns

Every answer is JSON, error or not, and is sent with `Cache-Control: no-store`, so nothing on the way keeps a copy.

### Stats

`/api/example.com/stats?range=last-week` returns what the dashboard shows over last week. Shortened, it looks like this:

```
{
  "totals": { "pageviews": 1250, "visits": 610, "visitors": 540 },
  "bucket": "day",
  "series": [
    { "day": "2026-09-14", "n": 190, "visits": 92, "visitors": 81 }
  ],
  "paths": [{ "label": "/", "n": 420 }],
  "sources": [{ "label": "Google", "n": 540 }],
  "counts": { "path": 57, "source": 12 },
  "visit": { "visits": 604, "bounces": 263, "duration": 41860 },
  "previous": {
    "from": "2026-09-07", "to": "2026-09-13", "hour": null, "minute": null,
    "totals": { "pageviews": 1102, "visits": 575, "visitors": 509 },
    "visit": { "visits": 571, "bounces": 260, "duration": 37115 }
  },
  "range": { "key": "last-week", "from": "2026-09-14", "to": "2026-09-20", "timezone": "Europe/London" },
  "source": "postgres"
}
```

- `totals` are the pageviews, visits and visitors over the range. Visitors are visitor-days: see [Metrics](https://fiveb.ar/docs/metrics.md#visitors).
- `series` is the chart. `bucket` says what each column is: `hour`, `day`, `week` or `month`. By the hour, each row is an hour with its pageviews as `n`; otherwise each is a day, with its pageviews as `n`, its visits and its visitors.
- `paths`, `channels`, `sources`, `referrers`, `countries`, `regions`, `cities`, `networks`, `devices`, `browsers`, `oses` and the four `utm_` lists are each pane’s top 8, with their pageviews as `n`. A network’s `label` is its AS number, and its `name` the one the dashboard shows, or null where it has none. A channel is worked out from each visit’s source and tags: see [Sources and campaigns](https://fiveb.ar/docs/sources-and-campaigns.md#channels). `counts` says how many values each has in all, and a pane’s own address returns the rest.
- `visit` is the visits that have ended, how many of them bounced and their seconds on screen: bounce rate is `bounces` over `visits`, and visit duration `duration` over `visits`. `entries` and `exits` are the Entry and Exit pages.
- `content`, `properties`, `goals`, `outbound`, `downloads`, `searches` and `engagement` are the content drilldown, the custom properties, the Goals pane, and the Links, Site search and Engagement sections, each search term with its `page`, the page it was searched from, and `none`, its searches there that found nothing. `searchPages` is how many pages the searches came from, and `searchTerms` how many terms they are, each once however many pages it came from. `week` is the Hours section: `n` has a list for each day of the week, Monday first, of the pageviews in its 24 hours from midnight, and `days` how many of each hour the range held from `from`, the first day it counted by the hour, so an hour’s `n` over its `days` is its average. `speed` has the six measures, the status mix and the errors that the Speed and Errors pages show, with `errorTotal` the errors in all.
- `previous` is the period before, which each figure’s change is from: its dates, its `totals` and its `visit`. For a range that runs to today, its last day is cut at the same time of day, and `hour` and `minute` say when; they are null where it counts whole. It is null for all time. See [Metrics](https://fiveb.ar/docs/metrics.md#the-change-from-the-period-before).
- `range` is the dates read, and the time zone they are the site’s days in.

Under a filter, what the dashboard leaves out is null or not there. Filtered by a goal, the answer is the goal’s: its `goal`, `totals` of its conversions, its events and the visits its conversion rate is of, and a `series` of conversions by day. See [Filters](https://fiveb.ar/docs/filters.md).

### Panes in full

A pane’s address returns every row of it, a page at a time, as its full view lists them. The panes are named as in their full views’ addresses: `content-drilldown`, `top-pages`, `entry-pages`, `exit-pages`, `channels`, `sources`, `referrers`, `countries`, `regions`, `cities`, `networks`, `medium`, `campaign`, `content`, `term`, `goals`, `devices`, `browsers`, `operating-systems`, `outbound-clicks`, `file-downloads`, `searches`, `errors`, `time-on-page`, `scroll-depth`, `slowest-pages-lcp` and `slowest-pages-inp`. A custom property’s is `property-` and its name, such as `property-plan`, and the data an event goal was sent with is `data-` and the data’s name, filtered by that goal.

`/api/example.com/sources?range=last-week&limit=2&trend=0` returns:

```
{
  "site": "example.com",
  "facet": "sources",
  "title": "Sources",
  "dimension": "source",
  "unit": "pageviews",
  "range": { "key": "last-week", "from": "2026-09-14", "to": "2026-09-20", "days": 7 },
  "filters": [],
  "available": true,
  "source": "postgres",
  "q": "",
  "limit": 2,
  "offset": 0,
  "total": 1250,
  "count": 12,
  "more": 10,
  "rows": [
    { "value": "Google", "label": "Google", "n": 540, "share": 0.432, "filter": "source:Google" },
    { "value": "Direct", "label": "Direct", "n": 310, "share": 0.248, "filter": "source:Direct" }
  ],
  "ms": 38
}
```

- `unit` is what each row’s `n` counts: pageviews for most panes, visits for entry and exit pages, conversions for goals and under a goal filter, and clicks, downloads, searches, page loads, readings or events for the rest.
- `total` is what every row counts to, before any search, and a row’s `share` its part of that. `count` is how many rows there are, or with `q` how many match, and `more` how many come after this page.
- A row’s `value` is as stored and its `label` as the dashboard shows it. Its `filter` is the `f` its link on the dashboard adds, or null where the pane’s rows aren’t links. Some panes add more, such as a country’s `code` and `name`, a network’s `asn` and `name`, an entry page’s `bounces` and `duration`, or a search term’s `page`, `none` and `noneShare`, the share of its searches that found nothing.
- A search term is a row for each page it was searched from, as the dashboard lists them: its `value` and `label` are the term, and its `page` the page as stored, as `top-pages` gives it, so `f=path:` and the page lists that page’s terms alone. `pages` is how many pages the searches came from, and `terms` how many terms the rows are, or with `q` those that match, each once however many pages it came from.
- With trends, `trend` lists the range’s columns, a day, a week from Monday or a month each, and each row has its counts in them as `trend`.
- `available` is false, with no rows, where the dashboard says the pane isn’t available under the filters.

### On the site now

`/api/example.com/realtime` returns how many are [on the site now](https://fiveb.ar/docs/metrics.md#on-the-site-now), as the top of its dashboard shows them:

```
{ "current": 3 }
```

## Examples

Each example reads your token from the `FIVEBAR_TOKEN` environment variable, so it’s never written into the code.

### curl

```
curl -H "Authorization: Bearer $FIVEBAR_TOKEN" "https://fiveb.ar/api/example.com/stats?range=last-month"
```

A pane, such as the top 20 sources:

```
curl -H "Authorization: Bearer $FIVEBAR_TOKEN" "https://fiveb.ar/api/example.com/sources?range=last-month&limit=20"
```

### JavaScript

For Node 18 or later. Save it as `stats.mjs`, since it uses `await` outside a function, and run `node stats.mjs`:

```
const res = await fetch('https://fiveb.ar/api/example.com/stats?range=last-month', {
  headers: { Authorization: 'Bearer ' + process.env.FIVEBAR_TOKEN },
});
const data = await res.json();
if (!res.ok) throw new Error(res.status + ' ' + data.error);
console.log(data.range.from, data.range.to, data.totals);
```

### Python

With the standard library alone:

```
import json, os, urllib.error, urllib.request

req = urllib.request.Request(
    'https://fiveb.ar/api/example.com/stats?range=last-month',
    headers={'Authorization': 'Bearer ' + os.environ['FIVEBAR_TOKEN']},
)
try:
    with urllib.request.urlopen(req) as res:
        data = json.load(res)
except urllib.error.HTTPError as e:
    raise SystemExit(str(e.code) + ' ' + json.load(e)['error'])
print(data['range']['from'], data['range']['to'], data['totals'])
```

### PHP

```
<?php
$context = stream_context_create(['http' => [
    'header' => ['Authorization: Bearer ' . getenv('FIVEBAR_TOKEN')],
    'ignore_errors' => true,
]]);
$body = file_get_contents('https://fiveb.ar/api/example.com/stats?range=last-month', false, $context);
$data = json_decode($body, true);
if (isset($data['error'])) {
    fwrite(STDERR, $data['error'] . PHP_EOL);
    exit(1);
}
print_r($data['totals']);
```

`ignore_errors` lets PHP read an error’s answer rather than stopping at its status.

### Paging through a pane

A pane gives up to 500 rows at a time. Ask for the next page with `offset` until `more` is 0. `offset` goes no further than 10,000, so the loop stops there too: to read past row 10,500, narrow the pane with a filter or `q`. In JavaScript, saved as `pages.mjs`:

```
const url = 'https://fiveb.ar/api/example.com/top-pages?range=last-month&trend=0&limit=500';
const headers = { Authorization: 'Bearer ' + process.env.FIVEBAR_TOKEN };
const rows = [];
for (let offset = 0; offset <= 10000; offset += 500) {
  const res = await fetch(url + '&offset=' + offset, { headers });
  const page = await res.json();
  if (!res.ok) throw new Error(res.status + ' ' + page.error);
  rows.push(...page.rows);
  if (page.more === 0) break;
}
console.log(rows.length + ' rows');
```

## Errors

An error has a status to match:

| Status | Answer | When |
| --- | --- | --- |
| 401 | `{ "error": "invalid token" }` | The `Authorization` header holds no token of yours: it isn’t one, or the token is unknown or revoked. Its `WWW-Authenticate` header says so too. |
| 403 | `{ "error": "https required" }` | The address starts http://. The token went unencrypted, so revoke it in Settings, make another, and use https://. |
| 404 | `{ "error": "unknown site" }` | The site doesn’t exist, is someone else’s, is private and you aren’t signed in, or isn’t one your token reads. All are answered alike. |
| 404 | `{ "error": "unknown facet" }` | The address names no pane. The answer also lists every pane’s name, as `facets`. |
| 405 | `{ "error": "method not allowed" }` | A request other than a GET. |
| 429 | `{ "error": "too many requests" }` | Your tokens have made 60 requests in a minute. Wait a minute: `Retry-After` says how long. |
| 502 | `{ "error": "stats could not be read" }` | The stats could not be read just now. Try again in a moment. |
