Sitelet https://roxyapi.com/docs/errors#invalid_api_key
Skip to content
  1. Docs
  2. Getting Started
  3. Error Codes

Astrology API error codes

Every failed RoxyAPI request answers with the same three fields. Find the code on this page and you have the fix.

{
  "error": "Card not found: the-fool-of-cups",
  "code": "not_found",
  "doc_url": "https://roxyapi.com/docs/errors#not_found"
}
FieldUse it for
errorReading. Plain English, and the wording can change, so never branch on it
codeBranching. A stable identifier, safe to switch on forever
doc_urlLooking it up. An absolute link to the entry on this page for that exact code

doc_url is the same URL in every environment, so log it, print it in your CLI, or paste it into a bug report and whoever reads it lands on the explanation.

A 400 adds one more field, issues, listing every field that failed at once so you can correct the whole request in a single retry instead of one round trip per mistake. See validation_error.

Every error the API answers is JSON, on every endpoint, including 404. Parse the body before you decide what went wrong, and branch on code, never on the status alone.

Every code at a glance

StatusCodeWhen
400validation_errorA field is missing or invalid. The body lists every failing field at once in issues, each with its path and the expected shape.
400bad_requestThe request was rejected as a whole, for example a body that is not valid JSON or a date range that ends before it starts.
400date_out_of_rangeA date lies outside the span the planetary positions are served for. The error names the supported dates.
401api_key_requiredNo API key was sent.
401invalid_api_keyThe key sent is malformed or cannot be verified.
401api_key_revokedThe key was valid once and has since been revoked from the account page.
401subscription_not_foundThe key verifies, but the subscription behind it no longer exists.
401subscription_inactiveThe subscription behind the key is cancelled, expired or suspended, or a trial is past its end date.
401publishable_key_not_allowed_on_mcpA publishable key was used on a Remote MCP server, which takes a secret key.
401invalid_client_ipA keyless request could not be attributed to a caller.
401unauthorizedAuthentication failed with no more specific reason to give.
403forbiddenThe key is valid, but this request is not permitted.
403origin_requiredA publishable key with an origin allowlist was used with no Origin header, as from a server or a native app.
403origin_not_allowedThe calling site is not on the origin allowlist of this publishable key.
404not_foundNothing is served at this path, or the identifier in it does not exist. A path miss adds suggestions, the closest real endpoints.
405method_not_allowedThe path exists for a different HTTP method. The body lists the accepted methods in allow, as does the Allow header.
409conflictThe request cannot be applied in the current state.
410goneThe path was removed permanently.
413payload_too_largeThe request body is larger than the API accepts.
415unsupported_media_typeA request with a body was sent without Content-Type: application/json.
422unprocessable_entityEvery field is valid, but the combination cannot be computed.
429rate_limit_exceededThe monthly request allowance is used up. Retry-After gives the seconds to the reset.
429rate_limit_per_minuteThe per-minute limit of the plan was reached. Retry-After gives the seconds to the next minute, and the monthly allowance is not charged.
429rate_limitedA rate limit was reached, reported without the plan detail.
429free_tier_exhaustedA keyless request has used up its trial allowance.
500internal_errorThe request failed inside our service. Retry with exponential backoff.
503compute_saturatedHeavy calculations are briefly at capacity. Send the same request again after Retry-After seconds.
AnyerrorA fallback for a status with no more specific code. The status and the error field carry the signal.

Keys and access

api_key_required

401. The request arrived with no key at all.

Send your key in the X-API-Key header, or as Authorization: Bearer ... from a browser. Authentication has a working example in curl and JavaScript. No key yet? Pick a plan on pricing and yours is created the moment payment clears.

invalid_api_key

401. A key was sent, but it is not one we can verify.

Almost always a copy and paste problem: a trailing space, a truncated value, or a placeholder like your_api_key_here left in the code. Copy the key again from the API Keys tab of your account. If you lost it, create a new one there.

api_key_revoked

