Recipes / Stock level monitor
InventoryWatch stock per product per location, and understand the difference between a location and a shop before it bites.
The read model behind our shortage planner.
Stock is counted per physical site, so everything starts with a location id. List the locations once and cache the mapping; the ids are stable. A location (physical site) and a shop (sales channel) live in different id spaces: /api/stock filters by location_id, never by a shop id.
curl -s https://api.storekeeper.me/api/locations -H "Authorization: Bearer $TOKEN"
location_id. The numbers look similar, both exist, and a wrong-but-existing id returns rows for the wrong site instead of an error. Resolve ids from /api/locations only.Scope with location_id plus a comma-separated product_ids list. Find the product ids first via catalog search; the id in a search row is the product id that stock rows carry.
# find the products (name or SKU search)
curl -s "https://api.storekeeper.me/api/products?q=croissant&lang=nl" \
-H "Authorization: Bearer $TOKEN"
# stock for those products at location 14
curl -s "https://api.storekeeper.me/api/stock?location_id=14&product_ids=812,813,957" \
-H "Authorization: Bearer $TOKEN"
{
"start": 0,
"limit": 100,
"count": 3,
"total": 3,
"data": [
{ "id": 40211, "shop_product_id": 5031, "product_id": 812, "location_id": 14,
"orderable_stock_value": 24, "unfulfilled_stock_value": 0,
"value": 24, "in_stock": true, "unlimited": false },
{ "id": 40212, "shop_product_id": 5032, "product_id": 813, "location_id": 14,
"orderable_stock_value": 3, "unfulfilled_stock_value": 2,
"value": 5, "in_stock": true, "unlimited": false },
{ "id": 40388, "shop_product_id": 5177, "product_id": 957, "location_id": 14,
"orderable_stock_value": 0, "unfulfilled_stock_value": 0,
"value": 0, "in_stock": false, "unlimited": false }
]
}
value is the raw counted stock; orderable_stock_value is what can still be sold (counted minus what open orders already claim, the unfulfilled_stock_value). For "can I sell this now" alerts, watch orderable_stock_value. unlimited: true means stock is not tracked for that product; skip it in alerting.
product_ids (empty, or non-numeric junk like abc) returns a 400 with an error body, not a silently unfiltered full-catalog result. Treat a 400 here as a bug in your id list, not as "no stock".For a nightly snapshot of everything at one site, drop product_ids and drain the pages with start/limit (max 500 per page) until you have total rows. The same drain pattern works on /api/products: omit q and it lists the whole catalog paginated (max 200 per page), which gives you titles and SKUs to join onto the stock rows by product_id.
# page 1, page 2, ... until start >= total
curl -s "https://api.storekeeper.me/api/stock?location_id=14&start=0&limit=500" \
-H "Authorization: Bearer $TOKEN"
curl -s "https://api.storekeeper.me/api/stock?location_id=14&start=500&limit=500" \
-H "Authorization: Bearer $TOKEN"
# names + SKUs to join on product_id (q omitted = full catalog)
curl -s "https://api.storekeeper.me/api/products?start=0&limit=200" \
-H "Authorization: Bearer $TOKEN"
Keep it boring and reliable:
Threshold per product, not global. Five croissants is a crisis; five wedding cakes is a warehouse. Store a reorder point per product_id and alert when orderable_stock_value drops to or below it.
Poll, do not hammer. Every 5 to 15 minutes is plenty for shelf alerting; once per night for the full-catalog sweep. Scope polls with product_ids to your watchlist so each poll is one cheap call.
Compare snapshots, alert on the crossing. Keep the previous poll's values and fire only when a product crosses its threshold downward. Alerting on the absolute state every poll spams the same message every cycle; alerting on the transition fires once.
# the alert condition, per watched product
prev.orderable_stock_value > threshold[product_id]
curr.orderable_stock_value <= threshold[product_id]
curr.unlimited == false
Recepten / Voorraadmonitor
InventoryVolg voorraad per product per locatie, en begrijp het verschil tussen een locatie en een winkel voordat het je raakt.
Het leesmodel achter onze tekortenplanner.
Voorraad wordt per fysieke vestiging geteld, dus alles begint met een locatie-id. Haal de locaties één keer op en cache de mapping; de ids zijn stabiel. Een locatie (fysieke vestiging) en een winkel (verkoopkanaal) leven in verschillende id-ruimtes: /api/stock filtert op location_id, nooit op een winkel-id.
curl -s https://api.storekeeper.me/api/locations -H "Authorization: Bearer $TOKEN"
location_id. De nummers lijken op elkaar, beide bestaan, en een fout-maar-bestaand id geeft rijen voor de verkeerde vestiging terug in plaats van een foutmelding. Haal ids alleen uit /api/locations.Beperk met location_id plus een kommagescheiden product_ids-lijst. Zoek eerst de product-ids op via catalogus-zoeken; de id in een zoekrij is het product-id dat de voorraadrijen dragen.
# zoek de producten (op naam of SKU)
curl -s "https://api.storekeeper.me/api/products?q=croissant&lang=nl" \
-H "Authorization: Bearer $TOKEN"
# voorraad voor die producten op locatie 14
curl -s "https://api.storekeeper.me/api/stock?location_id=14&product_ids=812,813,957" \
-H "Authorization: Bearer $TOKEN"
value is de rauwe getelde voorraad; orderable_stock_value is wat je nog kunt verkopen (geteld minus wat openstaande orders al claimen, de unfulfilled_stock_value). Voor "kan ik dit nu verkopen"-alerts kijk je naar orderable_stock_value. unlimited: true betekent dat voorraad voor dat product niet wordt bijgehouden; sla het over in je alerting.
product_ids (leeg, of niet-numerieke rommel zoals abc) geeft een 400 met een foutmelding terug, niet stilletjes een ongefilterd volledig-catalogusresultaat. Behandel een 400 hier als een bug in je id-lijst, niet als "geen voorraad".Voor een nachtelijke snapshot van alles op één vestiging laat je product_ids weg en loop je de pagina's af met start/limit (max 500 per pagina) tot je total rijen hebt. Hetzelfde patroon werkt op /api/products: laat q weg en je krijgt de hele catalogus gepagineerd (max 200 per pagina), met titels en SKU's om via product_id aan de voorraadrijen te koppelen.
# pagina 1, pagina 2, ... tot start >= total
curl -s "https://api.storekeeper.me/api/stock?location_id=14&start=0&limit=500" \
-H "Authorization: Bearer $TOKEN"
curl -s "https://api.storekeeper.me/api/stock?location_id=14&start=500&limit=500" \
-H "Authorization: Bearer $TOKEN"
# namen + SKU's om op product_id te koppelen (q weggelaten = hele catalogus)
curl -s "https://api.storekeeper.me/api/products?start=0&limit=200" \
-H "Authorization: Bearer $TOKEN"
Houd het saai en betrouwbaar:
Drempel per product, niet globaal. Vijf croissants is een crisis; vijf bruidstaarten is een magazijn. Bewaar een bestelpunt per product_id en alerteer wanneer orderable_stock_value op of onder dat punt zakt.
Pollen, niet hameren. Elke 5 tot 15 minuten is ruim voldoende voor schap-alerts; één keer per nacht voor de volledige catalogus. Beperk polls met product_ids tot je watchlist, dan is elke poll één goedkope aanroep.
Vergelijk snapshots, alerteer op de overgang. Bewaar de waarden van de vorige poll en vuur alleen wanneer een product zijn drempel neerwaarts kruist. Alerteren op de absolute stand spamt elke cyclus hetzelfde bericht; alerteren op de overgang vuurt één keer.
# de alertconditie, per gevolgd product
prev.orderable_stock_value > threshold[product_id]
curr.orderable_stock_value <= threshold[product_id]
curr.unlimited == false