# Chat with Panlytics

> Ask Panlytics a question through the API, follow the answer as it is written, and leave anything it proposes for a person to approve.

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

## What chat is

Chat is the same assistant people use in the app. It reads the shop's figures, its products and what Panlytics has learned about the shop, and answers in plain words: why a product is on the order list, what sold less than usual last week, which supplier is slowest.

Through the API, a question starts a **run**. The run works in the background, and you either poll it until it has its answer or follow it as it is written.

You need a key whose maker's role holds **Ask the agents**, at the shop you ask about. A key may ask; approving what the assistant proposes stays with a person in the app.

## Ask a question

```bash title="curl"
curl -X POST https://panlytics.net/api/v1/chat \
  -H "Authorization: Bearer $PANLYTICS_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "shopId": 12, "prompt": "Which soft drinks should I order this week, and why?" }'
```
```python title="Python"
import os
import uuid

import requests

answer = requests.post(
    "https://panlytics.net/api/v1/chat",
    headers={
        "Authorization": f"Bearer {os.environ['PANLYTICS_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"shopId": 12, "prompt": "Which soft drinks should I order this week, and why?"},
    timeout=30,
)
answer.raise_for_status()
started = answer.json()
print(started["runId"])
```

- `shopId` and `prompt` are required. `prompt` is at most 4,000 characters.
- Add `categoryId` to ask about one category's newest report. Without it the run opens on the shop's newest report of any category.
- Add the `threadId` of an earlier answer to continue that conversation.

The answer is `201` with `runId`, `threadId` and `status: "queued"`. Then:

- [`GET /v1/runs/{id}`](https://panlytics.net/developers/api/runs-get) answers with the run as it stands. Poll it every few seconds until `status` is `done`, `failed` or `cancelled`; `answer` holds the assistant's answer.
- [`GET /v1/runs/{id}/stream`](https://panlytics.net/developers/api/runs-stream) sends the answer as it is written, as server-sent events (a response that stays open and sends one line per piece). If the connection drops, reconnect with `?from=` set to the last offset you saw. A key may hold three streams open at once; a fourth answers `429 net.rate_limited` with `Retry-After: 15` until one closes.

```python title="Poll until it ends"
import time

while True:
    run = requests.get(
        f"https://panlytics.net/api/v1/runs/{started['runId']}",
        headers={"Authorization": f"Bearer {os.environ['PANLYTICS_KEY']}"},
        timeout=30,
    ).json()
    if run["status"] in ("done", "failed", "cancelled", "awaiting_approval"):
        break
    time.sleep(3)
print(run["status"], run["answer"])
```

## Proposals wait for a person

When the assistant wants to change something, such as keeping a product at a new level, it does not do it. It writes a **proposal**, the run's status becomes `awaiting_approval`, and the proposal is listed in the run's `waitingForPerson`. It waits in the app for a person to approve or reject it.

> [!IMPORTANT]
> No key can approve a proposal from a run a key started, whatever its maker's role holds. The approval answers `403 api.approve_in_app`, and the person decides in the app, signed in as themselves. That is part of the fence, so an AI asking the assistant cannot also be the one saying yes.

## What it costs

Every run spends the shop's daily AI budget, the same budget the app's chat uses: 2 euros a day per shop unless the CEO or a Manager changes it in the organisation's AI settings. The day turns at midnight UTC.

| Limit | What happens past it |
| --- | --- |
| 30 runs an hour per person | `429 net.rate_limited` with `limit: 30`; wait a few minutes, then ask again |
| The shop's daily AI budget | `429 api.ai_budget`, with `Retry-After` set to the seconds until midnight UTC |

Both are checked before the run starts, so a refused question costs nothing.

## When the key ends

A run outlives the call that started it. When its key stops being live (revoked, past its end, developer mode switched off, its maker removed, or its maker's role no longer holding the key ability), the run is stopped at its next step with the code `api.key_ended`, and an open stream closes on its next ping, within 15 seconds. Make a new key and ask again.
