Errors
Error envelope shape, full error code catalog, retry semantics, and rate-limit-specific handling.
Every Moonborn error response shares the same envelope:
{
"error": {
"code": "rate_limited",
"message": "Per-minute API rate limit exceeded.",
"details": { "retryAfter": 12 }
}
}code is a stable, machine-readable string. message is human-readable
(safe to show in logs; do not show to end users without translation).
details carries code-specific fields when relevant.
HTTP status code mapping
| Status | When |
|---|---|
400 | malformed request (parse error, missing required field) |
401 | missing or invalid bearer token |
403 | authenticated but unauthorized (scope or role insufficient) |
404 | resource not found |
409 | conflict — typically idempotency key collision, drift block, or persona state machine refusal |
422 | validation error (shape valid, semantics not) |
429 | rate-limited |
500 | unexpected server error |
502/503/504 | upstream LLM provider issue; retry |
Error code catalog
| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | Bearer token missing or expired |
forbidden | 403 | Token valid, insufficient scope/role |
not_found | 404 | Resource doesn't exist or isn't visible to caller |
validation_failed | 422 | Request body shape valid but value range / enum / required-field mismatch |
idempotency_conflict | 409 | Same idempotency key, different payload |
state_conflict | 409 | Resource state machine refused the transition (e.g. archive an already-deleted persona) |
drift_blocked | 409 | Drift detection action=block tripped on this reply |
audit_blocked | 409 | Audit verdict below threshold; persona returned in flagged state |
moderation_blocked | 422 | Output failed moderation; reply refused |
rate_limited | 429 | Per-tier rate cap hit; details.retryAfter (seconds) carries the cooldown |
quota_exceeded | 429 | Monthly quota hit (e.g. generation quota) |
provider_error | 502 | Upstream LLM provider failure; retry with backoff |
provider_timeout | 504 | Upstream LLM provider timed out |
internal_error | 500 | Unhandled exception; logged on our side |
Retry semantics
The SDKs auto-retry these codes with exponential backoff:
provider_error(5xx)provider_timeout(504)rate_limited(429) — honorsRetry-Afterheader
These codes are never auto-retried:
validation_failed— fix the request firstidempotency_conflict— change the keydrift_blocked/audit_blocked/moderation_blocked— caller decision
Idempotency-Key
Every write request accepts an Idempotency-Key header. Replays
within 24 hours return the original response. SDKs auto-generate the
key by default; override per-call if you have a deterministic
business key (Idempotency-Key: order-12345).
Rate-limit headers
Every response (success or error) carries:
X-RateLimit-Limit: 3000
X-RateLimit-Remaining: 2987
X-RateLimit-Reset: 1747498200
Use X-RateLimit-Remaining for backpressure in your client. See
Rate limits for per-tier caps.
Honest scope
We track the codes above; we do not surface internal exception
types, stack traces, or upstream provider error codes. If you're
debugging a provider_error, the audit log (GET /v1/audit/events)
carries the upstream error envelope under details.