# REST API overview

> Base URL, bearer authentication, the JSON envelope, error codes, rate limits, date ranges, the filter DSL and cursor paging.

Section: CLI, MCP and API  
Canonical page: https://statsy.co/docs/developers/api  
Last updated: 2026-09-15

The API is JSON over HTTPS. Every path below is relative to `https://statsy.co/api/v1`. The hosted API also answers on `https://api.statsy.co`, which is what the CLI uses by default.

## Authentication

Send an [API token](https://statsy.co/docs/developers/api-tokens) as a bearer token. There is no other header to set.

```bash
curl -H "Authorization: Bearer sta_your_token" \
  "https://statsy.co/api/v1/sites/ws_yoursitekey/overview?from=30d"
```

In a path, `:site` accepts either the site’s public id or its `ws_` public key. A missing or revoked token gives `401`.

## Response envelope

Every JSON response uses one of two shapes.

```json
{ "status": "success", "data": { } }

{ "status": "error", "error": { "code": "bad_dimension", "message": "..." } }
```

The CSV export is the one exception: it streams `text/csv` with a `Content-Disposition` filename instead.

## Errors

| Status | Typical codes | Meaning |
| --- | --- | --- |
| 400 | `validation_error`, `bad_range`, `bad_dimension`, `bad_filters`, `bad_cursor` | The request was malformed. The message says which part. |
| 401 | `unauthorized`, `invalid_token` | No bearer token, or the token is unknown or revoked. |
| 402 | `trial_expired`, `plan_cancelled`, `plan_limit` | Reads still work; writes are paused until you pick a plan, or a plan limit was reached. |
| 403 | `forbidden`, `session_required` | Your role or token kind is not allowed to do this. |
| 404 | `site_not_found`, `goal_not_found`, `not_found` | No such site, resource or route. |
| 409 | `already_member`, `widget_limit`, `last_owner` | The request conflicts with the current state. |
| 429 | `rate_limited`, `export_busy` | Too many requests. Wait and retry. |
| 500 | `internal_error` | Something went wrong on our side. |

## Rate limits

Token-authenticated requests are limited to **60 per minute**; requests from a signed-in dashboard session get 600. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`, and a `429` adds `Retry-After` in seconds. A few endpoints have their own, tighter limits, noted on their pages.

## Dates, presets and time zones

Analytics endpoints share the same range parameters. `from` takes either a preset or a date; `to` takes a date and defaults to now.

| Preset | Means |
| --- | --- |
| `now` | The last 30 minutes, always from raw events. |
| `24h` | A rolling 24 hours. |
| `7d` | Seven calendar days in the site’s time zone, up to now. |
| `30d` | Thirty calendar days. This is the default when from is omitted. |
| `90d` | Ninety calendar days. |
| `12mo` | The start of the month eleven months ago, up to now. |

For exact windows pass an ISO timestamp or `YYYY-MM-DD`. A bare date is read as UTC midnight. `from` must be before `to`, and a range may not exceed 400 days, or you get `400 bad_range`.

`tz` sets the IANA time zone for day and month boundaries. It defaults to the site’s time zone, and an unrecognised value falls back to UTC. Add `compare=prev` to get the equally long window immediately before yours alongside the current one.

> **Note.** Responses include `source` and `clamped`. Raw events are kept 90 days, so a long range that needs raw events is clamped to the last 90 days and `clamped` becomes `true`. Rolled-up totals go back as far as the site does.

## Filters

`filters` is a URL-encoded JSON array of `{k, op, v}` objects, up to 20, all combined with AND.

```bash
curl -G -H "Authorization: Bearer sta_your_token" \
  --data-urlencode 'filters=[{"k":"country","op":"is","v":"US"},{"k":"path","op":"contains","v":"/blog"}]' \
  "https://statsy.co/api/v1/sites/ws_yoursitekey/breakdown?dimension=channel"
```

Keys: `path`, `entry_path`, `referrer_host`, `channel`, `utm_source`, `utm_medium`, `utm_campaign`, `country`, `region`, `city`, `device`, `browser`, `os`, `lang`, `goal` and `post`. Operators: `is` (the default), `not`, `contains` and `not_contains`; the two `contains` forms ignore case.

`segment=<id>` loads a saved filter set and adds its filters to yours. Filtering on `entry_path`, `region`, `city`, `lang`, `goal` or `post` forces the query onto raw events, so the range is clamped to 90 days.

## Paging

List endpoints take `limit` and `cursor`. A cursor is an opaque string; pass back the one the previous response returned, and stop when it is `null`. The field name differs by endpoint: breakdowns and visitors return `cursor`, replays return `next_cursor`, and payments return `nextCursor`.

Previous: [API tokens and roles](https://statsy.co/docs/developers/api-tokens)  
Next: [Analytics API](https://statsy.co/docs/developers/api-analytics)