Keys and security
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.
The shape of a refusal
A call that fails answers with an HTTP status and a JSON body:
{
"code": "api.key_refused",
"error": "That API key is not a live key.",
"requestId": null,
"detail": null
}{
"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. |