Error body
Errors return a JSON body with a stable machine code inerror and, where available, a human-readable
detail naming the offending field:
Authentication errors
status
Missing, invalid, revoked, or expired key. Never reveals whether a key exists.
status
The key is valid but resolves to no organization, or is missing the scope the operation requires.
status
The authentication provider was temporarily unreachable — your key was not checked and its validity is
unknown. Body
{ "error": "auth_unavailable" } with a Retry-After header. Retry with backoff; do not treat
this as a bad key.POST /api/v1/events the auth body is { "error": "unauthorized" }; elsewhere it’s
{ "error": "Unauthorized" }. Same meaning, different casing.
Rate limits
Limits are per-org, counted in a fixed window that resets on the boundary (not a sliding window). Over-limit responses carry aRetry-After header (seconds).
The events limit and the trigger limit are separate buckets — using one doesn’t consume the other. A
dry_run event costs the same quota as a live one.
Separately, a per-IP limit of 60 requests / minute / IP is applied before authentication: a flood of
bad-key requests is throttled at the edge before it ever reaches key verification, so abusive traffic can’t
degrade auth for real tenants. This pre-auth 429 has body { "error": "rate_limited" } and a Retry-After
header — back off and retry; it says nothing about your key.
Request size
Every write endpoint caps the request body and returns413 payload_too_large — with a detail naming the
byte ceiling — when the declared Content-Length or the actual body exceeds it. POST /api/v1/events caps
at 256 KB; every other write endpoint (import/jobs, conversions, tracking/lead, webhooks/trigger,
customers/webhook) caps at 1 MB. A 413 is terminal — trim the payload and retry.
Retryable vs terminal
Retry (transient — the request was not applied):
429 rate_limited / Too many requests,
503 automations_disabled, 503 geocoding_unavailable, 503 auth_unavailable (auth provider unreachable —
the key was not checked), 502 geocoding_failed, 500, and 409 import_in_progress (the original request
with your Idempotency-Key is still running — retry with the same key for its result). Honor Retry-After
where present and back off. Because writes are idempotent, retrying is always safe.Do not retry unchanged (terminal — fix the request): 400, 401, 403, 413, 422, 404. Correct the
payload, key, scope, or automation id first. One exception: on POST /api/v1/webhooks/trigger the platform
kill switch returns 400 with error: "Automations are globally disabled". That one is transient — retry
it rather than dropping the record. (POST /api/v1/events reports the same condition as a 503.)Recovering from gaps
There is no reconciliation sweep — Mailr won’t pull missed records from your CRM. If deliveries failed or you suspect a gap, replay the affected events. Idempotency (dedup_id, deal_id, address) means replaying a
whole time window is free: already-processed items come back as duplicate / idempotent and nothing is
double-mailed.