- Docs
- Getting Started
- Error Codes
Astrology API error codes, what each one means and what to fix
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"
}
| Field | Use it for |
|---|---|
error | Reading. Plain English, and the wording can change, so never branch on it |
code | Branching. A stable identifier, safe to switch on forever |
doc_url | Looking 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.
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 before field validation ran.
Read the error field: it names the problem. A malformed JSON body is the usual cause, so check that the payload actually parses.
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 without Content-Type: application/json.
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_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 one on pricing.
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);
}
import os, requests
res = requests.get(
"https://roxyapi.com/api/v2/tarot/cards/fool",
headers={"X-API-Key": os.environ["ROXY_API_KEY"]},
)
if not res.ok:
err = res.json()
print(f"[{err['code']}] {err['error']} {err['doc_url']}")
if err["code"] == "validation_error":
print(err["issues"])
res.raise_for_status()
curl -s -o body.json -w '%{http_code}\n' \
https://roxyapi.com/api/v2/tarot/cards/fool \
-H "X-API-Key: $ROXY_API_KEY"
jq -r '"\(.code) \(.doc_url)"' body.json
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.
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.