Errors
Every response outside the 2xx range uses the same JSON envelope. Your client can handle errors from a single format.
Envelope
{
"code": "validation_failed",
"message": "Please review the submitted fields and try again.",
"detail": "Please review the submitted fields and try again.",
"hint": "Check the type and format of each field.",
"details": {
"errors": [
{ "loc": ["body", "email"], "msg": "value is not a valid email address" }
]
},
"debug_id": "8e1f2c3a4b5c6d7e8f90",
"request_id": "8e1f2c3a4b5c6d7e8f90"
}
Fields
| Field | Type | Description |
|---|---|---|
code | string | Canonical, machine-readable code (validation_failed, forbidden, etc.). Use this for client logic. |
message | string | Human-readable message, translated according to the x-locale header. |
detail | string | Same as message, kept for backward compatibility. |
hint | string | null | Suggested action for the user/developer. May be null when there is no specific hint. |
details | object | Additional failure-specific context (invalid fields, resource IDs, etc.). Structure varies per error. |
debug_id | string | Unique request identifier — use it on support for investigation. |
request_id | string | Same value as debug_id; also returned in the X-Request-Id response header. |
Canonical codes
| HTTP | code | Meaning | How to react |
|---|---|---|---|
| 400 | bad_request | Malformed request (invalid JSON, wrong type). | Fix the payload and retry. |
| 401 | unauthorized | Missing, invalid or expired token/key. | Re-authenticate. |
| 403 | forbidden | Authenticated but lacking permission. | Use a key with a broader scope, or ask the admin for the permission. |
| 404 | not_found | Resource doesn't exist or isn't in your scope. | Confirm the ID and the org/project context. |
| 409 | conflict | Incompatible current state (version already published, duplicate email, etc.). | Refresh local state and retry. |
| 413 | payload_too_large | Body above 8 MB. | Split the payload or reduce content. |
| 422 | validation_failed / request_validation_failed | Invalid field according to the schema. | Read details.errors and fix it. |
| 429 | rate_limited | Request limit exceeded. | Read Retry-After and wait. See Limits. |
| 500 | internal_error | Unexpected server error. | May retry with backoff. If it persists, report with the debug_id. |
Some endpoints return more specific codes in code (for example plan_quota_exceeded, api_key_scope_insufficient, project_not_found). Treat any unknown code as "error of this HTTP type" — the HTTP status is the high-level source of truth.
Schema validation (422)
Requests with a body that doesn't match the schema return:
{
"code": "request_validation_failed",
"message": "Invalid data in request.",
"hint": "Check the required fields and formats.",
"details": {
"errors": [
{
"type": "string_type",
"loc": ["body", "email"],
"msg": "Input should be a valid string",
"input": 42
}
]
},
"debug_id": "..."
}
details.errors[] follows the standard Pydantic v2 format. Relevant fields:
loc: path of the offending field inside the payload (["body", "items", 0, "name"]).type: error type (missing,string_too_short,int_parsing, etc.).msg: detailed message.input: rejected value (may be omitted).
Authentication and permission (401 / 403)
code | Typical detail |
|---|---|
unauthorized | Messages like "Session expired or revoked", "Invalid or expired API key". |
forbidden | "Insufficient permission", "Insufficient API key scope", "Organization out of session scope". |
Practical difference:
401: the client cannot proceed without refreshing credentials.403: the client is authenticated but the credential isn't enough for the operation. May be API key scope, role in the organization, or attempt to access a project from another organization.
Rate limiting (429)
Response when per-minute or per-hour limit is exceeded:
{
"code": "rate_limited",
"message": "Rate limit exceeded.",
"hint": "Wait for the reset shown and try again.",
"details": {
"route": "analysis.validate",
"reset_at": "2026-04-24T15:31:00Z"
},
"debug_id": "..."
}
Extra headers on this response:
Retry-After: <seconds>— how long to wait before retrying.X-RateLimit-Limit,X-RateLimit-Remaining— limit state at block time.
See Limits and usage for per-plan policies.
Large payload (413)
{
"code": "payload_too_large",
"message": "Payload above the allowed limit (8388608 bytes).",
"hint": "Reduce the payload size before retrying."
}
Default limit is 8 MB. For logtest events above that, split into separate calls.
Message internationalization
Error messages (message, hint) are resolved in the client's language. Resolution order:
x-localeheader (accepted values:pt-BR,en-US).Accept-Languageheader.- Fallback to
pt-BR.
code values are never translated — they are your stable API contract. Always branch logic on code; use message and hint only to display to end users.
Debug IDs and correlation
The debug_id field also appears in the response X-Request-Id header. It is generated automatically (or accepted from the client via the same request header).
Use this ID to:
- Attach to a bug/support report — makes server log search easy.
- Correlate chained calls — you can send the same
X-Request-Idon sub-calls for a single trace.
For distributed tracing (OpenTelemetry), see the Traceparent header described in Conventions.
Handling examples
Python
import httpx
resp = httpx.post("https://ruleforge.example.com/api/v1/analysis/validate",
headers={"X-API-Key": key},
json=payload)
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", "60"))
time.sleep(retry_after)
# retry
elif resp.status_code >= 400:
body = resp.json()
raise RuleForgeError(
code=body["code"],
message=body["message"],
debug_id=body.get("debug_id"),
)
JavaScript
const res = await fetch(`${base}/analysis/validate`, {
method: "POST",
headers: { "X-API-Key": key, "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
if (!res.ok) {
const err = await res.json()
console.error(`[${err.code}] ${err.message} (debug_id=${err.debug_id})`)
throw err
}