# API reference

> Every endpoint of the Panlytics API, its request, its answer and its errors, with the base URL, authentication and conventions they share.

Markdown twin of https://panlytics.net/developers/api (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.

## Base URL and authentication

Every endpoint is under one base URL:

```text title="Base URL"
https://panlytics.net/api
```

Send the key in the `Authorization` header of every call, as a bearer token:

```http title="HTTP"
Authorization: Bearer pan_...
```

A call with no key answers `401` with the code `api.key_missing`. A wrong, ended or revoked key answers `401` with `api.key_refused`. [Keys and rotation](https://panlytics.net/developers/security/keys) explains how keys are made and how they end.

## Conventions

- **JSON in and out.** Bodies are JSON, except a report upload, which is `multipart/form-data`. Three lists also answer CSV, and one run answers as server-sent events.
- **Ids are whole numbers**, except a run's id, which is a UUID.
- **Times are UTC**, in ISO 8601, such as `2026-10-25T14:07:00.000Z`.
- **Unknown fields are refused.** A query parameter or a body field the endpoint does not know answers `400 api.invalid`, with the field's name in `detail`.
- **Refusals are JSON** with a `code` to branch on. See [Errors](https://panlytics.net/developers/security/errors).
- **120 calls a minute per key.** See [Rate limits](https://panlytics.net/developers/security/rate-limits).

## What a key can see

Every id is looked up in two steps, always in this order:

1. **Is it in the key's company?** If not, the answer is `404 not_found.said`, the same for an id that never existed and an id of another company. An id tells a key nothing about other companies.
2. **May the key's maker do this to it?** If not, the answer is `403 refused`, and the refusal is written to the company's security log, as it is for a person.

So inside its own company a key may learn that a shop exists and that it may not open it, which its maker sees from the shop list in the app too.

## Pages

Every list takes `cursor` and `limit` and answers one page:

```json title="A page"
{
  "data": [],
  "nextCursor": "eyJvIjo1MH0",
  "total": 412
}
```

- `limit` is 1 to 200, and 50 when you leave it out.
- Send `nextCursor` back as `cursor` for the next page. It is `null` on the last page.
- `total` is how many rows the whole list has.
- Treat the cursor as an opaque string. A cursor the API did not make answers `400 api.invalid`.

## CSV

`GET /v1/categories/{id}/reorder-points`, `/order-list` and `/stop-stocking` also answer `?format=csv`: the whole list as one file, with the columns of the app's own download. Paging is ignored then. A CSV needs the export ability in that category, and every CSV is written to the company's audit log.

## Idempotency and retries

A program on a bad connection cannot tell "the shop was not opened" from "the shop was opened and the answer was lost". The `Idempotency-Key` header answers that.

- Every `POST` that creates something takes it: `POST /v1/shops`, `POST /v1/shops/{id}/reports`, `POST /v1/shops/{id}/suppliers` and `POST /v1/chat`.
- Send a new random string, 1 to 255 characters, with each new request, and the same string when you retry that request.
- **The same key with the same request** within 24 hours answers with the first answer again, with the header `Idempotent-Replayed: true`, and does nothing twice.
- **The same key with a different request** answers `409 api.idempotency_conflict`.
- **The same key while the first is still running** answers `409 api.idempotency_busy` with `Retry-After: 5`.
- **A refusal is not kept.** Every endpoint refuses before it writes anything, so after a `4xx` the same key can be sent again once the cause is fixed.
- **A fault is kept, with its `500`.** A fault of ours may come after the write, so running the request again could make the thing twice. For 24 hours the same key answers the same `500`. Check what exists first (list the shops, the reports or the suppliers), then send the request with a new key if it is still missing.
- Before a stored answer is sent again, the key's permission is checked again. A key whose role lost the ability since the first call gets `403 refused`, not the stored answer.
- A key is remembered for the key's maker in the company, not for one API key, so a retry with a new API key is still caught.

## Changing a category safely

`PATCH /v1/categories/{id}` takes the `updatedAt` you read from `GET /v1/categories/{id}`, in the body or as an `If-Unmodified-Since` header. When somebody changed the category after that time, the change is refused with `409 api.stale`: read it again and send your change over the new version. Without either time the change is made over whatever is there.

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/me`](https://panlytics.net/developers/api/me) | Who this key acts as, which company and shops it reaches, what it may do, and when it ends. |
| [`GET /v1/organisation`](https://panlytics.net/developers/api/organisation-get) | The company, the shops this key reaches, and its people where the key's maker may see them. |
| [`GET /v1/shops`](https://panlytics.net/developers/api/shops-list) | The shops this key reaches, one page at a time. |
| [`POST /v1/shops`](https://panlytics.net/developers/api/shops-create) | Open a new shop in the company. |
| [`GET /v1/shops/{id}`](https://panlytics.net/developers/api/shops-get) | One shop. |
| [`GET /v1/shops/{id}/categories`](https://panlytics.net/developers/api/shops-categories) | The categories of one shop, each with its newest report. |
| [`POST /v1/shops/{id}/reports`](https://panlytics.net/developers/api/reports-upload) | Upload a report: the first of a new category, or the next report of one. |
| [`GET /v1/categories/{id}`](https://panlytics.net/developers/api/categories-get) | One category, with the settings its figures are worked out with. |
| [`PATCH /v1/categories/{id}`](https://panlytics.net/developers/api/categories-update) | Rename a category, change the settings its figures use, or both. |
| [`GET /v1/categories/{id}/reorder-points`](https://panlytics.net/developers/api/categories-reorder-points) | Per product: the reorder point, what to order when it is reached, and the days of stock left. |
| [`GET /v1/categories/{id}/order-list`](https://panlytics.net/developers/api/categories-order-list) | What to order now: every product at or under its reorder point. |
| [`GET /v1/categories/{id}/stop-stocking`](https://panlytics.net/developers/api/categories-stop-stocking) | The products worth no longer stocking, most money tied up first. |
| [`POST /v1/categories/{id}/recompute`](https://panlytics.net/developers/api/categories-recompute) | Work a category's summary figures out again and answer with them. |
| [`GET /v1/shops/{id}/suppliers`](https://panlytics.net/developers/api/suppliers-list) | Who delivers to one shop, and the days a delivery takes from each. |
| [`POST /v1/shops/{id}/suppliers`](https://panlytics.net/developers/api/suppliers-create) | Add a supplier to one shop. |
| [`PATCH /v1/suppliers/{id}`](https://panlytics.net/developers/api/suppliers-update) | Rename a supplier or change the days a delivery takes from it. |
| [`DELETE /v1/suppliers/{id}`](https://panlytics.net/developers/api/suppliers-delete) | Remove a supplier from its shop. |
| [`PUT /v1/suppliers/{id}/categories`](https://panlytics.net/developers/api/suppliers-categories) | Set which categories of its shop a supplier delivers. |
| [`POST /v1/chat`](https://panlytics.net/developers/api/chat-start) | Ask Panlytics a question about a shop. The answer comes back as a run. |
| [`GET /v1/runs/{id}`](https://panlytics.net/developers/api/runs-get) | One run: its state, its answer, and the proposals that wait for a person. |
| [`GET /v1/runs/{id}/stream`](https://panlytics.net/developers/api/runs-stream) | Follow one run live, as server-sent events. |
| [`POST /v1/agent-sign-in-links`](https://panlytics.net/developers/api/agent-sign-in-links-create) | A one-time link that signs you in to the app as the person who made the key, in a browser profile nobody else is signed in to. |

## Machine-readable

- [/developers/openapi.json](https://panlytics.net/developers/openapi.json) describes every endpoint as OpenAPI 3.1, for code generators and for tools like a ChatGPT action.
- [/developers/llms.txt](https://panlytics.net/developers/llms.txt) lists every page of these docs for an AI agent.
