# Errors & limits

Every failed request returns a structured error. The hosted surfaces use
exactly three envelope shapes — tool errors, MCP protocol errors, and
account-surface errors — and this page lists every code, when it fires,
whether retrying helps, and how to fix it. Tool calls run at
`https://mcp.agentastrology.com` — REST at `POST /v1/tools/{tool}`, MCP at
`/mcp`. Key management runs at `https://api.agentastrology.com`.

## 1. The three envelopes

**Tool errors** — REST bodies and MCP tool results share this shape:

```json
{
  "error": {
    "code": "invalid_input",
    "message": "Invalid input: expected object, received undefined at birthData"
  }
}
```

`code` is stable and switchable; `message` is human-readable — include it in
bug reports. Some codes add fields (`method`, `requiredTier`, `resetAt`) —
shown per code below. On MCP, this object sits under
`structuredContent.error` of a result with `isError: true`; on REST it is
the response body and the HTTP status carries the class.

**MCP protocol errors** — JSON-RPC error objects
(`{"jsonrpc":"2.0","id":…,"error":{"code":-32602,"message":"…"}}`) for
transport-level failures: malformed JSON, wrong headers, unknown tool name,
malformed call params. Covered in §5.

**Account-surface errors** — a flat shape
`{"error":"unauthorized","message":"Sign in required"}` (note: `error` is
the code string itself, not an object). Covered in §7.

## 2. Master table — every tool-surface code

| Code | HTTP (REST) | Fires when | Retryable? | What to do |
| --- | --- | --- | --- | --- |
| `invalid_input` | 400 | A field is missing, mistyped, or out of range; the `method` value isn't one of the tool's methods; the request body isn't a JSON object | No | Fix the payload — the message names the first bad field and its path |
| `invalid_birth_data` | 400 | Compute rejects the birth data past schema validation (latitude/longitude/unknown-time constraint messages) | No | Fix the named birth-data field |
| `unknown_tool` | 404 | The REST slug doesn't exist | No | Check the slug against `GET /v1/tools` |
| `missing_api_key` | 401 | A REST tool call carries no credentials | No | Send `Authorization: Bearer <key>` or `x-api-key: <key>` |
| `invalid_api_key` | 401 | The key is unknown or revoked | No | Check the key on your account page; create a new one if revoked |
| `tier_denied` | 403 | The method is paid-tier and the key's tier doesn't include it | No | Call a free method, or upgrade the key when paid tiers launch |
| `rate_limited` | 429 | The key's monthly call quota is exhausted | Yes — after `resetAt` | Wait for the window reset (§6); retrying earlier returns the same 429 |
| `method_not_yet_implemented` | 501 | The method is announced but not shipped (none on the current surface) | No | Wait for the release; the message names the method |
| `sweph_failure` | 500 | The ephemeris cannot cover the request — most commonly a chart date before ~675 AD, which is outside Chiron's range (§8) | No | Move the date into the supported range |
| `geocode_failure` | 500 | A place name cannot be resolved to coordinates | Once | Retry once; if it persists, pass a more specific place or explicit coordinates |
| `internal_error` | 500 | Anything the service couldn't classify, including dates several millennia out (§8) | Once | Retry once; if it persists, report the full `message` |
| `auth_unavailable` | 503 | The auth backend is temporarily not accepting checks | Yes | Retry with backoff |
| `auth_upstream_error` | 502 | The auth backend was unreachable | Yes | Retry with backoff; if persistent, check status page |

`invalid_input` and `invalid_birth_data` are almost always caller bugs — a
string timezone instead of a number, `birthData` nested into a flat tool like
`astro_moment`, or a method that belongs to a different tool. Fix the
payload, not the service.

## 3. Worked failure payloads

### Missing required field

`astro_chart` called with `{"method": "natal"}` and no `birthData`:

```json
{
  "error": {
    "code": "invalid_input",
    "message": "Invalid input: expected object, received undefined at birthData"
  }
}
```

The message is built from the first schema issue — its text plus the path
(`birthData`). The underlying issue the validator raised for this call, shown
so you know what the parts mean:

