HTTP ingest API
The tracker's ingest endpoint, its payload contract, and a plain statement of what it is not: a supported server-side tracking API.
There is one ingest endpoint, and it is built to receive a genuine browser beacon. It is documented here so you can debug what your own pages are sending, not so you can send to it yourself.
The endpoint
POST /api/collect (aliased as /api/data on a managed proxy subdomain). The body is JSON. The tracker sends it as Content-Type: text/plain, which makes the request a CORS simple request and skips the preflight round trip; the server parses the body as JSON whatever the declared type. CORS is open (the endpoint replies with Access-Control-Allow-Origin: *) because the whole point is accepting cross-origin beacons from whatever domain a customer's site runs on.
The envelope
One request carries a batch. The identity and browser-context fields sit once on the envelope, and each event in events carries its own name, props, and path:
| Field | Type | Required | Notes |
|---|---|---|---|
site | string | Yes | Site id. Missing or empty returns 400. |
events | array | No | Up to 25 entries per request; anything beyond that is discarded. Each entry takes event, props, path, url, t. |
event | string | No | Single-event shorthand, used when events is absent or empty. Defaults to "pageview". |
props | object | No | Sanitized with the same 8-key / 256-char rules track() uses client-side. |
session | string | No | Without it, the event still passes the bot gate and updates rollup counters, but skips the sessions upsert and the events-table mirror entirely. |
client_id | string | No | Cookie-mode persistent visitor id. Ignored on a cookieless site. |
path, url, referrer, utm | string / object | No | Used for page/source classification. A per-entry path wins over the envelope's. |
tz, lang, screen, ua | string | No | Browser context; tz also feeds the geo/timezone bot check. |
wd, pr, nl, vp, nb | boolean | No | Client-side bot signals (webdriver, prerendering, empty languages, zero viewport, empty UA-data brands). Omitting them just means those specific checks can't fire. |
What this is not
Do not try to work around the gate. There is no authentication model, no stability commitment on the payload contract, and no rate-limit tier designed for backend traffic, so anything built that way will break without notice and its numbers will not be trustworthy in the meantime. If your product needs backend-originated events, tell us and we will treat it as the feature request it is.
Response shape
| Situation | HTTP status | Body |
|---|---|---|
| Accepted | 200 | { ok: true, accepted: n, replay: { enabled, sample_rate } } |
| Classified as a bot | 200 | { ok: true, bot: true, reason } |
| Every event in the batch was on an excluded path | 200 | { ok: true, excluded: true } |
| Every event in the batch was a retired autocapture name | 200 | { ok: true, dropped: "autocapture" } |
| Hard pageview quota exceeded (grandfathered free plan only) | 200 | { ok: true, quota: "exceeded" } |
Malformed JSON or missing site | 400 | { ok: false, error } |
Unknown site | 404 | { ok: false, error: "unknown site" } |
For the client-side API this endpoint exists to serve, see the JavaScript API reference. For what triggers a bot classification, see bots and spam.