Errors
{ "error": { "code": "<snake_case>", "message": "<human readable>", "details": [ { "path": "…", "message": "…" } ] } }
details appears on validation errors only.
| Status | code | Meaning | Do |
|---|---|---|---|
| 400 | unknown_kind | /v1/e/:kind was not d, o, c, x | fix the path |
| 400 | invalid_msg_id | tracking id failed HMAC verification | the push was not from this deployment, or HMAC_SECRET changed |
| 400 | invalid_idempotency_key | header over 255 chars | shorten it |
| 401 | unauthorized | no key, malformed key, unknown or revoked key | check the header and the app |
| 403 | forbidden | public key on a server endpoint, or the key lacks the scope | use a secret key with the right scope |
| 404 | not_found | no such subscription/user/campaign in this app | a foreign id looks identical to a missing one |
| 409 | invalid_state | campaign cannot be sent/cancelled from its current status; no content yet | check status; finish the draft |
| 409 | idempotency_in_progress | first request with this key is still running | retry in a moment |
| 409 | various (winner) | not an experiment, no remainder, already sent, still sending | see message |
| 422 | invalid_body | zod validation failed | read details[].path |
| 422 | invalid_segment | segment DSL did not compile | read details[0].path |
| 422 | idempotency_key_reused | same key, different body | use a new key per intent |
| 429 | rate_limited | window exhausted | wait the seconds in the message |
| 500 | upsert_failed | Mongo returned no document on upsert | retry; check logs |
| 503 | database_unavailable | MongoDB unreachable | retry with backoff; /health is 503 too |
| 503 | encryption_key_missing | admin surface without ENCRYPTION_KEY | configure the API |
| 404 | — (no body) | any /v1/admin/* route when ADMIN_API_TOKEN is unset or wrong | intentional: the surface does not exist without the token |
Reading details
"details": [
{ "path": "content.en.image", "message": "Invalid url" },
{ "path": "target", "message": "provide exactly one of externalIds, userIds, subscriptionIds, segment or all" },
{ "path": "schedule.localTime", "message": "expected HH:MM" }
]
path is the dotted path into your request body. An empty path means the whole body (e.g. "no updatable fields given" on a PATCH).