Capture API
The ingest endpoints the SDKs use, for languages without an SDK.
Ingest lives on your region's host, https://eu.i.analytics.atrix.dev for EU projects, and is authenticated
by the project's public key in the body. Prefer an SDK where one exists: they handle batching, retries and
deduplication for you.
POST /e
{
"api_key": "atx_pk_eu_production_XXXXXXXXXXXXXXXXXXXXXX",
"sent_at": "2026-09-25T10:00:05Z",
"batch": [
{
"uuid": "01927f3e-8a7b-7c3d-9e0f-1a2b3c4d5e6f",
"event": "order_completed",
"timestamp": "2026-09-25T10:00:00Z",
"distinct_id": "user_42",
"properties": { "amount": 1999, "currency": "AED" },
"lib": { "name": "import", "version": "1.0.0" }
}
]
}/batch is an alias of /e. Both also accept PostHog payloads, and ingest serves PostHog's /capture,
/i/v0/e, /decide and Segment's /v1/* too: see Migrating.
Event fields
| Field | Required | |
|---|---|---|
uuid | yes | A UUIDv7 you generate. The deduplication key end to end: send the same uuid on retry. |
event | yes | The event name. Names starting with $ are reserved; unknown ones are rejected. |
timestamp | yes | When it happened (RFC 3339). Corrected for clock skew using sent_at; future times are clamped. |
distinct_id | yes | Your user id, or an anonymous device id. 1–200 characters. Never an email or phone number. |
properties | yes | An object. Money as integer minor units plus currency. |
lib | yes | { name, version }. Hand-written integrations use "name": "import". |
anon_id | The device's anonymous id, on every event before and after identify. | |
session_id, window_id | UUIDv7 session and tab ids. | |
$set, $set_once | Person properties to set. | |
groups | Group type → key, at most 5 types. | |
consent | granted, pending or cookieless. |
Limits
- 1 to 1,000 events per request; 20 MB after decompression; 900 KiB per event.
- 5,000 events per second per project sustained, bursting to 50,000. Beyond that:
429withRetry-After. - Events older than the project's retention are rejected.
Compression
Content-Encoding: gzip or zstd, or ?compression=gzip-js|gzip|zstd|base64 for clients that cannot set
headers (the browser SDK sends text/plain with ?compression=gzip-js to avoid a CORS preflight). Gzip and
zstd bodies are also detected from their magic bytes. sendBeacon form bodies (data=…) are accepted.
Response
{ "accepted": 1, "rejected": [{ "uuid": "…", "reason": "clock_skew" }], "quota_limited": ["events"] }rejected[].reasonis one ofinvalid,too_large,reserved_name,clock_skew,duplicate.quota_limitedlists meters over their allowance. Events past the spend cap are sampled with weights, not cut off; kept events carry$sample_rateso counts stay statistically correct.- Errors are
{ "error": "…" }:400for a malformed body,401for a missing, unknown or revoked key (or a key from another region),413for a batch that is too large,503withRetry-Afterwhen ingest is briefly unavailable.
What ingest does to each event
- Looks up the location from the IP address (country, region, city, time zone), then drops the address.
$ipis never stored. Set$geoip_disable: trueto skip the lookup. - Drops self-declared crawlers (not stored, not billed) and marks headless or datacenter browser traffic as suspect.
- Drops duplicate uuids within a batch; storage deduplicates across batches.
CORS
Every response carries CORS headers without credentials. With no origin allowlist on the key, any origin is
accepted; with one, only listed origins are (others get 403).
POST /r
Session-replay chunks: { api_key, sent_at?, chunks: [...] } with 1–50 chunks, each carrying session_id,
seq, distinct_id, platform (web, ios, android, react-native, tauri), mode and 1–20,000
rrweb-compatible events. The answer lists sampled_out sessions and any rejected chunks. The SDKs and native
recorders speak this for you.
POST /flags and GET /flags/definitions
Remote flag evaluation with the public key, and flag definitions for local evaluation with a flags secret key. See Feature flags.
POST /wh/{provider}/{token}
Revenue webhooks from Stripe, RevenueCat, the App Store and Google Play. See Revenue.