```json
{
  "expected": "object",
  "code": "invalid_type",
  "path": ["birthData"],
  "message": "Invalid input: expected object, received undefined"
}
```

### Value out of range

`birthData.year: 9999` — years run −4000..4000:

```json
{
  "error": {
    "code": "invalid_input",
    "message": "Too big: expected number to be <=4000 at birthData.year"
  }
}
```

### Method that isn't on the tool

Two shapes depending on where it is caught. With a key (HTTP surfaces), the
gate answers and names the tool:

```json
{
  "error": {
    "code": "invalid_input",
    "message": "astro_chart has no method 'nope'",
    "method": "nope",
    "requiredTier": null
  }
}
```

Without the gate (local stdio), schema validation answers instead:

```json
{
  "error": {
    "code": "invalid_input",
    "message": "Invalid input at method"
  }
}
```

### Missing key

```sh
curl -sS -X POST https://mcp.agentastrology.com/v1/tools/astro_chart \
  -H "Content-Type: application/json" \
  -d '{"method":"natal"}'
```

```json
{
  "error": {
    "code": "missing_api_key",
    "message": "Provide Authorization: Bearer <key> or x-api-key"
  }
}
```

### Unknown or revoked key (HTTP 401)

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "Unknown or revoked API key"
  }
}
```

### Tier-gated method on a free key (HTTP 403)

```json
{
  "error": {
    "code": "tier_denied",
    "message": "astro_aspects.between-charts requires the paid tier",
    "method": "between-charts",
    "requiredTier": "paid"
  }
}
```

### Quota exhausted (HTTP 429)

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Monthly quota exhausted",
    "resetAt": "2026-09-01T00:00:00.000Z"
  }
}
```

`resetAt` is ISO 8601 — the start of the next UTC calendar month. There is no
rate-limit response header; read the reset time from the body.

### Not-yet-implemented method (HTTP 501)

No method on the current surface returns it — if you ever see it, the method
is announced but not shipped:

```json
{
  "error": {
    "code": "method_not_yet_implemented",
    "message": "Method astro_returns.saturn is not yet implemented (Paid tier, coming soon).",
    "method": "astro_returns.saturn"
  }
}
```

### Ephemeris limit — Chiron range (HTTP 500)

Natal chart for year 600:

```json
{
  "error": {
    "code": "sweph_failure",
    "message": "calc failed for planet 15: Chiron's ephemeris is restricted to JD 1967601.5 - JD 3419437.5"
  }
}
```

### Ephemeris limit — date too remote (HTTP 500)

For year −4000:

```json
{
  "error": {
    "code": "internal_error",
    "message": "calc failed for planet 0: SwissEph file 'seplm42.se1' not found in PATH <ephemeris dir>"
  }
}
```

### Non-object REST body (HTTP 400)

Body `[]` — returned before authentication, so you can see it without a key:

```json
{
  "error": {
    "code": "invalid_input",
    "message": "request body must be a JSON object"
  }
}
```

## 4. MCP results and protocol errors

**Tool-level failures** — failure is a normal result object with
`isError: true` and the envelope under `structuredContent.error`. Check
`isError` before parsing anything else — it is the only reliable
success/failure signal. Prefer `structuredContent` over `content[0].text`
when your client surfaces it; the text block is the same JSON as a string.
Example:

```json
{
  "isError": true,
  "structuredContent": {
    "error": {
      "code": "invalid_input",
      "message": "Invalid input: expected object, received undefined at birthData"
    }
  },
  "content": [
    {
      "type": "text",
      "text": "{\"error\":{\"code\":\"invalid_input\",\"message\":\"Invalid input: expected object, received undefined at birthData\"}}"
    }
  ]
}
```

**Protocol errors** — JSON-RPC error objects for transport-level failures:

