Recipes / Best sellers & product performance
ReportsRank products by revenue or quantity for any period, per location, straight from invoice rows.
Built on the invoice-row pipeline our financial report uses.
One call ranks every product sold in the period by revenue (incl VAT). It is built from the same invoiced line items as the financial report, so the revenue here reconciles with your bookkeeping numbers. Dates are inclusive, Europe/Amsterdam.
# top 10 by revenue for the whole of June
curl -s "https://api.storekeeper.me/api/reports/product-sales?from=2026-06-01&to=2026-06-30&limit=10" \
-H "Authorization: Bearer $TOKEN"
{
"from": "2026-06-01",
"to": "2026-06-30",
"location_id": null,
"sort": "revenue",
"count": 10,
"total": 412,
"distinct_products": 412,
"truncated": false,
"data": [
{ "product_id": 4477, "name": "Appeltaart groot", "sku": "TAART-APPEL", "quantity": 118, "revenue_incl_vat": 6200.90 },
{ "product_id": 4410, "name": "Volkorenbrood 800g", "sku": "BROOD-VLB-800", "quantity": 1240, "revenue_incl_vat": 4216.00 },
{ "product_id": 4512, "name": "Croissant roomboter", "sku": "CRS-RB", "quantity": 2110, "revenue_incl_vat": 3376.00 }
]
}
The same call with sort=quantity answers a different business question. Revenue ranks your cash cows; quantity ranks your traffic drivers, the products that get people through the door even at a low price point. Products near the top of one list and far down the other are the interesting ones.
curl -s "https://api.storekeeper.me/api/reports/product-sales?from=2026-06-01&to=2026-06-30&sort=quantity&limit=10" \
-H "Authorization: Bearer $TOKEN"
# June, side by side
by revenue: 1. Appeltaart groot 2. Volkorenbrood 800g 3. Croissant roomboter
by quantity: 1. Croissant roomboter 2. Volkorenbrood 800g 3. Appeltaart groot
Call once per location_id to compare branches. The same product can be a best seller in one branch and dead stock in another; find the ids via /api/locations and diff the top lists.
curl -s "https://api.storekeeper.me/api/reports/product-sales?from=2026-06-01&to=2026-06-30&location_id=14&limit=25" \
-H "Authorization: Bearer $TOKEN"
curl -s "https://api.storekeeper.me/api/reports/product-sales?from=2026-06-01&to=2026-06-30&location_id=17&limit=25" \
-H "Authorization: Bearer $TOKEN"
Two immediate uses: assortment pruning (everything below rank 300 of 412 that also has thin margin is a delisting candidate) and shelf placement (your quantity top 10 belongs at eye level and near the register). total and distinct_products tell you how long the tail is; raise limit (up to 500) to see it.
# the long tail: how much of the assortment barely sells?
curl -s "https://api.storekeeper.me/api/reports/product-sales?from=2026-06-01&to=2026-06-30&limit=500" \
-H "Authorization: Bearer $TOKEN" | jq '[.data[] | select(.revenue_incl_vat < 50)] | length'
quantity and revenue_incl_vat, so a product can even show a negative total. That is correct for reconciliation, but remember it when reading "units sold" as foot traffic.Recepten / Bestsellers & productprestaties
ReportsRangschik producten op omzet of aantal voor elke periode, per locatie, rechtstreeks uit factuurregels.
Gebouwd op de factuurregel-pijplijn van ons financieel rapport.
Eén aanroep rangschikt elk verkocht product in de periode op omzet (incl. btw). Hij is gebouwd op dezelfde gefactureerde orderregels als het financieel rapport, dus de omzet hier sluit aan op je boekhoudcijfers. Datums zijn inclusief, Europe/Amsterdam.
# top 10 op omzet voor heel juni
curl -s "https://api.storekeeper.me/api/reports/product-sales?from=2026-06-01&to=2026-06-30&limit=10" \
-H "Authorization: Bearer $TOKEN"
Dezelfde aanroep met sort=quantity beantwoordt een andere bedrijfsvraag. Omzet rangschikt je melkkoeien; aantallen rangschikken je loopstukken, de producten die mensen binnenhalen ook al is de prijs laag. Producten die hoog in de ene lijst staan en laag in de andere zijn de interessante.
curl -s "https://api.storekeeper.me/api/reports/product-sales?from=2026-06-01&to=2026-06-30&sort=quantity&limit=10" \
-H "Authorization: Bearer $TOKEN"
Roep één keer per location_id aan om vestigingen te vergelijken. Hetzelfde product kan in de ene vestiging een topper zijn en in de andere winkeldochter; vind de ids via /api/locations en vergelijk de toplijsten.
curl -s "https://api.storekeeper.me/api/reports/product-sales?from=2026-06-01&to=2026-06-30&location_id=14&limit=25" \
-H "Authorization: Bearer $TOKEN"
curl -s "https://api.storekeeper.me/api/reports/product-sales?from=2026-06-01&to=2026-06-30&location_id=17&limit=25" \
-H "Authorization: Bearer $TOKEN"
Twee directe toepassingen: assortiment snoeien (alles onder rang 300 van 412 met ook nog dunne marge is een kandidaat om te schrappen) en schappenplan (je aantallen-top-10 hoort op ooghoogte en bij de kassa). total en distinct_products vertellen hoe lang de staart is; verhoog limit (tot 500) om hem te zien.
# de lange staart: hoeveel van het assortiment verkoopt nauwelijks?
curl -s "https://api.storekeeper.me/api/reports/product-sales?from=2026-06-01&to=2026-06-30&limit=500" \
-H "Authorization: Bearer $TOKEN" | jq '[.data[] | select(.revenue_incl_vat < 50)] | length'
quantity als revenue_incl_vat, dus een product kan zelfs negatief uitkomen. Dat is correct voor reconciliatie, maar houd het in gedachten als je "verkochte stuks" als loop leest.