Reference6 min read
HTTP ingest 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.
Mrkr has no backend event API, no API key, and no supported way to post events from a server, a script, or curl. Requests that don't look like a real browser are classified as bots and discarded. Read What this is not before you build anything against this endpoint.
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. |
click_link, click_button, submit_form, select_change and toggle_change are the retired autocapture names. They are dropped before any write, with no rollup, no event row, and no session marker consumed, so a tracker still cached in a visitor's browser cannot keep writing click data. Do not reuse these names for your own events.
What this is not
There is no API key, no server-side authentication, and no documented, stable contract for posting events from a backend process. The gate that decides whether a request counts as real traffic assumes a genuine browser: it requires an Origin header that resolves to the site's configured domain, a Sec-Fetch-Site header (which only real browsers attach to fetch/sendBeacon requests), and a User-Agent that starts with Mozilla/5.0 and passes header-consistency and pattern checks. A plain POST from a server, a script, or curl fails these by construction and is classified and discarded as a bot, silently, with a 200 response that looks successful.
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" } |
Every rejection short of a malformed request or an unknown site still returns HTTP 200. The tracker is designed to never retry a rejected event, so a 200 does not mean the event was stored. If you're probing this endpoint directly, you have to read the body, not just the status code.
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.