4. Response envelope and error format
4.1 Success
{
"data": { "...": "..." },
"error": null,
"requestId": "<request-id>"
}
Responses carry Cache-Control: no-store and X-Request-Id. X-Request-Id is echoed when supplied (validated against ^[A-Za-z0-9._:-]{8,128}$; otherwise a UUID is generated).
4.2 Error
{
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Resource validation failed",
"fields": { "name": "Required" }
},
"requestId": "<request-id>"
}
fields is present only for validation errors. Keys are field paths; request is used when the path is empty.
4.3 Error codes
Codes below are the values found in the code. Each endpoint section lists the codes it can return.
| Code | Typical HTTP status | Meaning |
|---|---|---|
UNAUTHENTICATED | 401 | No session. Also used for "fresh authentication required" in the finance wrapper. |
MFA_REQUIRED | 401 or 403 | Account has no MFA enrolment where required. Status depends on the route (see endpoint sections). |
FRESH_AUTH_REQUIRED | 401 or 403 | Session is older than the freshness window. |
FORBIDDEN | 403 | Role, scope or policy does not permit the action. |
ORIGIN_DENIED | 403 | Origin not in the allowed set. |
IDEMPOTENCY_KEY_REQUIRED | 400 | Idempotency-Key header missing or malformed. |
IDEMPOTENCY_CONFLICT | 409 | Same idempotency key used with different data, or a duplicate transaction reference. |
VALIDATION_ERROR | 400, 422 | Request body or parameter failed validation. Status varies by route. |
NOT_FOUND | 404 | Record missing, or outside the actor's scope. Scope-denied and missing records are indistinguishable. |
INVALID_STATE | 409 | Record not in a state that allows the action. |
STALE_STATE | 409 | Expected lifecycle value no longer matches (optimistic concurrency). |
CONFLICT | 409 | Unique constraint violation on a generic create. |
FEATURE_UNAVAILABLE | 501 or 503 | Resource is gated, reporting is disabled, or a dependency is unavailable. |
PROVIDER_DISABLED | 503 | Webhook provider secret not configured. |
NOT_READY | 503 (readiness) or 409 (report download) | Dependency unavailable, or report not ready. |
RATE_LIMITED | 429 | Contact-form limit. |
LIMIT_REACHED | 429 | Report export concurrency or daily limit. |
PAYLOAD_TOO_LARGE | 413 | Body exceeds the route's limit. |
INVALID_CONTENT_TYPE | 400 or 415 | Body is not application/json. |
INTERNAL_ERROR | 500 | Unhandled error. Message is generic by design. |
WORKER_FAILED | 500 | Cron worker failed. |
UNAUTHORIZED | 401 | Worker secret mismatch or missing. |
Fields that appear on specific routes:
- Finance, financing and application errors use a domain code in
error.code, for exampleUNBALANCED_JOURNAL,ACCOUNT_INVALID,INVALID_STATE_TRANSITION,FINAL_TERMS_INVALID,ELIGIBILITY_FAILED. Those are listed per endpoint. - Better Auth errors follow Better Auth's own format.