Ingest API
The Swift SDK and the web tracker speak this API for you. Use it directly to write an SDK for another platform, or to send events from a server.
Base URL: https://e.millimetry.app.
POST /v1/app/batch
Section titled “POST /v1/app/batch”For native apps: a batch of events from one device.
| Header | Required | Notes |
|---|---|---|
Content-Type: application/json |
yes | |
Content-Encoding: gzip |
recommended | Uncompressed bodies are accepted too |
X-App-Key |
yes | The app’s public key, ak_… |
X-Batch-Id |
yes | A UUID you create when you form the batch, reused on every retry of it |
{ "sdk": "kotlin@0.1.0", "system": { "os_name": "Android", "os_version": "16", "device_model": "Pixel 10", "locale": "en-CA", "app_version": "1.5.0", "app_build": "42", "is_debug": false }, "events": [ { "name": "screen_view", "ts": "2026-10-05T17:03:12.345Z", "session_id": "0199b3c2-5f1e-7a10-9c3d-2b8e4f6a1d22", "props": { "screen": "home", "items": 3, "premium": true } } ]}systemdescribes the device once per batch. Start a new batch when any of it changes, for example after an app update.session_idis a UUID (version 7 preferred) you create at the start of a session and keep in memory only. End a session after 30 minutes in the background or without events.- Never send an identifier for the device or the person. The server derives the daily visitor ID itself.
| Response | Meaning | What your SDK does |
|---|---|---|
200 {"accepted": n, "dropped": m} |
Stored, or this batch ID was already stored | Delete the batch |
200 {"accepted": 0, "dropped": n, "reason": "over_quota"} |
The organization’s monthly limit is used up | Delete the batch |
200 {"accepted": 0, "dropped": n, "reason": "paused"} |
The app is paused in the dashboard | Delete the batch |
400 |
Malformed body or invalid system |
Delete the batch; it will never succeed |
401 |
Unknown key | Delete the batch |
413 |
Body too large | Split the batch and retry |
429 with Retry-After |
Rate limited | Keep the batch; retry after the delay |
503, possibly with Retry-After |
Temporarily unavailable, or this batch ID is still being stored | Keep the batch; retry with exponential backoff |
Because the batch ID is reused across retries, a batch is stored once however many times it arrives.
POST /v1/web/event
Section titled “POST /v1/web/event”For websites: one event per request, sent as JSON with Content-Type: text/plain. That keeps it a simple CORS request, with no preflight, and lets navigator.sendBeacon send it.
{ "k": "wk_…", "n": "pageview", "u": "https://example.com/pricing?utm_source=newsletter", "r": "https://www.google.com/", "w": 1440, "p": { "plan": "pro" } }| Field | Meaning |
|---|---|
k |
The site’s key, wk_… |
n |
Event name (pageview for page views) |
u |
The page URL |
r |
document.referrer (optional) |
w |
screen.width (optional) |
p |
Properties (optional) |
The request’s Origin must be one of the site’s origins. The answer is 202 with an empty body, 400 for a malformed event, 403 for an unknown key or origin, and 429 when rate limited. Trackers do not retry.
Validation
Section titled “Validation”| Item | Rule |
|---|---|
| Events per batch | 1–100 |
| Body | at most 512 KB after decompression |
| Event name | 1–64 characters, no control characters; names starting with $ are reserved |
| Event time | ISO 8601 in UTC. Up to 10 minutes in the future is treated as now; anything later, or older than 7 days, is dropped |
| Properties | at most 25; keys 1–64 characters; strings cut at 256 characters; finite numbers; Booleans; null, arrays and objects are dropped |
| Personal data | properties named like personal data, and values that look like an email, a phone number or a UUID, are dropped |
Rate limits
Section titled “Rate limits”- Per client address: 10 batch requests a second (bursts of 20), or 30 web events a second (bursts of 60).
- Per key: in proportion to your plan, averaged over a minute, and at least 50 events a second.
Health
Section titled “Health”GET /healthz answers 200 while the service is up.