401. The key was valid once and has since been revoked.

Someone on your team deleted it from the account page, usually after a leak. Create a fresh key and roll it out to every deployment that still carries the old one.

subscription_not_found

401. The key verifies, but the subscription behind it no longer exists.

This is what a key from a deleted account looks like. Start a new subscription on pricing, then swap in the key it gives you.

subscription_inactive

401. The subscription behind this key is not active.

A cancelled or suspended plan keeps working until the paid period runs out, and this is the response after that. Reactivate from your account and the same key starts working again.

unauthorized

401. A generic authentication failure with no more specific code to give.

Treat it exactly like api_key_required: check that the key is present, complete, and sent on every request rather than only the first.

forbidden

403. The key is valid but this particular request is not permitted.

Read the error field for the specific reason, then check the two publishable key entries below, which are the common causes.

origin_required

403. A publishable key with an origin allowlist was used from somewhere that sent no Origin header.

Browsers set that header for you, so this means the call came from a server, a script, or a native mobile app. Those are server side callers: use a secret key there and keep the publishable key for the browser.

origin_not_allowed

403. The page that made the call is not on the allowlist for this publishable key.

Add the site to the key on the API Keys tab of your account. Entries are plain hostnames such as example.com, protocol and port are ignored, and there are no wildcards, so staging and preview domains each need their own entry.

publishable_key_not_allowed_on_mcp

401. A publishable key was used against an MCP server.

MCP is server side traffic, so a publishable key carries no browser origin to check there. Use a secret key in your MCP client config. MCP setup shows where it goes for each client.

invalid_client_ip

401. A keyless request could not be attributed to a caller.

Send your API key in X-API-Key and the call runs against your own plan.

Request problems

validation_error

400. One or more fields in the request did not pass validation.

The body carries an issues array with every failure, not just the first, so one read tells you everything to change:

{
  "error": "date: Invalid date",
  "code": "validation_error",
  "doc_url": "https://roxyapi.com/docs/errors#validation_error",
  "issues": [
    {
      "path": "date",
      "message": "Invalid date",
      "code": "invalid_format",
      "format": "date"
    }
  ]
}

path is the dot separated field path, or (root) for the body itself. expected, minimum, maximum, format and pattern appear when the failure has them, which is what lets an AI agent rebuild a valid payload without reading English. Dates are YYYY-MM-DD and times are 24 hour HH:MM:SS; field formats lists the shapes per domain.

bad_request

400. The request was rejected as a whole rather than field by field.

Read the error field: it names the problem. The usual causes are a body that does not parse as JSON, and values that are each valid but contradict each other, such as a date range that ends before it starts.

date_out_of_range

400. A date in the request lies outside the span planetary positions are served for.

Every calculation that reads the planets supports 1550-01-02 to 2650-01-22; Chiron and the asteroids cover a shorter span, and the error field names the dates for the body you asked about. Send a date inside the span. Numerology, biorhythm and the other date-only domains answer for any calendar date.

unprocessable_entity

422. The request is well formed and every field is valid, but the combination cannot be computed.

The error field says which values conflict. Change one of them and retry.

unsupported_media_type

415. A POST, PUT or PATCH arrived with a body but without Content-Type: application/json. A request with no body at all is read as {}, so an endpoint whose fields are all optional answers it.

Every endpoint that takes a body takes JSON. Add the header. Most HTTP clients set it for you when you pass a JSON body rather than a string, so this usually means the body was sent as raw text.

payload_too_large

413. The request body is bigger than we accept.

Our endpoints are built around one subject per request, and the largest legitimate body is a compatibility call with two people, so a body near the cap almost always means a batch was assembled by mistake. Split it into one call per subject. A body far over the cap is refused before it reaches the API and comes back as a bare 413 with no JSON body, which is deliberate: nothing oversized is read.

method_not_allowed

405. The path exists, but not for this verb.

The body carries an allow array and the response carries an Allow header, both listing what the path does accept. Charts and readings are POST because they take a body; lookups and reference data are GET.

