Recipes / Discount audit

Orders

Discount audit

Find every order that carried a discount in a period: who gave it, on what, and how much margin it cost.

GET/api/discount-orders GET/api/orders/{id}/items

Extracted from our discount-orders support tool.

List the discounted orders for the period

One call returns every order in the date range that carries a non-zero discount. from is required; to defaults to from, both inclusive, Europe/Amsterdam. Money is decimal euros; every _wt field is incl. VAT, its counterpart excl. VAT.

# all discounted orders in June
curl -s "https://api.storekeeper.me/api/discount-orders?from=2026-06-01&to=2026-06-30" \
  -H "Authorization: Bearer $TOKEN"
{
  "from": "2026-06-01",
  "to": "2026-06-30",
  "shop_id": null,
  "only_full": false,
  "include_negative": false,
  "count": 2,
  "total": 2,
  "data": [
    { "id": 90412, "order_number": "20260614-0412", "date_purchased": "2026-06-14T11:42:10+02:00",
      "status": "complete", "shop_id": 3, "currency": "EUR",
      "value_wt": 42.30, "value_ex_wt": 38.81,
      "discount_value_wt": 4.70, "discount_value_ex_wt": 4.31,
      "customer_name": "Anouk Bakker", "customer_email": "anouk.bakker@example.nl",
      "backoffice_url": "https://mybakery.storekeepercloud.com/#order/details/90412" },
    { "id": 90598, "order_number": "20260621-0598", "date_purchased": "2026-06-21T15:03:44+02:00",
      "status": "complete", "shop_id": 5, "currency": "EUR",
      "value_wt": 0, "value_ex_wt": 0,
      "discount_value_wt": 24.95, "discount_value_ex_wt": 20.62,
      "customer_name": "S. van Dijk", "customer_email": "",
      "backoffice_url": "https://mybakery.storekeepercloud.com/#order/details/90598" }
  ]
}

value_wt is what the customer actually paid after the discount; discount_value_wt is what was given away. Each row carries a ready-to-click backoffice_url straight to the order, which is what makes this an audit tool: every suspicious number is one click from its source. Pass shop_id to scope to one sales channel.

Understand the two flags

Two refinements change what counts as "discounted":

only_full=1 keeps only orders where the entire amount was discounted (value_wt is 0). This is the fraud-and-friends filter: 100% discounts are staff comps, test orders, or someone ringing up freebies. The second row above would survive it; the first would not.

include_negative=1 widens the base filter from "discount greater than zero" to "discount not zero", which also catches negative discounts: refund-like corrections. Off by default, so a plain call shows giveaways only.

# every 100%-discounted order in June, one shop
curl -s "https://api.storekeeper.me/api/discount-orders?from=2026-06-01&to=2026-06-30&shop_id=3&only_full=1" \
  -H "Authorization: Bearer $TOKEN"

# the complete picture including corrections
curl -s "https://api.storekeeper.me/api/discount-orders?from=2026-06-01&to=2026-06-30&include_negative=1" \
  -H "Authorization: Bearer $TOKEN"
Gotcha: when you enable include_negative, do not blindly sum discount_value_wt anymore. Positive and negative rows will partially cancel and understate how much discounting actually happened. Sum the positives and negatives separately and report both.

Drill into one order's items

To see what was discounted, fetch the order's line items. Every line has a kind: product, shipping, payment or discount. An order-level discount shows up as its own kind: "discount" line with a negative price; a per-product discount shows up on the product line itself, where ppu_wt × quantity is more than the charged price_wt.

curl -s https://api.storekeeper.me/api/orders/90412/items -H "Authorization: Bearer $TOKEN"
{
  "order_id": 90412,
  "count": 3,
  "truncated": false,
  "data": [
    { "id": 512001, "order_id": 90412, "kind": "product", "sku": "BROOD-VOLKOREN",
      "name": "Volkorenbrood", "quantity": 4, "ppu": 2.98, "ppu_wt": 3.25,
      "price": 11.92, "price_wt": 13.00, "tax_rate_id": 52,
      "product_id": 812, "shop_product_id": 5031, "pickup_date": null },
    { "id": 512002, "order_id": 90412, "kind": "product", "sku": "TAART-APPEL",
      "name": "Appeltaart groot", "quantity": 2, "ppu_wt": 17.00,
      "ppu": 15.60, "price": 28.72, "price_wt": 31.30, "tax_rate_id": 52,
      "product_id": 957, "shop_product_id": 5177, "pickup_date": null },
    { "id": 512003, "order_id": 90412, "kind": "discount", "sku": null,
      "name": "Kortingscode ZOMER10", "quantity": 1, "ppu": -1.83, "ppu_wt": -2.00,
      "price": -1.83, "price_wt": -2.00, "tax_rate_id": 52,
      "product_id": null, "shop_product_id": null, "pickup_date": null }
  ]
}

Both mechanisms are visible here: line 512002 sells two 17.00 taarten for 31.30 instead of 34.00 (a product-line discount of 2.70), and line 512003 subtracts another 2.00 order-wide. Together they explain the header's discount_value_wt of 4.70. The discount line's name usually tells you the mechanism (a coupon code, a manual POS discount).

Aggregate, then act

With the rows in hand, three aggregates answer most audit questions:

Total discount per period and per shop. Sum discount_value_wt grouped by shop_id and compare against the same period last month. A jump at one shop and not the others is a local habit, not a campaign.

Biggest single discounts. Sort by discount_value_wt descending and eyeball the top ten via their backoffice_url. Large one-offs are where mistakes and abuse live.

The 100% list. Run only_full=1 monthly and expect to recognise every row (staff meals, samples, replacements). Any row you cannot explain is exactly what this recipe exists for.

# the monthly audit set, in three calls
per_shop_total  = sum(discount_value_wt) group by shop_id
top_10          = sort by discount_value_wt desc, take 10
full_discounts  = ?only_full=1 rows, reviewed by a human