Errors
The canonical error envelope and the full /v1 error catalog.
Every /v1/* error returns the same JSON shape, regardless of HTTP status.
Switch on error.code (stable, machine-readable), never on the human
message, which we may rephrase between releases.
Envelope
{
"error": {
"code": "validation_error",
"message": "Request validation failed",
"details": {
"errors": [
{ "loc": ["body", "name"], "msg": "Field required", "type": "missing" }
]
}
},
"request_id": "01HZK3X4P5MQR8AY..."
}
| Field | Description |
|---|---|
error.code | Stable identifier from the catalog below. Switch on this. |
error.message | Human-readable summary. Safe to display to end users. |
error.details | Code-specific structured data (validation paths, retry hints, etc). Optional. |
request_id | Echoes the X-Correlation-ID response header. Quote this when you contact support. |
Examples
A couple of the codes you’re most likely to hit during integration:
# Missing or wrong scope (403)
$ curl -i https://api.rankprompt.com/v1/brands \
-H "Authorization: Bearer rp_live_READ_ONLY_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-X POST -d '{"name":"Acme Co"}'
HTTP/1.1 403 Forbidden
{
"error": {
"code": "insufficient_scope",
"message": "This endpoint requires the 'write:brands' scope",
"details": { "required_scope": "write:brands", "key_scopes": ["read:brands"] }
},
"request_id": "01HZK3..."
}
# Cross-tenant brand access (403)
$ curl https://api.rankprompt.com/v1/brands/other-workspace-brand-id \
-H "Authorization: Bearer rp_live_YOUR_KEY"
{ "error": { "code": "brand_not_authorized", "message": "..." }, "request_id": "..." }
# Quota exhausted (429)
$ curl -i https://api.rankprompt.com/v1/brands -H "Authorization: Bearer rp_live_YOUR_KEY"
HTTP/1.1 429 Too Many Requests
{ "error": { "code": "request_quota_exceeded", "message": "Monthly request quota exceeded" }, "request_id": "..." }
Status code conventions
/v1 follows a small, deliberate set of status codes. Catalog entries are the
canonical source; these conventions explain why each maps the way it does:
400 validation_error: caller mistake in the URL or service-layer logical validation: malformed query string, bad path param, exceeded a cap, or invalid state transition.details.errors[*]carries the field-level breakdown when produced by Pydantic.422 body_validation_error: the JSON body parsed but didn’t match the endpoint’s schema (missing required field, wrong type, range violation, unknown extra field onextra="forbid"schemas).details.errors[*]has the per-field breakdown. This is the RFC 7231 standard for schema-mismatched payloads, so generated SDKs should map it to their “Unprocessable Entity” exception class.422 invalid_*: the body parsed cleanly but a value is semantically invalid in a way the SDK should be able to branch on (currently onlyinvalid_frequencyfor unsupportedSchedule.frequencyvalues). Treat these like a typed enum violation, not a generic validation failure: theerror.codeis stable, so SDKs can map it to a typed exception.409 *_conflict/*_in_flight: request shape is fine, but it conflicts with current state (idempotency replay race, schedule already at cap, etc).402 insufficient_credits: auth and validation passed, but the call cannot be billed against the customer’s plan.429 request_quota_exceeded/rate_limit_exceeded: the call was rejected without running. Retryable after the window resets.5xx: server-side. Safe to retry with the sameIdempotency-Key(see the Idempotency page).
Catalog
The full list of codes the /v1 surface can return. New codes will only ever
be added; existing codes will not change their HTTP status or stable id.
| HTTP | Code | Message |
|---|---|---|
| 400 | idempotency_key_required | This endpoint requires an `Idempotency-Key` header to safely retry on network failures |
| 400 | invalid_cursor | Pagination cursor is malformed; pass back the value from a previous response unchanged |
| 400 | validation_error | Request validation failed |
| 401 | expired_api_key | API key has expired |
| 401 | invalid_api_key | API key is invalid or revoked |
| 401 | unauthorized | Missing or invalid credentials |
| 402 | insufficient_credits | Not enough credits to complete this operation |
| 403 | api_subscription_past_due | API subscription payment is past due. Update billing to continue. |
| 403 | api_subscription_required | An active API SKU subscription is required to call this endpoint |
| 403 | brand_not_authorized | API key is not authorized for this brand |
| 403 | browser_calls_forbidden | The Public Developer API is server-to-server only. Browser CORS calls are blocked; call /v1 from your backend with a bearer API key. |
| 403 | forbidden | You do not have permission to access this resource |
| 403 | insufficient_scope | API key does not have the required scope |
| 404 | facts_not_generated | Brand facts have not been generated yet; POST to /v1/brands/{id}/facts to create them |
| 404 | not_found | Resource not found |
| 409 | alias_already_accepted | Alias is already part of the brand's `brand_aliases` array |
| 409 | conflict | Resource conflict |
| 409 | idempotency_key_in_flight | A previous request with this `Idempotency-Key` is still being processed; retry shortly |
| 409 | idempotency_key_mismatch | An `Idempotency-Key` was reused with a different request body; pick a new key or send the original body |
| 409 | report_has_no_prompts | Report has no prompts; add prompts before triggering an analysis run |
| 409 | report_not_modifiable | Report is in a state that does not allow this operation (e.g. processing or already completed) |
| 409 | schedule_has_no_regions | Scheduled report has no region configurations linked; add at least one before triggering a run |
| 409 | schedule_inactive | Scheduled report is inactive; activate it before triggering a run |
| 410 | gone | Resource is no longer available |
| 422 | body_validation_error | Request body did not match the expected schema |
| 422 | brand_facts_required | This action requires the brand to have facts (a non-empty `description`). Either POST /v1/brands/{brand_id}/facts to research them asynchronously, or PATCH /v1/brands/{brand_id}/facts with `{"description": "..."}` to set them manually. |
| 422 | invalid_frequency | Schedule frequency must be one of: daily, weekly, monthly |
| 429 | rate_limit_exceeded | Per-minute rate limit exceeded |
| 429 | request_quota_exceeded | Monthly request quota exceeded |
| 500 | internal_error | An unexpected error occurred |
| 503 | service_unavailable | Service temporarily unavailable, please retry |
| 504 | external_timeout | Upstream service timed out, please retry |
Need help? Capture the failing
request_idand the time of the request, then email support@rankprompt.com. We can trace any single call end-to-end from that pair.