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..."
}
FieldDescription
error.codeStable identifier from the catalog below. Switch on this.
error.messageHuman-readable summary. Safe to display to end users.
error.detailsCode-specific structured data (validation paths, retry hints, etc). Optional.
request_idEchoes 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 on extra="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 only invalid_frequency for unsupported Schedule.frequency values). Treat these like a typed enum violation, not a generic validation failure: the error.code is stable, so SDKs can map it to a typed exception.
  • 422 ai_platform_unsupported: an ai_platforms value is not one of the six engines (chatgpt, claude, gemini, grok, perplexity, ai_overviews). details.value names the rejected value, details.supported lists the accepted ones, and for a retired engine (chatgpt_search, claude_search) details.replacement names 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.reason is plan_limit and details also carries feature, limit, current_count, current_plan, plans, suggested_plan and can_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_id names 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.reason says 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 same Idempotency-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
400 google_connection_expired

This brand's Google connection expired and has to be reconnected

400 google_not_connected

This brand has not connected Google yet

400 google_not_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

400 idempotency_key_required

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

400 invalid_cursor

Pagination cursor is malformed; pass back the value from a previous response unchanged

400 validation_error

Request validation failed

401 api_key_required

The public /v1 API requires an API key (Bearer rp_…).

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

402 payment_failed

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

403 account_plan_required

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.

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 brand_plan_required

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.

403 forbidden

You do not have permission to access this resource

403 insufficient_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.

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 payment_processing

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

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

409 webhook_limit_reached

Maximum number of active webhook endpoints per brand reached (5)

410 gone

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.

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 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

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 google_connection_unavailable

Google could not be reached; the connection is intact, so retry

503 service_unavailable

Service temporarily unavailable, please retry

504 external_timeout

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.

Need help? Capture the failing request_id and the time of the request, then email support@rankprompt.com. We can trace any single call end-to-end from that pair.