Guides
Upload a report
Send a shop's sales export to Panlytics through the API, to start a new category or bring the next period into one that exists.
What you need
- A key whose maker's role holds Edit the sheet and upload a report. A new category needs it over the whole shop; the next report of a category needs it in that category.
- The shop's id.
GET /v1/shopslists the shops the key reaches, with their ids. - The report: the sales export your till or ERP already makes, as
.xlsxor.csv. The limit is the same as in the app: at most 8 MB.
The full request and answer are on the reference page, POST /v1/shops/{id}/reports.
Start a new category
Send the file as multipart/form-data, with a category field naming the new category and periodDays, the number of days the report covers:
curl -X POST https://panlytics.net/api/v1/shops/12/reports \
-H "Authorization: Bearer $PANLYTICS_KEY" \
-H "Idempotency-Key: soft-drinks-2026-10-25" \
-F "category=Soft drinks" \
-F "periodDays=30" \
-F "file=@soft-drinks.xlsx"import os
import requests
with open("soft-drinks.xlsx", "rb") as report:
answer = requests.post(
"https://panlytics.net/api/v1/shops/12/reports",
headers={
"Authorization": f"Bearer {os.environ['PANLYTICS_KEY']}",
"Idempotency-Key": "soft-drinks-2026-10-25",
},
data={"category": "Soft drinks", "periodDays": "30"},
files={"file": report},
timeout=120,
)
answer.raise_for_status()
print(answer.json())import { readFile } from "node:fs/promises";
const form = new FormData();
form.set("category", "Soft drinks");
form.set("periodDays", "30");
form.set("file", new Blob([await readFile("soft-drinks.xlsx")]), "soft-drinks.xlsx");
const answer = await fetch("https://panlytics.net/api/v1/shops/12/reports", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PANLYTICS_KEY}`,
"Idempotency-Key": "soft-drinks-2026-10-25",
},
body: form,
});
console.log(answer.status, await answer.json());The answer is 201 with the new report and its category:
{
"reportId": 311,
"categoryId": 40,
"shopId": 12,
"newCategory": true,
"rows": 412,
"deduplicated": false
}Panlytics reads the rows, repairs what it can and flags what it cannot, exactly as it does for a report uploaded in the app. deduplicated is true when this exact file was uploaded before. If the shop already has a category with that name, the report becomes that category's next report, and newCategory is false.
Bring the next period into a category
To file the next report into a category that exists, send categoryId instead of category. periodDays is optional here: left out, the category keeps its period. GET /v1/shops/{id}/categories lists the categories with their ids.
curl -X POST https://panlytics.net/api/v1/shops/12/reports \
-H "Authorization: Bearer $PANLYTICS_KEY" \
-H "Idempotency-Key: soft-drinks-2026-11-01" \
-F "categoryId=40" \
-F "file=@soft-drinks-november.xlsx"Send it once
When it is refused
| Code | Status | What to change |
|---|---|---|
api.invalid | 400 | detail names the field: no file, both or neither of category and categoryId, or no periodDays for a new category. |
upload.said | 400 | The file failed a check, such as its type. detail says what. Send .xlsx or .csv. |
upload.too_large | 413 | Send a file under 8 MB. |
form.field | 422 | What the file holds was refused. detail says why. |
refused | 403 | The key's maker may not upload to this shop or category. |
refused.demo | 403 | A shared demo account cannot upload. |
not_found.said | 404 | The shop or category id is wrong, or the category is not in this shop. |
An agent should ask its person before it uploads anything, as the agent prompt tells it to. Every upload is written to the audit log under the maker's name.