| JSON-RPC code | HTTP | Fires when | What to do |
| --- | --- | --- | --- |
| `-32700` | 400 | The POST body isn't valid JSON | Fix the JSON |
| `-32000` | 406 | Missing `Accept` pair — the endpoint requires both `application/json` and `text/event-stream` | Send `Accept: application/json, text/event-stream` |
| `-32601` | 200 | Unknown JSON-RPC method | Use the MCP methods your client library sends |
| `-32602` | 200 | `tools/call` names a tool that doesn't exist | Check the name against `tools/list` |
| `-32603` | 200 | `tools/call` params are malformed (e.g. `name` missing) | Fix the call params; the message quotes the failed fields |

Tool not found (note: in-protocol errors ride HTTP 200 — always check the
body, not just the status):

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "MCP error -32602: Tool astro_nope not found"
  }
}
```

Malformed JSON body:

```json
{
  "jsonrpc": "2.0",
  "error": { "code": -32700, "message": "Parse error: Invalid JSON" },
  "id": null
}
```

Two facts to keep in mind. First, invalid tool ARGUMENTS are never protocol
errors — they come back as `isError: true` results with `invalid_input` so
your agent can read and fix them. Second, auth on `/mcp` is checked per HTTP
request before the protocol runs, so auth failures arrive as plain JSON HTTP
errors (§3 codes), not JSON-RPC errors.

## 5. Rate limits

- Free tier: 500 calls per month. Paid tier: 50,000 calls per month. Enforced
  per key over the UTC calendar month; the window resets at 00:00 UTC on the
  first of the next month (that instant is the 429 body's `resetAt`).
- All keys are currently issued at paid tier with full access during the
  beta, so 50,000 is the number to plan against today.
- Every authenticated call draws one unit: each REST tool call, and each HTTP
  POST to `/mcp` — including `initialize` and `tools/list`.
- `GET /v1/tools`, `GET /v1/openapi.json`, and the health endpoint are
  unauthenticated and never count.
- Retrying while limited returns the same 429 and does not push the reset
  later.
- There is no rate-limit header on responses; the reset time lives only in
  the 429 body (`error.resetAt`, ISO 8601).

## 6. Account-surface errors (`api.agentastrology.com`)

Key management (`/keys`, `/usage`) is session-authenticated — sign in on the
website; the dashboard drives these. Its envelope is flat — `error` IS the
code string:

```json
{
  "error": "unauthorized",
  "message": "Sign in required"
}
```

| Code | HTTP | Fires when | What to do |
| --- | --- | --- | --- |
| `unauthorized` | 401 | No valid session | Sign in at the website and retry from the account page |
| `invalid_input` | 400 | Body isn't valid JSON, or `name` isn't a string of 1..64 chars | Fix the request body |
| `not_found` | 404 | Deleting a key that doesn't exist (or isn't yours) | List keys first; it may already be revoked |
| `endpoint_moved` | 410 | Calling the retired `/rpc/*` or `/chart/*` paths | Use the current REST/MCP endpoints on `mcp.agentastrology.com` |

## 7. Compute limits and silent fallbacks

- Chart dates accept years −4000..4000; outside that, schema validation
  answers with `invalid_input` naming `year` (§3, "Value out of range").
- Precision ephemeris data covers roughly 1200–2999 AD. Outside it, main
  bodies fall back to a built-in lower-precision model, but Chiron has a hard
  range of JD 1967601.5–3419437.5 (about 675 AD to 4650 AD) — and default
  charts include Chiron, so charts before ~675 AD fail with the
  `sweph_failure` shown under "Ephemeris limit — Chiron range". Dates several
  millennia out exceed the fallback model too and fail with the
  `internal_error` naming a missing ephemeris file.
- Polar latitudes are NOT an error: inside the polar circles, where
  Placidus/Koch-family cusps are undefined, the service computes Porphyry
  cusps instead and returns success — the same behavior as the major
  reference software. The chart's `summary.houseSystem` label still names the
  system you asked for, so check latitudes above ~66° if cusps look evenly
  trisected.

## Related

- [`public-connect.md`](public-connect.md) — get a key and connect Claude
  Code, Claude Desktop, or plain REST.
- [`public-quickstart.md`](public-quickstart.md) — first successful calls.
