Recipes / Price list export

Catalog

Price list export

Export the catalog with current base prices: search or drain products, then join the price rows.

GET/api/products GET/api/product-prices GET/api/tax-rates

The read side of our bulk price tooling.

Drain the catalog, page by page

Omit q and GET /api/products lists the whole catalog. The response is the standard pagination envelope: count is the size of this page, total is the size of the catalog. Loop with start and limit until start reaches total. The limit caps at 200; 200 is a fine page size for a full export.

# first page of the full catalog (no q = match all)
curl -s "https://api.storekeeper.me/api/products?start=0&limit=200" \
  -H "Authorization: Bearer $TOKEN"

# then start=200, start=400, ... while start < total
{
  "q": "",
  "start": 0,
  "limit": 200,
  "count": 200,
  "total": 1418,
  "data": [
    { "id": 8211, "sku": "BROOD-VOLKOREN", "title": "Volkorenbrood heel", "product_group_id": 3, "stock": 24.0, "shop_product_id": 9152 },
    { "id": 8214, "sku": "CROIS-ROOMB", "title": "Roomboter croissant", "product_group_id": 4, "stock": 112.0, "shop_product_id": 9155 }
  ]
}
Gotcha: a product carries two ids. id is the inner product id, and it is the one the price rows join on. shop_product_id is the shop-level key. Keep both in your export state, but join prices on id.

Fetch the price rows for each page's ids

For every catalog page, collect the id values and pass them comma-separated as product_ids to GET /api/product-prices. Chunk the ids to match your page size (up to 200 ids fits comfortably in one call; the endpoint's own limit goes up to 500 rows, and a product can have more than one price row). Join each row back to its product via product_id.

curl -s "https://api.storekeeper.me/api/product-prices?product_ids=8211,8214&limit=100" \
  -H "Authorization: Bearer $TOKEN"
{
  "start": 0,
  "limit": 100,
  "count": 3,
  "total": 3,
  "data": [
    { "id": 30112, "product_id": 8211, "product_price_type_id": 1, "is_sale_price": true,
      "ppu": 2.94, "ppu_wt": 3.20, "tax_rate_id": 52, "currency": "EUR", "from_qty": null },
    { "id": 30113, "product_id": 8211, "product_price_type_id": 3, "is_sale_price": false,
      "ppu": 1.10, "ppu_wt": 1.20, "tax_rate_id": 52, "currency": "EUR", "from_qty": null },
    { "id": 30140, "product_id": 8214, "product_price_type_id": 1, "is_sale_price": true,
      "ppu": 1.28, "ppu_wt": 1.40, "tax_rate_id": 52, "currency": "EUR", "from_qty": null }
  ]
}

Keep only the rows with is_sale_price: true (that is product_price_type_id = 1); the other type ids are cost and purchase rows you do not want in a customer-facing price list. ppu_wt is the price including VAT, ppu excluding.

Gotcha: a typo in product_ids (a stray letter, an empty list after trimming) returns a 400, never a silently-full catalog. That is deliberate: a malformed filter that quietly falls back to "everything" is how exports end up with 1,400 rows where 200 were expected. Treat the 400 as a bug in your id string, fix it, and do not retry as-is.
Gotcha: these prices are the base catalog. Customer-segment pricelists are not exposed through this API, so a segment-specific price a customer sees at checkout can differ from what you export here.

Resolve the tax rates once

Each sale price row carries a tax_rate_id. One reference call turns that into a percentage; cache the mapping for the whole run.

curl -s "https://api.storekeeper.me/api/tax-rates?country_iso2=NL" \
  -H "Authorization: Bearer $TOKEN"
{
  "count": 3,
  "total": 3,
  "data": [
    { "id": 51, "name": "0%",  "value": 0,  "country_iso2": "NL", "alias": "special_zero" },
    { "id": 52, "name": "9%",  "value": 9,  "country_iso2": "NL", "alias": "special_low" },
    { "id": 55, "name": "21%", "value": 21, "country_iso2": "NL", "alias": "standard" }
  ]
}
Gotcha: /api/tax-rates without country_iso2 returns the whole EU registry, Austria first. Always filter.

Write the export file

One row per product, joined from the three sources. The recommended columns:

ColumnSource
skuproduct row, sku
titleproduct row, title
price incl. VATsale price row, ppu_wt
price excl. VATsale price row, ppu
VAT percenttax_rate_id resolved to value via /api/tax-rates

Use both ppu and ppu_wt as given; never derive one from the other with your own VAT arithmetic, or your export will disagree with the till by a cent. A product without a sale price row is normal (never priced yet): export it with empty price columns rather than dropping it, so the gap is visible.

For refreshes, simply re-run the whole drain. The export is a point-in-time snapshot; there is no delta feed for prices, and at catalog sizes of a few thousand products a full re-run takes seconds. Schedule it nightly, or run it on demand before a print run.