Errors
Every error code the API can return, what triggers it, and how to fix it.
Every Millimetric error is a JSON response with a stable string error field and an HTTP status code. The string is the contract — the human-readable message is not.
{
"error": "invalid_payload",
"details": { "fieldErrors": { "event": ["Required"] } }
}Auth errors (401 / 403)
Status
error
Trigger
Fix
401
missing_bearer_token
No Authorization header.
Send Authorization: Bearer {key}.
401
malformed_api_key
Key doesn't match `(pk
sk
401
invalid_api_key
No matching key in Supabase, or HMAC doesn't verify.
Mint a new key. Old one was revoked or never existed.
401
key_kind_mismatch
Stored key has a different kind than the prefix claims.
Re-mint. Likely a copy-paste from another row.
401
invalid_session
Admin endpoint: user JWT failed Supabase validation.
Sign in again.
403
origin_not_allowed
pk_* key from an origin not in the project's allowed_origins.
Add the origin in the dashboard, or use sk_* server-side.
403
insufficient_scope
Read endpoint called with pk_*/sk_*, or write endpoint with rk_*.
Use a key with the right scope: pk_/sk_ for ingest, rk_ for read.
403
forget_requires_secret_key
/v1/forget called with pk_*.
Use sk_*. Browser keys are explicitly rejected to prevent leak-induced wipes.
Validation errors (400)
Status
error
Trigger
Fix
400
invalid_payload
Body failed Zod parse. details.fieldErrors is the flattened tree.
Inspect details. Common causes: event missing, properties > 8 KB, malformed url.
400
invalid_params
Query string failed validation.
Check from/to are ISO-8601, metric is one of count/uniques.
400
invalid_group_by
Unknown column passed to /v1/stats?group_by=. Response includes the allowed list.
Use only: event_name, source, medium, country, device_type, browser, os, path.
Rate limits (429)
Status
error
Trigger
Fix
429
rate_limited
Token bucket exhausted for this (project_id, route). Retry-After header present.
Back off Retry-After seconds, then retry. For sustained throughput, switch to /v1/batch.
Limits:
POST /v1/track
50/sec
200
POST /v1/batch
5/sec
20
(Per-Worker-instance today. Move to a Durable Object if needed.)
Server errors (5xx)
Status
error
Trigger
Fix
500
internal_error
Unhandled exception. Worker logs have the trace.
Retry once with jitter. If persistent, check wrangler tail.
500
forget_failed
ClickHouse rejected the ALTER TABLE … DELETE.
Worker logs. Likely a transient ClickHouse issue.
502
upstream_failed
Couldn't reach ClickHouse / Supabase.
Same as above — retry.
MCP-specific errors (JSON-RPC)
The MCP transport uses JSON-RPC error envelopes:
-32600
invalid_request — body not a valid JSON-RPC envelope.
-32601
method_not_found — unknown method.
-32602
unknown_tool / unknown_resource / unhandled_tool.
-32000
tool_failed (server error) / insufficient_scope.
Retry guidance
4xx (validation, auth, scope, origin)
No
The payload is wrong. Fix it.
429 rate_limited
Yes
Honour Retry-After. Use /v1/batch for sustained writes.
5xx
Yes
Exponential backoff. The Node SDK does 100/200/400/800 ms by default.
Network errors
Yes
Same as 5xx.
Worker logs
The HTTP response only ever returns a short error code. To see the full traceback:
Every error logs the request path, the project (when known), and the failure point.
See also
API overview — base URL, content type, idempotency.
Rate limits — full table.
API keys — key kinds and scopes.
Last updated
Was this helpful?