Skip to main content
Every error response — validation, auth, rate limit, not found, server — uses the same JSON shape:
{
  "error": {
    "type": "invalid_request",
    "code": "attribute_enum_invalid",
    "message": "\"vip\" is not a valid value for \"lifecycle_stage\"; expected one of: lead, customer, churned",
    "param": "lifecycle_stage"
  }
}

type

Coarse category. One of:
typeWhen
invalid_requestBad input — missing field, wrong type, bad enum, etc. (mostly 400)
authenticationAPI key missing, invalid, or revoked (401)
permissionAuthenticated but not allowed (v1: unused; reserved for scoped keys in v1.1)
rate_limitBucket exhausted; check Retry-After (429)
idempotencyIdempotency-Key mismatch or in-flight conflict (409 / 422)
not_foundResource doesn’t exist or you can’t see it (404)
conflictSlug already used, unique violation, etc. (409)
serverInternal error (500)

code

Specific machine-readable identifier. Stable per (type, code) pair — safe to switch on:
if (error?.code === 'attribute_enum_invalid') {
  // surface the message to the user; check error.param
}

param

If the error is about a specific field, param names it (e.g., email, custom_attributes.tier). Absent for non-field errors.

message

Human-readable string. Do not parse this — code is the stable contract.