Overview

API reference

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

Base URL and authentication

Every endpoint is under one base URL:

Base URL
https://panlytics.net/api

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

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 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.
  • 120 calls a minute per key. See 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:

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

EndpointWhat it does
GET /v1/meWho this key acts as, which company and shops it reaches, what it may do, and when it ends.
GET /v1/organisationThe company, the shops this key reaches, and its people where the key's maker may see them.
GET /v1/shopsThe shops this key reaches, one page at a time.
POST /v1/shopsOpen a new shop in the company.
GET /v1/shops/{id}One shop.
GET /v1/shops/{id}/categoriesThe categories of one shop, each with its newest report.
POST /v1/shops/{id}/reportsUpload a report: the first of a new category, or the next report of one.
GET /v1/categories/{id}One category, with the settings its figures are worked out with.
PATCH /v1/categories/{id}Rename a category, change the settings its figures use, or both.
GET /v1/categories/{id}/reorder-pointsPer product: the reorder point, what to order when it is reached, and the days of stock left.
GET /v1/categories/{id}/order-listWhat to order now: every product at or under its reorder point.
GET /v1/categories/{id}/stop-stockingThe products worth no longer stocking, most money tied up first.
POST /v1/categories/{id}/recomputeWork a category's summary figures out again and answer with them.
GET /v1/shops/{id}/suppliersWho delivers to one shop, and the days a delivery takes from each.
POST /v1/shops/{id}/suppliersAdd a supplier to one shop.
PATCH /v1/suppliers/{id}Rename a supplier or change the days a delivery takes from it.
DELETE /v1/suppliers/{id}Remove a supplier from its shop.
PUT /v1/suppliers/{id}/categoriesSet which categories of its shop a supplier delivers.
POST /v1/chatAsk Panlytics a question about a shop. The answer comes back as a run.
GET /v1/runs/{id}One run: its state, its answer, and the proposals that wait for a person.
GET /v1/runs/{id}/streamFollow one run live, as server-sent events.
POST /v1/agent-sign-in-linksA 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