not_found

404. Nothing is served at this path, or the identifier in it does not exist.

On a path miss the body adds a suggestions array of the closest real endpoints, plus a docs link to the reference for that domain, so a typo is usually one field away from the fix. On an identifier miss the error field names the accepted values. Identifiers are forgiving about casing and separators, so Third Eye, third-eye and THIRD_EYE all resolve.

conflict

409. The request cannot be applied in the current state.

The error field says what already exists or what is already in progress. Read the current state, then retry with values that fit it.

gone

410. The path was removed permanently and is not coming back.

Delete the call from your code. Nothing here needs a retry.

Limits

rate_limit_exceeded

429. You are through your monthly request allowance.

The response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used and X-RateLimit-Reset, plus Retry-After in seconds, so a client that honours Retry-After stops instead of hammering. Cache the calls that never change (a natal chart for a fixed birth input is the same forever) or move up a plan on pricing. Caching and cost covers what is safe to keep.

rate_limit_per_minute

429. You sent more requests in one minute than your plan allows. Your monthly allowance is untouched: a request refused here is not counted against it.

Retry-After gives the seconds until the next minute starts, which is never more than 60. Wait that long and retry, and spread steady traffic evenly across the minute instead of firing it in one burst. A backfill or batch job should pace itself under the per-minute figure listed for your plan on pricing.

rate_limited

429. The same outcome as rate_limit_exceeded, reported without the plan specific detail.

Handle both the same way: back off for Retry-After seconds, then retry.

free_tier_exhausted

429. A keyless request has used up its trial allowance.

Send your API key in X-API-Key to run against your own plan. Get your API key.

Something went wrong on our side

internal_error

500. The request failed inside our service, not in your code.

Retry with exponential backoff. If it keeps happening, send us the error string, the endpoint, and the time in UTC from our contact page and we will trace it.

compute_saturated

503. We are briefly at capacity for heavy calculations.

The response carries Retry-After in seconds. Wait that long and send the same request again, unchanged: nothing about it is wrong.

error

Any status. A fallback code for a status that has no more specific name.

The HTTP status and the error field are the useful signals here. Retry 5xx, do not retry 4xx.

Handling this in code

Branch on code, retry only what is worth retrying, and print doc_url in your logs so the next person does not have to guess.

const res = await fetch('https://roxyapi.com/api/v2/tarot/cards/fool', {
  headers: { 'X-API-Key': process.env.ROXY_API_KEY }
});

if (!res.ok) {
  const err = await res.json();
  console.error(`[${err.code}] ${err.error} ${err.doc_url}`);
  if (err.code === 'validation_error') console.error(err.issues);
  if (res.status === 429 || res.status >= 500) {
    // Safe to retry after Retry-After seconds
  }
  throw new Error(err.code);
}

Retry 429 and 5xx with exponential backoff. Never retry a 4xx other than 429: the request will fail the same way until you change it.

Frequently asked questions

Is the code stable enough to switch on?

Yes. code is the machine readable half of the contract and is safe to branch on. The error sentence beside it is written for people and the wording can change at any time, so never parse it.

Why does my error response have a doc_url field?

It is a direct link to the entry for that exact code on this page, so a failing response explains itself without a search. The URL is identical in every environment, which makes it safe to log and to paste into a bug report.

Which errors should my client retry?

Retry 429 and any 5xx, with exponential backoff, and honour the Retry-After header on a 429. Every other 4xx describes something about the request itself and will keep failing until the request changes.

Does a failed request count toward my quota?

Yes. Every request that reaches the API counts toward your monthly quota whether it succeeds or fails; a 429 rate limit answer is the one exception. Validate inputs before you send them and never retry a 4xx unchanged. How to keep the bill flat with your own cache is in caching and cost.

How do I see every validation problem at once?

Read the issues array on a 400. It lists every field that failed in that request, each with its path and the reason, so one retry can fix all of them.