# Server events

A service with no pages, such as an API or an MCP server, has nowhere to put the tag. Its own server can send fivebar an event for each call instead.

It takes an API token made to send events, and a request to fivebar for each call, sent once your service has answered, so it never waits on fivebar. Each call counts as a pageview of its route or tool, or as a custom event, on the site’s dashboard, beside any pages its tag counts. A call is never taken for a bot’s, as your token vouches for it.

## Make a token

In [Settings](https://fiveb.ar/settings), under API tokens, make a token for one site, under **Only the ones you choose**, with **Send events** ticked and **Read stats** unticked. A token that only sends reads nothing, so one that leaks from your service shows nobody your stats. Give it to your service in its environment, as `FIVEBAR_TOKEN`, never in its code.

The site can be your site, its calls counted beside its pages, or a site of its own, such as `api.example.com`, to keep them apart: add it on [Sites](https://fiveb.ar/sites) first.

A token sends for a site only while you own it or are its editor. It stops within a minute of being revoked, or of you leaving the site, being removed or being made a viewer: see [Sharing a site](https://fiveb.ar/docs/sharing.md). A token you’ve just made can send from a minute later.

## Send an event

For each call, send a `POST` to `https://fiveb.ar/api/events`, with the token in the `Authorization` header, as `Bearer` and the token, and the event as a JSON object of up to 64 KiB. A basic event, its caller told apart by address and user agent, as a browser is:

```
{
  "site": "example.com",
  "name": "/v1/postcodes/:code",
  "ip": "203.0.113.7",
  "user_agent": "curl/8.7.1"
}
```

An advanced one: your service’s account 4821, calling through Claude, with two properties of the call:

```
{
  "site": "example.com",
  "name": "/v1/postcodes/:code",
  "props": { "status": 200, "format": "json" },
  "ip": "203.0.113.7",
  "id": "4821",
  "client": "claude"
}
```

| Field | Holds |
| --- | --- |
| `site` | Required. The site’s domain, as the tag’s `data-domain` names it. |
| `name` | Required. The route or tool called, as your code names it, such as `/v1/postcodes/:code` rather than the address called, or `get_overview`. Spaces at its ends are trimmed, anything from a `?` or `#` is cut off, a name without a `/` first is given one, so `get_overview` is `/get_overview`, and it’s cut to 2,048 characters, as a page’s path is. |
| `event` | Optional. A [custom event’s name](https://fiveb.ar/docs/events.md#names), to count the call as that event, on the page `name` gives, rather than as a pageview. It carries no [data](https://fiveb.ar/docs/events.md#data): send `props` instead. |
| `props` | Optional. [Custom properties](https://fiveb.ar/docs/properties.md) for this call, as an object of names and values, such as `{"status": 200}`, read by [the tag’s rules](https://fiveb.ar/docs/properties.md#names-and-values). A value is text or a number, and anything else is dropped, as is a name with capitals in it. Never put anything about the caller in one: that’s what `id` is for. |
| `ip` | Optional. The caller’s IP address. |
| `user_agent` | Optional. The caller’s user agent. |
| `id` | Optional. Your service’s own word for the caller, as text, such as an account’s number or an MCP session’s id. |
| `client` | Optional. The app that called, as text, such as MCP’s `clientInfo.name`. |

Any other field is ignored, and a call is counted at the time fivebar has it. An event needs `ip` or `id`, as [Who called](#who-called) says. fivebar answers **202**, with no body, once it has the event, or with one of the [Errors](#errors). No answer can be read by a page on another site, so send events from your server alone.

## Who called

Send `ip`, `id` or both: an event with neither is refused.

- `ip` gives the caller’s country, city and network, as a visitor’s address does, and leaves the call out where the site [excludes](https://fiveb.ar/docs/excluding-traffic.md) that address, network or country. An app such as Claude calls an MCP server from its own servers, so there it places the app’s servers, not the person using it.
- `id` tells your callers apart: an account’s number counts each account as a visitor, and an MCP session’s id each session.
- `user_agent`, beside `ip`, tells callers apart where there’s no `id`, as a browser’s does. It names no browser.

Visitors are counted once a day each, as on any site, so over a range they’re visitor-days. Without `id`, a caller is told apart by `ip` and `user_agent`, so everyone calling from one address with one user agent, such as one of an app’s servers, is one visitor. On a site that counts its pages too, someone using both can be two visitors a day, one by their browser and one by your `id`.

fivebar makes the visitor code from `id` as it makes a browser’s from its address and user agent, one way, with the site and the day’s secret, and keeps none of the three: see [Privacy](https://fiveb.ar/privacy/index.md).

## What is counted

A call is a pageview of its `name`, in Top pages and the Content drilldown, and reaches any [page goal](https://fiveb.ar/docs/goals.md#page-goals) it matches. With `event`, it’s that [custom event](https://fiveb.ar/docs/events.md) instead, which, as on a page, makes no visit or visitor of its own, so callers who send only events aren’t counted as visitors.

`client` is the call’s source, read as a link’s `utm_source` is: `claude` or `claude.ai` is Claude, under AI Assistants, as are Claude’s MCP apps, which call themselves `claude-ai` and `claude-code`. A name fivebar doesn’t know, such as `cursor`, is kept as sent, under Referral. Without it, the source is Direct. See [Sources and campaigns](https://fiveb.ar/docs/sources-and-campaigns.md).

A server event is never taken for a bot’s, whatever its user agent or network, as your token vouches for it. Traffic the site excludes is still left out.

A call has no browser, operating system, device, speed, time on page or scroll depth, so those panes leave it out. Calls make visits as pages do: a visit of calls alone lasts 0 seconds, and is a bounce if it’s one call.

Calls count towards your plan’s billable pageviews, as pages’ pageviews and events do: see [Your trial](https://fiveb.ar/docs/account.md#your-trial).

## Examples

See [Server event examples](https://fiveb.ar/docs/server-events/examples.md) for curl, Python, Node and an MCP server.

## Errors

fivebar checks an event in this order, and the first that applies is the answer. An answer with a body is JSON.

| Status | Answer | When |
| --- | --- | --- |
| 405 | `{ "error": "method not allowed" }` | A request other than a POST. `Allow` says POST. |
| 401 | `{ "error": "token required" }` | No `Authorization` header. Its `WWW-Authenticate` header asks for a token. |
| 401 | `{ "error": "invalid token" }` | The header holds no token that can send: it isn’t one, or the token is unknown, revoked or made without **Send events**, was made in the last minute, or is for no site you own or edit. Its `WWW-Authenticate` header says so too. |
| 413 | `{ "error": "body too large" }` | The body is over 64 KiB. |
| 400 | `{ "error": "malformed body" }` | The body isn’t a JSON object. |
| 400 | `{ "error": "missing fields" }` | `site` or `name` isn’t text, or is empty once trimmed and cut. |
| 404 | `{ "error": "unknown site" }` | The site isn’t on fivebar, or the token can’t send for it. Both are answered alike. |
| 400 | `{ "error": "bad event" }` | `event` isn’t a name a custom event can have, such as one starting with `/`: see [Names](https://fiveb.ar/docs/events.md#names). |
| 400 | `{ "error": "bad ip" }` | `ip` isn’t one public IP address, such as a range or a private address. |
| 400 | `{ "error": "missing caller" }` | Neither `ip` nor `id`. |
| 202 | No body | The site excludes the caller’s address, network or country, so the call is left out. |
| 503 | `{ "error": "no salt" }` | The day’s secret for the site’s time zone hasn’t reached fivebar’s server yet. Nothing was counted, so send it again later. |
| 503 | `{ "error": "unavailable" }` | The event couldn’t be kept just now. Nothing was counted, so send it again later. |
| 202 | No body | fivebar has the event. |

## Check it works

Send an event with the [curl example](https://fiveb.ar/docs/server-events/examples.md#curl), with your site’s domain: it prints the status. A **202** means fivebar has it, and within a minute or two the call is in the site’s Top pages, under its name.

---

fivebar is web analytics without cookies, at https://fiveb.ar. Every page of its docs is listed at https://fiveb.ar/docs/index.md.
