# API reference

Base URL: `https://analyticsapi.specvista.com/api/v1`. Bodies are JSON. The machine-readable contract is the [OpenAPI document](https://analyticsapi.specvista.com/api/v1/openapi.json), which is checked against the API's routes in its test suite.

## Authentication

| Endpoints | Header |
|---|---|
| `/ingest`, `/ingest/identify`, `/ingest/batch`, `/sdk/linked-domains` | `X-Api-Key: <project key>`. Browsers may send `X-Domain: <hostname>` instead, for a web property with that domain |
| `/forget` | `X-Api-Key` with a backend property's key |
| `/auth/me`, `/auth/password`, `/auth/profile`, `/projects`, `/analytics/...` | `Authorization: Bearer <token>`, where the token is `user.token` from `/auth/login` or `/auth/register` |
| `/auth/login`, `/auth/register`, `/sdk.js`, `/openapi.json`, `/ingest/ignore-*` | None |

## Send events

### POST /ingest

Stores one event, usually within a second. Returns `202 {"ok":true}` straight away; processing happens afterwards, so an event that fails to store is dropped silently rather than rejected, and there is no way to see dropped events yet.

| Field | Type | Notes |
|---|---|---|
| `event` | string | For example `signup.completed`. Defaults to `pageview`. The API stores up to 100 characters and does not check the format; the Node.js SDK only sends names matching `^[a-z][a-z0-9._:-]{1,79}$` |
| `source` | `web`, `mobile` or `backend` | Defaults to `web`. Servers must send `backend` |
| `userId` | string | Your user id, up to 255 characters |
| `sessionId` | string | Up to 100 characters |
| `properties` | object | Your own structured fields. No secrets or free text |
| `environment` | `development`, `preview` or `production` | Anything except `production` is stored as internal and left out of reports |
| `consent` | `full` or `essential` | `essential` stores only the event, path, referrer, device type and country |
| `url`, `path`, `referrer` | string | Web context. UTM parameters are read from `url` |
| `os`, `osVersion`, `appVersion`, `appBuild`, `deviceModel` | string | App context |
| `deviceType`, `browser`, `language`, `locale`, `timezone`, `screenWidth`, `screenHeight` | | Device context |
| `utmSource`, `utmMedium`, `utmCampaign`, `utmContent`, `utmTerm` | string | Override values read from `url` |
| `anonymousId`, `installId`, `deviceId` | string | Visitor and install identifiers |

Country, region and city come from the request IP address. Events whose user agent looks like a bot or script (crawlers, headless browsers, Playwright, Puppeteer, Selenium, curl, wget, python-requests, Go's HTTP client, uptime monitors) are stored as bot traffic unless `source` is `backend`. The default user agents of mobile apps (`okhttp` on Android, `CFNetwork` on iOS) are not. Events from Apple's network (17.0.0.0/8, which App Review uses) are stored as internal. Bot and internal events are left out of every report.

There is no idempotency key yet: a retried request stores a second event.

### POST /ingest/batch

A JSON array of events, or `{"events":[...]}`. At most 100 are stored per request. Returns `202 {"ok":true,"queued":<number received>}`.

### POST /ingest/identify

`{"userId":"user_123","email":"person@example.com","name":"Ada","traits":{"plan":"pro"}}`. Attaches an identity to a user id so people show by name. Blank fields never overwrite stored ones. Add `"emailStorage":"hash_only"` to store only a salted hash of the email. Returns `202 {"ok":true}`, or `202 {"ok":false,"error":"userId required"}`.

### POST /forget

`{"userId":"user_123"}` with a backend key, and optionally `"projectId"` for another property owned by the same account. Deletes the stored identity and strips the user id and personal properties from that user's events. Returns 204. Permanent. Limited per key per minute. Sending the event `account.deleted` with a `userId` from a backend key does the same.

### GET /ingest/ignore-on, /ingest/ignore-off, /ingest/ignore-status

Set, clear or read the `sa_ignore` cookie in the calling browser. While it is set, ingest returns `{"ok":true,"skipped":true}` and stores nothing.

## Account

| Method and path | Body | Result |
|---|---|---|
| `POST /auth/register` | `email`, `password` (8 or more characters), `firstName`, `lastName` | 201 `{"user":{...,"token":"..."}}` |
| `POST /auth/login` | `email`, `password` | 201 `{"user":{"id","uuid","email","firstName","lastName","token"}}` |
| `GET /auth/me` | | The account |
| `PATCH /auth/password` | `currentPassword`, `newPassword` | `{"ok":true}` |
| `PATCH /auth/profile` | `firstName`, `lastName` | The account with a fresh token |

Tokens last 90 days and cannot be revoked early; changing the password does not end existing tokens.

## Properties

| Method and path | Body | Result |
|---|---|---|
| `GET /projects` | | Your properties, newest first, each with `id`, `name`, `slug`, `apiKey`, `domain`, `type`, `createdAt` |
| `POST /projects` | `name` (required), `domain`, `type` (`web`, `mobile`, `backend`, `other`) | 201 and the property |
| `GET /projects/{id}` | | The property |
| `PATCH /projects/{id}` | `name`, `domain` | The property |
| `DELETE /projects/{id}` | | `{"ok":true}` |
| `POST /projects/{id}/rotate-key` | | 201 and the property with a new `apiKey`. The old key stops at once |

## Reports

All `GET`, with a bearer token, `period` of `24h`, `7d` or `30d` (default `30d`). Bot and internal traffic is excluded.

| Path | Returns |
|---|---|
| `/analytics/global` | Per property: `live`, `pageviewsToday`, `pageviewsPeriod`; plus totals |
| `/analytics/{id}/overview` | `total`, `pageviews`, `uniqueSessions`, `liveVisitors`, `trend`, `previous` |
| `/analytics/{id}/pageviews` | `[{date, views}]`; `granularity` is `day`, `week` or `month` |
| `/analytics/{id}/top-pages` | Top 10 `[{path, views, sessions}]` |
| `/analytics/{id}/landing-pages` | `{sessions, pages:[{path, sessions, bounced, bounceRate}]}` |
| `/analytics/{id}/top-events` | Top 10 non-pageview `[{event, source, count}]` |
| `/analytics/{id}/event-names` | Up to 50 `[{event, count}]` |
| `/analytics/{id}/funnel?from=&to=` | `{fromEvent, toEvent, period, current, previous}`, each with `started`, `converted`, `rate` |
| `/analytics/{id}/referrers` | Top 10 `[{referrer, visits}]` |
| `/analytics/{id}/channels` | `[{channel, count}]`: Organic, Direct, Referral, Social, Email, Paid |
| `/analytics/{id}/utm-campaigns` | Top 20 `[{campaign, source, medium, visits}]` |
| `/analytics/{id}/sources`, `/devices`, `/browsers`, `/os` | `[{<field>, count}]` |
| `/analytics/{id}/countries` | Top 50 `[{countryCode, country, visits}]` |
| `/analytics/{id}/cities` | Top 20 `[{city, region, countryCode, country, visits}]` |
| `/analytics/{id}/bounce-rate` | `{bounceRate}` as a whole percentage |
| `/analytics/{id}/new-vs-returning` | `{new, returning}` among identified users |
| `/analytics/{id}/people` | Up to 500 user ids active in the period, with email and name where identified |
| `/analytics/{id}/live-events` | Up to 20 events from the last hour |
| `/analytics/{id}/export` | CSV of up to 50,000 events |
| `/analytics/tracking-health` | Whether each property is still receiving events |

## Other

- `GET /sdk.js` and `GET /sdk.min.js`: the browser script.
- `GET /sdk/linked-domains`: domains of the same account, used by the script to link visitors across your sites.
- `GET /openapi.json`: the OpenAPI 3.0 document.

## Errors

Every error code, and what to do about it, is in [Errors](https://analytics.specvista.com/docs/errors.md).
