# Errors

> Every refusal is JSON with a code, a sentence and a request id. The tables list every code an API caller can meet, with what to do about it.

Markdown twin of https://panlytics.net/developers/security/errors (last updated 2026-10-25). Every page of the Panlytics developer docs is listed at https://panlytics.net/developers/llms.txt, and all of them are in one file at https://panlytics.net/developers/llms-full.txt. The site guide is https://panlytics.net/llms.txt.

## The shape of a refusal

A call that fails answers with an HTTP status and a JSON body:

```json title="401"
{
  "code": "api.key_refused",
  "error": "That API key is not a live key.",
  "requestId": null,
  "detail": null
}
```
```json title="500"
{
  "code": "server",
  "error": "Something went wrong on our side.",
  "requestId": "3f6c2a9e-7d41-4b8a-9c0e-5a1d2b7e8f43",
  "detail": null
}
```

| Field | What it is |
| --- | --- |
| `code` | A stable name for what went wrong. Branch on this. |
| `error` | One sentence for a person. It may be reworded at any time, so never match on it. |
| `requestId` | Filled in when the fault is ours, and `null` otherwise. Quote it when you report a problem, and we can find the call. |
| `detail` | The one value a sentence names, such as a size or a field, or `null`. |

A `401` also carries the header `WWW-Authenticate: Bearer realm="panlytics"`, and a `429` carries `Retry-After`. Every answer, good or bad, is sent with `Cache-Control: no-store`.

## Every code

These are the codes any endpoint can answer with. Each endpoint's reference page lists the ones it can give, with when.

| Code | Status | What happened | What to do |
| --- | --- | --- | --- |
| `api.invalid` | 400 | The field is missing or not valid. | Read that endpoint in the API reference, correct the field and call again. |
| `bad_request.said` | 400 |  | Change it and try again. |
| `upload.said` | 400 |  | Change what you are sending and try again. |
| `api.key_missing` | 401 | This call carried no API key. | Send the key in the Authorization header as Bearer, then call again. |
| `api.key_refused` | 401 | That API key is not a live key. | Ask the person who gave you the key to make a new one. |
| `api.wrong_key_kind` | 401 | This key does not open these endpoints. | Use the key these endpoints were made for, or ask the person who gave you the key. |
| `agent.sign_in_off` | 403 | Letting an AI sign in to the app is switched off for this company. | Ask your CEO to switch it on in the company's Developer settings. |
| `api.approve_in_app` | 403 | A run an API key started waits for a person to approve it in the app. | Open the conversation in Panlytics, signed in as yourself, and decide there. |
| `api.key_ended` | 403 | The API key that started this run is no longer live, so the run was stopped. | Make a new key and start the run again. |
| `auth.not_fresh` | 403 | This step needs you to prove it is you. | Confirm your password or passkey, then try again. |
| `refused` | 403 | You do not have permission to do that. | Ask your CEO or a manager if you need it. |
| `refused.demo` | 403 | This is a public demo, and that part is switched off. | Open your own shop to do this for real. |
| `refused.said` | 403 |  | Ask an owner of your company if you need this. |
| `not_found.said` | 404 |  | Go back and pick it from the list again. |
| `api.idempotency_busy` | 409 | The first request with that Idempotency-Key is still running. | Wait a few seconds, then send it again. |
| `api.idempotency_conflict` | 409 | That Idempotency-Key was already used for a different request. | Send a new key with a new request, or send the first request again unchanged. |
| `api.stale` | 409 | This was changed after the time you sent. | Read it again, then send your change with the new updatedAt. |
| `conflict.said` | 409 |  | Reload the page and try again. |
| `report.none` | 409 | There is no report open for this shop. | Upload a report or load an example first. |
| `store.none` | 409 | No shop is open in this session. | Choose a shop from the switcher and try again. |
| `upload.too_large` | 413 | That file is too big to upload. | Send a file under instead. |
| `form.field` | 422 |  | Correct it and save again. |
| `upload.unreadable` | 422 | That file could not be read. | Check it opens on your machine, then upload it again. |
| `agent.links_limit` | 429 | This key's maker has asked for six sign-in links in the last hour. | Wait a minute and ask for a new link. |
| `api.ai_budget` | 429 | This shop has used today's assistant budget. | Try again after midnight UTC, or ask an owner to raise the daily limit in AI settings. |
| `api.rate_limited` | 429 | This key made too many calls in one minute. | Wait the seconds in Retry-After, then call again. |
| `net.rate_limited` | 429 | That was asked for too many times. | Wait a minute and try again. |
| `server` | 500 | Something went wrong on our side. | Try again in a minute; if it keeps happening, report it. |
| `agent.unreachable` | 503 | The assistant is not answering right now. | Everything else still works; try the assistant again in a few minutes. |
| `developer.unavailable` | 503 | API keys are not set up on this deployment yet. | Report it, and we will switch them on. |

`api.invalid` names the refused field in `detail`, as a dotted path such as `limit` or `settings.periodDays`. It is `body` for a body that is not JSON or is over 256 KB, `file` for a missing upload, and `cursor` for a cursor the API did not make. A code ending in `.said` carries its sentence in `detail`.

## What to do, by status

| Status | Meaning | What a program should do |
| --- | --- | --- |
| 400 | The request was not understood | Fix the request. Retrying it unchanged gets the same answer. |
| 401 | No key, or a key that is not live | Stop. Ask the person for a new key. |
| 403 | The key may not do this | Stop. Tell the person. Do not look for another way. |
| 404 | Not in the key's company | Check the id. An id of another company answers 404, the same as one that never existed. Inside the company, a shop the key may not open answers 403. |
| 409 | It clashes with something: no report yet, a name in use, a stale `updatedAt`, an `Idempotency-Key` in use | Read the current state, then decide again. |
| 413, 422 | The uploaded file was refused | Change the file, then upload again. |
| 429 | Too many calls, or the shop's AI budget for the day is used up | Wait the seconds in `Retry-After`, or until midnight UTC for the budget. |
| 500 | Something failed on our side | Retry once after a short wait. If it fails again, report the `requestId`. |
| 503 | Not switched on, or briefly too busy | Wait a minute and retry. If it keeps happening, report it. |
