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":"adidas","website":"https://www.adidas.com"}'
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.422 ai_platform_unsupported: anai_platformsvalue is not one of the six engines (chatgpt,claude,gemini,grok,perplexity,ai_overviews).details.valuenames the rejected value,details.supportedlists the accepted ones, and for a retired engine (chatgpt_search,claude_search)details.replacementnames the engine that runs in its place. Send the replacement; reads still return the retired values on past results.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. When a plan cap stopped it (brands, tracked keywords, rank checks per day and the like),details.reasonisplan_limitanddetailsalso carriesfeature,limit,current_count,current_plan,plans,suggested_planandcan_subscribe(see below).403 account_membership_required: the key was made in a team account its creator is no longer an active member of;details.account_idnames that account. Retrying will not help: create a key in an account you belong to. See Team keys.402 payment_failed: a purchase whose payment could not complete.details.reasonsays why (the payment token was spent, too small or in another currency), so an agent can ask its wallet for a new one.409 payment_processing: the buyer paid and the purchase is still being applied. Repeat the same call shortly; it will not charge again.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).
Plan limits and can_subscribe
A plan_limit 402 says who can lift the cap. details.can_subscribe is
true only when the key acts in its creator’s own account, the capped plan is
the creator’s own, and the key holds write:billing: then
POST /v1/me/subscriptions can start the plan that lifts it. It is false
when the plan belongs to someone else, a team account’s owner or the owner
of the brand, so only they can upgrade.
{
"error": {
"code": "insufficient_credits",
"message": "Your current plan allows up to 25 tracked keywords. Upgrade your plan to track more.",
"details": {
"reason": "plan_limit",
"feature": "tracked_keywords",
"current_plan": "starter",
"limit": 25,
"current_count": 25,
"plans": [
{ "plan": "free", "limit": 10 },
{ "plan": "starter", "limit": 25 },
{ "plan": "pro", "limit": 100 },
{ "plan": "agency", "limit": 250 },
{ "plan": "agency_plus", "limit": 500 }
],
"suggested_plan": "pro",
"can_subscribe": true
}
},
"request_id": "01HZK3..."
}
Past-due billing
403 api_subscription_past_due (the API plan’s payment failed) and
403 subscription_past_due (the plan’s payment failed, so it is suspended)
are billing failures, not missing access. details.billing_url opens the
Subscription page, where Manage billing updates the card; the next call
after the payment clears goes through. details.upgrade_url still points at
the API plans page. In a team account neither link is sent: the message says
that the account’s owner updates billing.
{
"error": {
"code": "api_subscription_past_due",
"message": "API subscription payment is past due. Update billing to continue.",
"details": {
"billing_url": "https://app.rankprompt.com/subscription",
"upgrade_url": "https://app.rankprompt.com/developers?source=api",
"docs_url": "https://rankprompt.com/docs/v1/errors/#past-due-billing"
}
},
"request_id": "01HZK3..."
}
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 | google_connection_expired This brand's Google connection expired and has to be reconnected | This brand's Google connection expired and has to be reconnected |
| 400 | google_not_connected This brand has not connected Google yet | This brand has not connected Google yet |
| 400 | google_not_linked Google is connected but no Business Profile location is linked | Google is connected but no Business Profile location is linked |
| 400 | google_property_not_selected Google is connected but no GA4 property or Search Console site is selected | Google is connected but no GA4 property or Search Console site is selected |
| 400 | idempotency_key_required This endpoint requires an `Idempotency-Key` header to safely retry on network failures | This endpoint requires an `Idempotency-Key` header to safely retry on network failures |
| 400 | integration_not_connected What this call needs is not connected; details.connect_url is where the user connects it | What this call needs is not connected; details.connect_url is where the user connects it |
| 400 | invalid_cursor Pagination cursor is malformed; pass back the value from a previous response unchanged | Pagination cursor is malformed; pass back the value from a previous response unchanged |
| 400 | validation_error Request validation failed | Request validation failed |
| 401 | api_key_required The public /v1 API requires an API key (Bearer rp_…). | The public /v1 API requires an API key (Bearer rp_…). |
| 401 | expired_api_key API key has expired | API key has expired |
| 401 | invalid_api_key API key is invalid or revoked | API key is invalid or revoked |
| 401 | unauthorized Missing or invalid credentials | Missing or invalid credentials |
| 402 | insufficient_credits Not enough credits to complete this operation | Not enough credits to complete this operation |
| 402 | payment_failed The payment could not be completed; `details.reason` says why and what to change. | The payment could not be completed; `details.reason` says why and what to change. |
| 403 | account_membership_required The API key acts in a team account its creator is no longer an active member of | The API key acts in a team account its creator is no longer an active member of |
| 403 | account_plan_required The plan of the account this key acts in does not include this feature | The plan of the account this key acts in does not include this feature |
| 403 | api_subscription_past_due API subscription payment is past due. Update billing to continue. | 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 | An active API SKU subscription is required to call this endpoint |
| 403 | brand_not_authorized API key is not authorized for this brand | API key is not authorized for this brand |
| 403 | brand_plan_required The brand owner's plan does not include this feature | The brand owner's plan does not include this feature |
| 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. | 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 | You do not have permission to access this resource |
| 403 | insufficient_scope API key does not have the required scope | API key does not have the required scope |
| 403 | subscription_past_due Your plan's subscription is past due, so the plan is suspended. Update billing to continue. | Your plan's subscription is past due, so the plan is suspended. Update billing to continue. |
| 404 | facts_not_generated Brand facts have not been generated yet; POST to /v1/brands/{id}/facts to create them | Brand facts have not been generated yet; POST to /v1/brands/{id}/facts to create them |
| 404 | not_found Resource not found | Resource not found |
| 409 | alias_already_accepted Alias is already part of the brand's `brand_aliases` array | Alias is already part of the brand's `brand_aliases` array |
| 409 | conflict Resource conflict | Resource conflict |
| 409 | idempotency_key_in_flight A previous request with this `Idempotency-Key` is still being processed; retry shortly | 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 | An `Idempotency-Key` was reused with a different request body; pick a new key or send the original body |
| 409 | payment_processing The payment went through and is still being applied. Repeat the same call shortly. | The payment went through and is still being applied. Repeat the same call shortly. |
| 409 | report_has_no_prompts Report has no prompts; add prompts before triggering an analysis run | 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) | 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 | 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 | Scheduled report is inactive; activate it before triggering a run |
| 409 | webhook_limit_reached Maximum number of active webhook endpoints per brand reached (5) | Maximum number of active webhook endpoints per brand reached (5) |
| 410 | gone Resource is no longer available | Resource is no longer available |
| 422 | ai_platform_unsupported An AI platform in `ai_platforms` is not supported. `details.supported` lists the ones that are; `details.replacement` names the engine that runs in place of a retired one. | An AI platform in `ai_platforms` is not supported. `details.supported` lists the ones that are; `details.replacement` names the engine that runs in place of a retired one. |
| 422 | body_validation_error Request body did not match the expected schema | 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. | 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 is not one this endpoint accepts; `details.supported` lists the ones it does. | Schedule frequency is not one this endpoint accepts; `details.supported` lists the ones it does. |
| 422 | webhook_url_unreachable Webhook URL must be a public HTTPS endpoint; it is not HTTPS, does not resolve, or is a private address | Webhook URL must be a public HTTPS endpoint; it is not HTTPS, does not resolve, or is a private address |
| 429 | rate_limit_exceeded Per-minute rate limit exceeded | Per-minute rate limit exceeded |
| 429 | request_quota_exceeded Monthly request quota exceeded | Monthly request quota exceeded |
| 500 | internal_error An unexpected error occurred | An unexpected error occurred |
| 503 | google_connection_unavailable Google could not be reached; the connection is intact, so retry | Google could not be reached; the connection is intact, so retry |
| 503 | service_unavailable Service temporarily unavailable, please retry | Service temporarily unavailable, please retry |
| 504 | external_timeout Upstream service timed out, please retry | Upstream service timed out, please retry |
| 504 | query_timeout The read ran past the database's time limit. The same call will time out again: narrow it with date_from and date_to, a report or another filter. To read a brand's whole history, start an export with POST /v1/brands/{brand_id}/exports. | The read ran past the database's time limit. The same call will time out again: narrow it with date_from and date_to, a report or another filter. To read a brand's whole history, start an export with POST /v1/brands/{brand_id}/exports. |
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.