message.comDevelopers

Errors, rate limits, versioning

How the API behaves at the edges. Error response shape, the status-code table, per-route rate-limit caps with their retry semantics, and the v1 versioning contract that lets us iterate without breaking your code.

Error shape

Every error response is a single JSON object with a consistent shape. Parse it once and you have a generic error path for the entire API.

Error body
{
  "statusCode": 400,
  "error": "invalid_body",
  "message": "Human-readable description",
  "detail": {
    "fieldErrors": {
      "email": "Required",
      "subject": "Must be 1-200 characters"
    }
  }
}

Fields:

  • statusCode mirrors the HTTP status. Always present.
  • error is a short stable machine-readable slug. Always present. Use this for branching in code (do not parse message).
  • message is a human-readable description. Safe to display to logged-in agents; never log raw messages to public surfaces.
  • detail is optional. Validation failures include a fieldErrors map keyed by request-body path.

Status codes

StatusSlugWhen it fires
400invalid_body / invalid_queryRequest payload failed validation. detail.fieldErrors shows what.
401unauthorized / token_revokedMissing, malformed, expired, or revoked bearer token.
402feature_locked / seat_limit_reachedPlan limit hit. Upgrade or remove a resource to unblock.
403forbiddenAuthenticated, but role / channel / department scope denies this action.
404not_foundResource does not exist or belongs to another workspace. We never leak existence.
409stale_versionexpectedVersion mismatch on an optimistic-lock mutation.
409conflictDuplicate resource (e.g., second department with same name).
409illegal_status_transitionState machine refused the requested transition.
410goneResource was deleted. Distinct from 404 so you can stop polling.
422semantic_errorBody parses but violates a business rule (e.g., closing a call that was never answered).
429rate_limitedSee rate-limit section below for caps and retryAfter.
500server_errorUnhandled exception on our side. Logged to Sentry. Retry with backoff.
503service_unavailableA dependency (Postgres, Redis) is degraded. Retry with jittered backoff.

Why 404 instead of 403 on cross-workspace? Returning 403 would confirm that the resource ID exists in a different workspace. 404 is information-hiding by design.

Rate limits

The default cap is 600 requests per minute per authenticated agent. Heavy-traffic endpoints (such as the agent's own /me and /me/workspace) get higher per-route caps to support the dashboard's live polling fallback.

Strict caps on auth endpoints

EndpointCapBucket
POST /api/v1/auth/register5/minper IP
POST /api/v1/auth/login5/minper email
POST /api/v1/auth/forgot-password3/minper IP
POST /api/v1/auth/reset-password5/minper IP

When you hit a cap

The response is 429 rate_limited with a retryAfter field (seconds until the cap resets). We also return a standard Retry-After header, so any well-behaved HTTP client library will already respect it.

429 response
{
  "statusCode": 429,
  "error": "rate_limited",
  "message": "Too many requests",
  "retryAfter": 12
}

Recommended retry strategy

  • Read retryAfter from the body or the Retry-After header.
  • Sleep that many seconds plus a small random jitter (50 to 300ms) to avoid retry stampedes.
  • Cap your total retries at three. If you still see 429, you have an unrelated bug or a tight loop.
  • For batch jobs, prefer cursor pagination over parallel fan-out. Two workers at 10 RPS each will outperform 50 workers at 1 RPS while staying under the cap.

Versioning

The major version lives in the URL path: /api/v1/.... We commit to never making a breaking change inside v1. Breaking changes ship as /api/v2/... with a 12-month deprecation window where both versions run side by side.

Optional date pin

For fine-grained pinning inside the v1 timeline, pass an optional header:

Version pin header
Message-Version: 2026-05-13

Pinning opts your request into the API behaviour that was current on that date. Useful when a non-breaking enhancement still changes a response field you rely on. The default is latest; we publish the version log in Changelog.

What counts as non-breaking

Inside v1 we reserve the right to:

  • Add new endpoints.
  • Add new fields to response objects.
  • Add new optional request parameters.
  • Add new error slugs at existing status codes.
  • Loosen validation (accept inputs we previously rejected).

Anything tighter than this counts as breaking and waits for v2. Treat unknown response fields as forward-compatible; do not enforce JSON strict-mode in your parser.

Deprecation policy

  • A deprecated endpoint or field appears in a Sunset response header and in Changelog.
  • The minimum deprecation window for v1 surface is 12 months from announcement.
  • Real customer migrations get hands-on support during the window; we will not yank a surface out from under you.