{"openapi":"3.0.0","info":{"title":"Storekeeper API","description":"The **public API of Storekeeper**: a clean, token-authenticated REST surface over the\nretail \u0026 hospitality commerce platform (POS, webshop, back-office, inventory,\ninvoicing) used by shops, bakeries, and franchises.\n\n**This is the only supported way to connect to Storekeeper.** No other integration\nmethod is recommended or allowed. Your subscription must cover both the volume of\ncalls you make and the scope you access.\n\n\u2192 **New here? Read the [Guide \u0026 use cases](\/guide) first.**\n\n## What you can do with it\n- **Accounting \/ BI**: structured financial report, VAT breakdowns, payment\n  reconciliation, daily close (Z-report), per-product sales. (Drives exports like AFAS.)\n- **Orders**: list\/search orders, read one order with addresses, and its line items.\n- **Catalog \u0026 stock**: search products, read a product, list prices, per-location stock.\n- **CRM**: search customers, read a customer, list customer segments.\n- **Reference data**: resolve the ids reports emit: shops, tax rates, turnover\n  (product) groups, payment methods, locations.\n\n## Getting started\n1. Ask your Storekeeper administrator for an **API key**. They create one in the\n   Storekeeper app and give you a `client_id` and a `client_secret`.\n2. Exchange those for an access token at **your own account\u0027s** OAuth endpoint \u2014\n   `client_credentials`, RFC 6749 \u00a74.4, which every stock OAuth client library\n   already speaks. The endpoint is discoverable at\n   `https:\/\/api-\u003Caccount\u003E.storekeepercloud.com\/.well-known\/oauth-authorization-server`,\n   and your `client_id` (`sk_ak_\u003Caccount\u003E.\u003Ckey id\u003E`) names the account to look up.\n   **That exchange happens at Storekeeper, not here**: this API never sees your\n   secret and never brokers it.\n3. Send `Authorization: Bearer \u003Caccess_token\u003E` on every request\n   (click **Authorize** above). **It has to be a token minted from an API key.**\n   A token minted for a *browser session* \u2014 what a merchant gets by opening this\n   app from their Storekeeper backoffice \u2014 is refused on every `\/api` route with a\n   `401` that names this exchange, whatever role it carries. There is no role that\n   makes a session token work here; there is only a key.\n4. `GET \/api\/me` tells you who the token belongs to, and which capabilities the\n   key\u0027s role actually holds \u2014 ask it rather than discovering your permissions by\n   collecting 403s.\n5. When `expires_in` runs out, exchange the key again. **There is no refresh\n   endpoint here and you do not need one**: the exchange is cheap, repeatable and\n   consumes nothing. A 401 on the exchange itself means the key was revoked or\n   rolled, not that it expired. There is no password endpoint\n   (`POST \/api\/auth\/login`, removed 2026-09) and no read-only demo token\n   (`POST \/api\/auth\/demo`, withdrawn 2026-09).\n\n## Conventions\n- **Dates** are `YYYY-MM-DD` in Europe\/Amsterdam; ranges are inclusive.\n- **Money** is decimal euros. `_wt` = incl. VAT (Dutch *met*), plain \/ `_ex` = excl. VAT.\n- **Lists** return `{count, total, data}`; paginated lists add `{start, limit}`, so you drive paging.\n- **Errors** are JSON `{error, message, status}`: `400` bad input \u00b7 `401` auth \u00b7\n  `404` not found \u00b7 `429` over the rate limit \u00b7 `502` upstream Storekeeper error.\n\n## Limits\nYour request volume is counted against the **API key** your token was minted from \u2014\nper key, per minute. One key is for one integration: sharing a key across a fleet of\nworkers spends a single budget between them, so adding workers on the same key makes\nyou slower, not faster.\n\n- **Writes are limited separately and more tightly than reads.** A `POST`, `PUT`,\n  `PATCH` or `DELETE` is charged against both the write counter and the general one,\n  so a job that reads a lot and writes a little meets the write ceiling first.\n- **Past a ceiling the answer is `429 Too Many Requests`**, with `Retry-After` in\n  seconds. Sleep for that long and continue. Retrying hot only spends the next\n  window on refusals too.\n- **Every response to an authenticated call carries the numbers**:\n  `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the\n  window rolls). You do not have to be refused to find out where you stand \u2014 read\n  `RateLimit-Remaining` and slow down before it reaches zero. The three always\n  describe **one** ceiling: whichever is closest to refusing you next. On a write\n  that is usually the write ceiling, so the number drops when you write and rises\n  again when you read \u2014 it steps between two ceilings rather than counting down as\n  one budget. Read each response\u0027s trio on its own; do not infer your rate from the\n  difference between two of them.\n- **The same budget is published a second time in the newer IETF Structured Fields\n  form**, beside those three and never instead of them. `RateLimit-Policy` names\n  **every** ceiling that applies to your key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with\n  `q` (quota) and `w` (window in seconds) \u2014 and is built from configuration alone,\n  so it is identical on every response: a `GET` advertises the write policy too,\n  which is how a client that has only ever read learns a tighter write ceiling\n  exists before its first write. `RateLimit` then reports the state of exactly\n  **one** policy \u2014 the binding one, the one that will refuse you next \u2014 and says\n  **which**, with `r` and `t` carrying the same two numbers as `RateLimit-Remaining`\n  and `RateLimit-Reset`. That name is what makes the step between two ceilings\n  above attributable instead of a mystery. On a `429`, `RateLimit` names the policy\n  that refused you, with `r=0`.\n- **No figure is published here on purpose.** The ceilings are deployment\n  configuration, so a number printed in this document would be wrong the day one is\n  tuned. Size a bulk sync from `RateLimit-Limit` on a single call rather than from a\n  rate you copied out of documentation.\n\n## Resolving ids\nReports emit raw Storekeeper ids; resolve them with the reference endpoints:\n\n| id emitted by reports | resolver endpoint |\n| --- | --- |\n| `shop_id` | `GET \/api\/shops` |\n| `tax_rate_id` | `GET \/api\/tax-rates` |\n| `product_group_id` | `GET \/api\/turnover-groups` |\n| `provider_method_type_id` | `GET \/api\/payment-methods` |\n| `location_id` | `GET \/api\/locations` |\n","version":"1.0.0"},"servers":[{"url":"https:\/\/api-dev.storekeeper.software","description":"Production"}],"paths":{"\/api\/customers":{"get":{"tags":["Customers"],"summary":"List \/ search customers","description":"Lists customer relations (`ShopModule.listCustomerRelations`). Pass `q` to\nsearch by name\/email\/etc. (`search__word`), or `segment_id` to list the members\nof a customer segment (`customer_segment_id__has_=`, see `\/api\/customer-segments`).\n\nThe row `id` **is** the `relation_data_id` used elsewhere (e.g. an order\u0027s\n`relation_data_id`), so this endpoint resolves that id to a customer.","operationId":"searchCustomers","parameters":[{"name":"q","in":"query","description":"Free-text search (name, email, \u2026).","required":false,"schema":{"type":"string"}},{"name":"segment_id","in":"query","description":"Only customers in this segment.","required":false,"schema":{"type":"integer"}},{"name":"start","in":"query","required":false,"schema":{"type":"integer","default":0}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":500}}],"responses":{"200":{"description":"Paginated list of customers","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/CustomerList"}}}},"401":{"description":"Unauthorized"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/customers\/{id}":{"get":{"tags":["Customers"],"summary":"Get a single customer by id","description":"Single customer relation (`ShopModule.listCustomerRelations` filtered by id). The `id` is the `relation_data_id`. Includes contact details and address.","operationId":"getCustomer","parameters":[{"name":"id","in":"path","description":"Customer **relation id** \u2014 the `id` of a `GET \/api\/customers` row, and the same namespace an order carries as `relation_data_id`.","required":true,"schema":{"type":"integer","pattern":"\\d+"}}],"responses":{"200":{"description":"Single customer","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/CustomerDetail"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Customer not found"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/customer-segments":{"get":{"tags":["Customers"],"summary":"List customer segments","description":"Lists the customer segments configured on the account\n(`ShopModule.listCustomerSegments`). A segment groups customers and can bind a\nprice list. The `id` joins to `\/api\/customers?segment_id=` and to an order\u0027s\ncustomer-segment filter. `customer_count` and `product_prices_count` summarise\nthe segment\u0027s size and how many custom prices it carries.","operationId":"listCustomerSegments","responses":{"200":{"description":"Array of customer segments, alphabetical by name","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/CustomerSegmentList"}}}},"401":{"description":"Unauthorized"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/discount-orders":{"get":{"tags":["Discount Orders"],"summary":"List orders with a non-zero discount for a date range","description":"Returns every order where `discount_value_wt \u003E 0` (or `!= 0` if `include_negative` is set)\nwithin the given date range. Paginates the upstream `ShopModule.listOrders` call in batches\nof 100 and flattens the result into a tidy list. Each row carries a ready-to-click\n`backoffice_url` pointing at the order in the user\u0027s Storekeeper Backoffice.\n\nFilters:\n- `shop_id` narrows to a single sales channel. Omit for all shops.\n- `only_full` = 100%-discount orders only (adds `value_wt__= 0` to the upstream filter).\n- `include_negative` flips the base filter from `__\u003E 0` to `__!= 0`, which also catches\n  negative discounts (refund-like corrections).","operationId":"listDiscountOrders","parameters":[{"name":"from","in":"query","description":"Start date (YYYY-MM-DD), inclusive","required":true,"schema":{"type":"string","format":"date"}},{"name":"to","in":"query","description":"End date (YYYY-MM-DD), inclusive. Defaults to `from`.","required":false,"schema":{"type":"string","format":"date"}},{"name":"shop_id","in":"query","description":"Filter to a single sales channel (shop). Omit for all.","required":false,"schema":{"type":"integer"}},{"name":"only_full","in":"query","description":"Only return orders where the full amount is discounted (value_wt = 0).","required":false,"schema":{"type":"boolean"}},{"name":"include_negative","in":"query","description":"Also include negative-discount rows (refunds\/corrections). Default: only positive discounts.","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Array of discounted orders","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DiscountOrderList"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Your role carries no usable location binding, so this report covers nothing"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/financial-report":{"get":{"tags":["Financial Report"],"summary":"Structured financial report for a date range","description":"Returns a structured report for the given date range. Response sections:\n\n- **revenue** \u2014 `total_sales`, `total_returns`, `total_differences`, `total_turnover` (sales minus returns and differences), `total_cost_of_goods_sold`, `margin` (value and percentage), and `non_revenue` (gift cards, tips, payments on invoice, prepayments).\n- **direct_paid** \u2014 per-rate breakdown and grand total for what was paid directly, a list of payment methods used with friendly names and amounts, plus a `balance_check` that verifies the payments sum matches the total.\n- **not_paid_now** \u2014 per-rate breakdown and total for invoiced amounts that are still unpaid.\n- **turnover_by_group** \u2014 rows per product group with a per-shop split, per-shop totals, and an overall total. When `location_id` is passed, the result is filtered to the shop(s) that belong to that location.","operationId":"getFinancialReport","parameters":[{"name":"from","in":"query","description":"Start date (YYYY-MM-DD), inclusive","required":true,"schema":{"type":"string","format":"date"}},{"name":"to","in":"query","description":"End date (YYYY-MM-DD), inclusive. Defaults to `from`.","required":false,"schema":{"type":"string","format":"date"}},{"name":"location_id","in":"query","description":"Limit to a single location. Omit for all.","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Structured financial report","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/FinancialReport"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Your location scope cannot be computed as one location \u2014 several ids or an open `except` set (add `location_id` naming one), or a binding that covers no location at all"},"404":{"description":"The requested location is outside your scope"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/locations":{"get":{"tags":["Locations"],"summary":"List all locations","description":"The locations configured on the Storekeeper account\n(`RelationsModule.listLocations`). Each row\u0027s `id` is a **location id** \u2014 the\nsame namespace as `\/api\/shops` rows\u0027 `location_id`, as the `location_id` query\nparameter of `\/api\/stock` and `\/api\/reports\/*`, and as the ids a caller\u0027s\nlocation scope is declared in. It is unrelated to the product-side id\nnamespaces.\n\n**Scoped callers see only their own locations.** The binding is pushed into the\nupstream query where it is an enumerable list, and every returned row is\nfiltered against it either way, so `count`\/`total` describe what *you* may see\nand never the account-wide figure.\n\nThe route is unpaged: it asks upstream for a single page of 500 and emits what\ncomes back. `total` is therefore always equal to `count`.","operationId":"listLocations","responses":{"200":{"description":"Locations envelope. Each row is a projection of the upstream\n`RelationsModule` location, including its nested `address` \u2014 which is the\nonly place a location\u0027s country is reported, and is why the older contract\npassed the row through untouched rather than projecting it. Both captured\npayloads carry exactly these fields; an upstream field outside them is no\nlonger forwarded.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/LocationList"}}}},"401":{"description":"Unauthorized"},"403":{"description":"The caller\u0027s role does not hold `location.list`"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/locations\/{id}":{"get":{"tags":["Locations"],"summary":"Get a single location by id","description":"One location (`RelationsModule.getLocation`), projected field for field \u2014 the same\nrow shape `GET \/api\/locations` emits, see that operation for the fields.\n\n`{id}` is a **location id**: the namespace the `location` scope binding is\ndeclared in and the one `\/api\/shops` rows carry as `location_id`. It is not a\nproduct-side id.\n\nA location outside the caller\u0027s assignment scope answers a plain 404, **byte for\nbyte identical** to a location that does not exist \u2014 so the route cannot be used\nto enumerate the locations you were not given.","operationId":"getLocation","parameters":[{"name":"id","in":"path","description":"Location id (the `id` of a `\/api\/locations` row)","required":true,"schema":{"type":"integer","pattern":"\\d+"}}],"responses":{"200":{"description":"A single location","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Location"}}}},"401":{"description":"Unauthorized"},"403":{"description":"The caller\u0027s role does not hold `location.view`"},"404":{"description":"Not found, or outside your location scope \u2014 the two are indistinguishable"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/me":{"get":{"tags":["Auth"],"summary":"Introspect the current bearer token (whoami)","description":"Resolves the bearer token to the account it authenticates, the token\u0027s\nremaining lifetime, and \u2014 best-effort \u2014 the Storekeeper user behind it\n(`RelationsModule.getMyProfile`): identity, per-module roles, and the store\nthe user is locked to (`locked_location`, null for backoffice\/account-wide).\n\nThe `account`, `subaccount`, `token` and `authorization` blocks always resolve\nfrom the token itself. If the upstream profile lookup fails, `user`\/`roles`\/\n`locked_location` are null and the reason is surfaced in `profile_error`\n(never silently dropped).\n\n`authorization` is the **role document**: the caller\u0027s role, the capabilities\nit holds, the resource types those capabilities must be bound by\n(`requires_bindings`) and the bindings themselves. Ask here rather than\ndiscovering your permissions by collecting 403s. An empty `capabilities`\nmap is a real answer: a role that holds nothing.","operationId":"me","responses":{"200":{"description":"Token + user identity","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Me"}}}},"401":{"description":"Unauthorized"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."}}}},"\/api\/orders":{"get":{"tags":["Orders"],"summary":"List orders for a date range","description":"Lists orders (`ShopModule.listOrders`) purchased within the given date range,\nnewest first. Unlike `\/api\/discount-orders` this is a general, paginated list \u2014\nit returns one page plus a `total`, so the caller drives pagination via `start`\nand `limit` rather than the server draining every page.\n\nFilters (all optional, AND-combined):\n- `shop_id` \u2014 single sales channel (see `\/api\/shops`).\n- `status` \u2014 comma-separated list (e.g. `new,complete,shipped`). Omit for all statuses.\n- `is_internal` \u2014 `1` internal orders only, `0` exclude internal. Omit for the Storekeeper default.\n- `is_paid` \u2014 `1`\/`0`.\n- `segment_id` \u2014 customer segment id (`customer_segment_id__in_list`).\n\nEach row carries a ready-to-click `backoffice_url`.","operationId":"listOrders","parameters":[{"name":"from","in":"query","description":"Start date (YYYY-MM-DD), inclusive","required":true,"schema":{"type":"string","format":"date"}},{"name":"to","in":"query","description":"End date (YYYY-MM-DD), inclusive. Defaults to `from`.","required":false,"schema":{"type":"string","format":"date"}},{"name":"shop_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"status","in":"query","description":"Comma-separated status list. Omit for all.","required":false,"schema":{"type":"string"}},{"name":"is_internal","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"is_paid","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"segment_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"start","in":"query","required":false,"schema":{"type":"integer","default":0}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":500}}],"responses":{"200":{"description":"Paginated list of orders","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/OrderList"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/orders\/delta":{"get":{"tags":["Orders"],"summary":"Incrementally sync orders changed since a timestamp","description":"Delta feed over orders, keyed on `date_updated` (`ShopModule.listOrders` with a\n`date_updated__\u003E=` filter, sorted by `date_updated` then `id`, both ascending).\nStart with `since` (ISO-8601, or YYYY-MM-DD interpreted as midnight\nEurope\/Amsterdam), then follow `next_cursor` on every subsequent call; keep\npolling with the last cursor to pick up new changes. Pass exactly one of\n`since` or `cursor`.\n\n**At-least-once delivery:** a row can appear on more than one page (the cursor\nre-enters at the last seen timestamp), so consumers must dedupe or upsert on\n`id`. Rows whose `date_updated` is null (never touched since creation on some\naccounts) never match the filter and only appear in full listings such as\n`GET \/api\/orders`. `date_updated` is bumped by order and order-item changes,\nbut not necessarily by every payment or shipment mutation.\n\nResponse: `{count, data, next_cursor, has_more}`. `has_more` is true when the\npage is full; an empty page returns the incoming cursor unchanged.","operationId":"listOrderDelta","parameters":[{"name":"since","in":"query","description":"ISO-8601 timestamp or YYYY-MM-DD (midnight Europe\/Amsterdam). Mutually exclusive with `cursor`.","required":false,"schema":{"type":"string"}},{"name":"cursor","in":"query","description":"Opaque cursor from a previous response. Mutually exclusive with `since`.","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":250}}],"responses":{"200":{"description":"One delta page with a follow-up cursor","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/OrderDelta"}}}},"400":{"description":"Missing\/conflicting since+cursor, or a malformed value"},"401":{"description":"Unauthorized"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/orders\/{id}":{"get":{"tags":["Orders"],"summary":"Get a single order by id","description":"Full order (`ShopModule.getOrder`): header fields plus lightly-shaped billing and shipping addresses. Line items live at `\/api\/orders\/{id}\/items`.","operationId":"getOrder","parameters":[{"name":"id","in":"path","description":"Storekeeper **order id** \u2014 the `id` of a `GET \/api\/orders` row. Not the human-facing `order_number`.","required":true,"schema":{"type":"integer","pattern":"\\d+"}}],"responses":{"200":{"description":"Single order with addresses","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/OrderWithAddresses"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Order not found"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/orders\/{id}\/items":{"get":{"tags":["Orders"],"summary":"List line items for an order","description":"Line items for a single order (`ShopModule.listOrderItems`, filtered by `order_id`). Paginated upstream and flattened; product lines and non-product lines (shipping, payment, discount) are all returned with `kind` set accordingly.","operationId":"listOrderItems","parameters":[{"name":"id","in":"path","description":"Storekeeper **order id** \u2014 the `id` of a `GET \/api\/orders` row. Not the human-facing `order_number`.","required":true,"schema":{"type":"integer","pattern":"\\d+"}}],"responses":{"200":{"description":"Array of order line items","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/OrderItemList"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Order not found, or outside your location scope \u2014 the two are deliberately indistinguishable. The route places the order before reading its lines, so a missing order is a 404 here and not an empty list."},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/payment-methods":{"get":{"tags":["Payment Methods"],"summary":"List payment method types","description":"Lists the payment method types registered on the Storekeeper account\n(`PaymentModule.listProviderMethodTypes`). Each `provider_method_type_id`\njoins directly to the financial report\u0027s `payments[].provider_method_type_id`,\nso the two reconcile 1:1. The numeric ids are registered per account, so the\nstable identifier is `type_alias` (e.g. \u0022Cash\u0022, \u0022TerminalDevice\u0022).","operationId":"listPaymentMethods","responses":{"200":{"description":"Array of payment method types, alphabetical by name","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/PaymentMethodList"}}}},"401":{"description":"Unauthorized"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/products":{"get":{"tags":["Products"],"summary":"Search the catalog","description":"Full-text search over the shop catalog (`ShopModule.naturalSearchShopFlatProducts`).\nPass `q` to search by name\/SKU; omit it to list the whole catalog (the upstream\n\u0022match all\u0022 query). Each row resolves to the sellable product: `id`, `sku`,\n`title`, `product_group_id` (joins to `\/api\/turnover-groups`) and a rolled-up\n`stock` value. Prices live at `\/api\/product-prices`.","operationId":"searchProducts","parameters":[{"name":"q","in":"query","description":"Search term (name or SKU). Omit to list all.","required":false,"schema":{"type":"string"}},{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"nl"}},{"name":"location_id","in":"query","description":"Scope stock\/availability to a location. Needs `inventory.view_local` for a location your assignment covers, `inventory.view_other_locations` for any other.","required":false,"schema":{"type":"integer"}},{"name":"start","in":"query","required":false,"schema":{"type":"integer","default":0}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":25,"maximum":200}}],"responses":{"200":{"description":"Paginated catalog search results","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ProductList"}}}},"401":{"description":"Unauthorized"},"403":{"description":"The requested location needs an inventory capability the caller does not hold"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/products\/delta":{"get":{"tags":["Products"],"summary":"Incrementally sync products changed since a timestamp","description":"Delta feed over the catalog, backed by Storekeeper\u0027s purpose-built cache-update\nsearch (`ShopModule.naturalSearchShopFlatProductsForCacheUpdate`). The server-side\nfilter returns products whose update stamp is strictly newer than the cursor\ntimestamp, OR equal to it but not yet seen (boundary ids carried inside the\ncursor), so every change is delivered **exactly once**. Start with `since`\n(ISO-8601, or YYYY-MM-DD interpreted as midnight Europe\/Amsterdam), then follow\n`next_cursor` on every subsequent call. Pass exactly one of `since` or `cursor`.\n\nThe timestamp is the **product\u0027s** update stamp\n(`flat_product.product.date_updated`): a price or content change bumps it, but\nshop-level assignment changes may not.\n\nResponse: `{count, data, next_cursor, has_more}`. Each row is the standard\nproduct shape plus `date_updated`. `has_more` is true when the page is full; an\nempty page returns the incoming cursor unchanged.","operationId":"listProductDelta","parameters":[{"name":"since","in":"query","description":"ISO-8601 timestamp or YYYY-MM-DD (midnight Europe\/Amsterdam). Mutually exclusive with `cursor`.","required":false,"schema":{"type":"string"}},{"name":"cursor","in":"query","description":"Opaque cursor from a previous response. Mutually exclusive with `since`.","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":250}},{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"nl"}},{"name":"location_id","in":"query","description":"Scope stock\/availability to a location. Needs `inventory.view_local` for a location your assignment covers, `inventory.view_other_locations` for any other.","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"One delta page with a follow-up cursor","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ProductDelta"}}}},"400":{"description":"Missing\/conflicting since+cursor, or a malformed value"},"401":{"description":"Unauthorized"},"403":{"description":"The requested location needs an inventory capability the caller does not hold"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/products\/{id}":{"get":{"tags":["Products"],"summary":"Get a single shop product by id","description":"Single product (`ShopModule.getShopProduct`). `location_id` scopes stock to one location.","operationId":"getProduct","parameters":[{"name":"id","in":"path","description":"The **shop_product_id** \u2014 the `shop_product_id` field of a list\/create response, NOT the inner product id this endpoint returns as `id`. `ShopModule.getShopProduct` keys on the shop_product_id (ADR-0021); the two are usually equal numbers for the same product, and passing the inner one returns a *different* product rather than a 404.","required":true,"schema":{"type":"integer","pattern":"\\d+"}},{"name":"location_id","in":"query","description":"Needs `inventory.view_local` for a location your assignment covers, `inventory.view_other_locations` for any other.","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Single product","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Product"}}}},"401":{"description":"Unauthorized"},"403":{"description":"The requested location needs an inventory capability the caller does not hold"},"404":{"description":"Product not found"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/product-prices":{"get":{"tags":["Products"],"summary":"List product prices","description":"Lists price rows (`ProductsModule.listProductPrices`). Pass `product_ids`\n(comma-separated) to scope to specific products \u2014 recommended, since the full\nlist is large. `product_price_type_id = 1` is the sale price (carries the\n`tax_rate_id`); other type ids are cost\/purchase rows.","operationId":"listProductPrices","parameters":[{"name":"product_ids","in":"query","description":"Comma-separated product ids to scope to.","required":false,"schema":{"type":"string"}},{"name":"start","in":"query","required":false,"schema":{"type":"integer","default":0}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":500}}],"responses":{"200":{"description":"Array of price rows","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ProductPriceList"}}}},"401":{"description":"Unauthorized"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/reports\/payments":{"get":{"tags":["Reports"],"summary":"Payment method breakdown for a date range","description":"The directly-paid slice of the financial report: how much came in per payment\nmethod (`ReportModule.calculateFinancialReport`), labelled via the account\u0027s\nprovider-method-type catalog (see `\/api\/payment-methods`). Includes a\n`balance_check` that verifies the payments sum matches the paid turnover.\n\nThis is the same computation `\/api\/financial-report`\u0027s `direct_paid` section\nuses \u2014 exposed standalone for reconciliation.","operationId":"getPaymentsReport","parameters":[{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date"}},{"name":"to","in":"query","description":"Defaults to `from`.","required":false,"schema":{"type":"string","format":"date"}},{"name":"location_id","in":"query","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Payment breakdown","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/PaymentsReport"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Your location scope cannot be computed as one location \u2014 several ids or an open `except` set (add `location_id` naming one), or a binding that covers no location at all"},"404":{"description":"The requested location is outside your scope"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/reports\/daily-close":{"get":{"tags":["Reports"],"summary":"Daily close (Z-report) for one day","description":"A compact end-of-day summary for a single date: turnover (incl\/excl\/VAT),\nnon-revenue (gift cards, tips, \u2026), the payment-method breakdown, and the order\ncount. Built from `ReportModule.calculateFinancialReport` for that day plus an\norder count from `ShopModule.listOrders`.","operationId":"getDailyClose","parameters":[{"name":"date","in":"query","description":"The day to close (YYYY-MM-DD).","required":true,"schema":{"type":"string","format":"date"}},{"name":"location_id","in":"query","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Daily close summary","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DailyClose"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Your location scope cannot be computed as one location \u2014 several ids or an open `except` set (add `location_id` naming one), or a binding that covers no location at all"},"404":{"description":"The requested location is outside your scope"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/reports\/product-sales":{"get":{"tags":["Reports"],"summary":"Per-product sales (best sellers) for a date range","description":"Aggregates invoiced line items over the date range into per-product sales:\nquantity sold and revenue (incl. VAT). Built from the same line-item pipeline\nas the financial report (`getInvoiceRowListReportData` \u2192\n`listReportInvoiceRows`), then joined to product names\/SKUs via\n`ProductsModule.listProducts`. Sorted by revenue (or `sort=quantity`),\ncapped at `limit`.","operationId":"getProductSales","parameters":[{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date"}},{"name":"to","in":"query","description":"Defaults to `from`.","required":false,"schema":{"type":"string","format":"date"}},{"name":"location_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"revenue","enum":["revenue","quantity"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":500}}],"responses":{"200":{"description":"Per-product sales, ranked","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ProductSalesReport"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Your location scope cannot be computed as one location \u2014 several ids or an open `except` set (add `location_id` naming one), or a binding that covers no location at all"},"404":{"description":"The requested location is outside your scope"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/shops":{"get":{"tags":["Shops"],"summary":"List sales channels (shops)","description":"Lists the sales channels configured on the Storekeeper account\n(`ShopModule.listShops`). The `id` of each row joins to the financial\nreport\u0027s per-shop turnover split (`turnover_by_group.rows[].shops`) and to\n`discount-orders[].shop_id`, so this endpoint is the catalog that resolves\nthose shop ids to names. `location_id` links a shop to a physical location\n(see `\/api\/locations`).","operationId":"listShops","responses":{"200":{"description":"Array of shops, alphabetical by name","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ShopList"}}}},"401":{"description":"Unauthorized"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/stock":{"get":{"tags":["Stock"],"summary":"List stock levels","description":"Per-product, per-location stock rows (`ShopModule.listShopProductStocks`).\nScope with `location_id` and\/or `product_ids` (comma-separated) \u2014 the full\ncatalog is large, so at least one scope is recommended. Returns the orderable\nand unfulfilled stock values plus the raw counted `value`.\n\nWhich capability a call needs is decided by the location it asks about:\n`inventory.view_local` for a location your assignment covers,\n`inventory.view_other_locations` for any other. Rows you hold neither for are\nnot returned.","operationId":"listStock","parameters":[{"name":"location_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"product_ids","in":"query","description":"Comma-separated product ids.","required":false,"schema":{"type":"string"}},{"name":"start","in":"query","required":false,"schema":{"type":"integer","default":0}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":500}}],"responses":{"200":{"description":"Array of stock rows","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/StockList"}}}},"401":{"description":"Unauthorized"},"403":{"description":"The requested location needs the capability the caller does not hold"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/tax-rates":{"get":{"tags":["Tax Rates"],"summary":"List tax rates","description":"Lists the tax rates registered on the Storekeeper account\n(`ProductsModule.listTaxRates`). The `id` of each row joins to the financial\nreport\u0027s per-rate breakdowns (`direct_paid.per_rate`, `not_paid_now.per_rate`\nand the per-group rate split), so this endpoint resolves those rate ids to a\nhuman `value` (e.g. `21` for 21% BTW) and `name`.\n\nWithout a filter, Storekeeper returns the whole EU registry alphabetised (Austria\nfirst). Pass `country_iso2=NL` to narrow to a single country \u2014 the value is\nupper-cased before it hits Storekeeper.","operationId":"listTaxRates","parameters":[{"name":"country_iso2","in":"query","description":"ISO 3166-1 alpha-2 country code (case-insensitive). Omit for the full EU registry.","required":false,"schema":{"type":"string","example":"NL"}}],"responses":{"200":{"description":"Array of tax rates, sorted by country then value","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/TaxRateList"}}}},"401":{"description":"Unauthorized"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/turnover-groups":{"get":{"tags":["Turnover Groups"],"summary":"List turnover groups","description":"Returns the turnover groups configured on the Storekeeper account.\n(Internally Storekeeper calls these *product groups* \u2014 `ProductGroup`.)\n\nThe `id` of each row joins to the financial report\u0027s\n`turnover_by_group.rows[].product_group_id`, so this endpoint is the\ncatalog that resolves those ids to names. A report row with\n`product_group_id: 0` means \u0022without turnover group\u0022 and has no entry here.","operationId":"listTurnoverGroups","responses":{"200":{"description":"Array of turnover groups, alphabetical by title","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/TurnoverGroupList"}}}},"401":{"description":"Unauthorized"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}},"\/api\/users":{"get":{"tags":["Users"],"summary":"List account users (admin profiles)","description":"Lists the account\u0027s admin\/back-office users (`CommerceModule.listAdminProfiles`).\nEach row is flattened to the identity fields: `id` (subuser id), `email`,\n`lastLogin` (last login timestamp), `Profile` (role\/profile name, e.g. \u0022Admin\u0022),\nand `language` (ISO2). PIN-login and self-scan pseudo-users are filtered out.","operationId":"listUsers","parameters":[{"name":"start","in":"query","required":false,"schema":{"type":"integer","default":0}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":500}}],"responses":{"200":{"description":"Paginated list of users","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/UserList"}}}},"401":{"description":"Unauthorized"},"429":{"description":"Too Many Requests \u2014 the request ceiling for the API key this token was minted from has been reached for the current minute. `Retry-After` carries the number of seconds to wait; sleep that long rather than retrying hot. Every response also carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, and the same budget a second time in the newer IETF Structured Fields form: `RateLimit-Policy` lists every ceiling that applies to the key \u2014 `\u0022general\u0022` and `\u0022write\u0022`, each with `q` (quota) and `w` (window in seconds) \u2014 while `RateLimit` reports the state of the one that will refuse you next and names it, `r` and `t` carrying the same two numbers as `RateLimit-Remaining` and `RateLimit-Reset`. On this refusal that name is the ceiling that actually refused, with `r=0`. Pace yourself off the headers rather than off a rate you hard-coded \u2014 the ceilings are deployment configuration and no figure is published."},"502":{"description":"Upstream Storekeeper error"}}}}},"components":{"schemas":{"Customer":{"description":"One customer relation, as `GET \/api\/customers` emits it.","required":["id","name","own_id","email","business_name","contact_person_name"],"properties":{"id":{"description":"Customer relation id \u2014 the `{id}` of `GET \/api\/customers\/{id}`, and what orders carry as `relation_data_id`.","type":"integer","example":16},"name":{"description":"The relation name as Storekeeper stores it.","type":"string","example":"Klaas Bakker"},"own_id":{"description":"Your own reference for this customer, where one is set; `\u0027\u0027` otherwise.","type":"string","example":"KL-0042"},"email":{"description":"Contact set email; `\u0027\u0027` when the customer carries none.","type":"string","example":"klaas.bakker@example.com"},"business_name":{"description":"Company name for a B2B relation; `\u0027\u0027` for a private consumer.","type":"string","example":"Bakkerij Bakker BV"},"contact_person_name":{"description":"Contact person\u0027s name, assembled from parts when upstream sends no computed one.","type":"string","example":"Klaas Bakker"}},"type":"object"},"CustomerList":{"description":"The `GET \/api\/customers` page envelope.","required":["q","start","limit","count","total","data"],"properties":{"q":{"description":"Echo of the `q` search parameter.","type":"string","example":"koffie"},"start":{"description":"Zero-based offset of the first row \u2014 echo of the `start` parameter.","type":"integer","example":0},"limit":{"description":"Page size \u2014 echo of the `limit` parameter.","type":"integer","example":50},"count":{"description":"Rows in `data`.","type":"integer","example":1},"total":{"description":"The upstream total for the query \u2014 what drives your paging.","type":"integer","example":1},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/Customer"}}},"type":"object"},"CustomerDetail":{"description":"One customer with its contact details, as `GET \/api\/customers\/{id}`, `POST \/api\/customers` and `PATCH \/api\/customers\/{id}` emit it.","required":["id","name","own_id","email","business_name","contact_person_name","phone","address"],"properties":{"id":{"description":"Customer relation id \u2014 the `{id}` of `GET \/api\/customers\/{id}`, and what orders carry as `relation_data_id`.","type":"integer","example":16},"name":{"description":"The relation name as Storekeeper stores it.","type":"string","example":"Klaas Bakker"},"own_id":{"description":"Your own reference for this customer, where one is set; `\u0027\u0027` otherwise.","type":"string","example":"KL-0042"},"email":{"description":"Contact set email; `\u0027\u0027` when the customer carries none.","type":"string","example":"klaas.bakker@example.com"},"business_name":{"description":"Company name for a B2B relation; `\u0027\u0027` for a private consumer.","type":"string","example":"Bakkerij Bakker BV"},"contact_person_name":{"description":"Contact person\u0027s name, assembled from parts when upstream sends no computed one.","type":"string","example":"Klaas Bakker"},"phone":{"description":"Contact set `phone`, else `mobile`, else null \u2014 passed through unshaped.","example":"+31201234567","nullable":true},"address":{"title":"The upstream `contact_address` sub-object, **passed through verbatim** \u2014 street\/zip\/city\/country.","description":"The upstream `contact_address` sub-object, passed through verbatim \u2014 street, zip, city, country.","type":"object","nullable":true,"additionalProperties":true}},"type":"object"},"CustomerSegment":{"description":"One customer segment, as `GET \/api\/customer-segments` emits it.","required":["id","name","description","price_list_id","customer_segment_type_id","customer_count"],"properties":{"id":{"description":"Segment id \u2014 the `{id}` of `POST \/api\/customer-segments\/{id}\/assign`.","type":"integer","example":3},"name":{"description":"Segment name; `\u0027\u0027` when upstream carried none.","type":"string","example":"Stamgasten"},"description":{"description":"Free-text description, where one is set.","type":"string","example":"Klanten met een klantenkaart","nullable":true},"price_list_id":{"description":"Price list the segment applies, where one is attached.","type":"integer","example":2,"nullable":true},"customer_segment_type_id":{"description":"Segment type, where upstream carried one.","type":"integer","example":1,"nullable":true},"customer_count":{"description":"Customers currently in the segment; null when upstream did not count.","type":"integer","example":184,"nullable":true}},"type":"object"},"CustomerSegmentList":{"description":"The `GET \/api\/customer-segments` envelope, alphabetical by name.","required":["count","total","data"],"properties":{"count":{"description":"Rows in `data`.","type":"integer","example":1},"total":{"description":"The upstream total for the query. The route is unpaged, so it is equal to `count` unless upstream reported a larger set.","type":"integer","example":1},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/CustomerSegment"}}},"type":"object"},"DiscountOrder":{"description":"One order carrying a discount, as `GET \/api\/discount-orders` emits it.","required":["id","order_number","date_purchased","status","shop_id","currency","value_wt","value_ex_wt","discount_value_wt","discount_value_ex_wt","customer_name","customer_email","backoffice_url"],"properties":{"id":{"description":"Storekeeper order id \u2014 the `{id}` of `GET \/api\/orders\/{id}`.","type":"integer","example":34},"order_number":{"description":"Human-facing order number.","type":"string","example":"S4-000000013-4"},"date_purchased":{"description":"`YYYY-MM-DD HH:MM:SS\u00b1TZ`; `\u0027\u0027` when upstream carried none.","type":"string","example":"2026-09-16 17:14:11+02:00"},"status":{"description":"Upstream order status.","type":"string","example":"new"},"shop_id":{"description":"Sales channel \u2014 resolve with `GET \/api\/shops`.","type":"integer","example":4},"currency":{"description":"ISO-4217; defaults to `EUR`.","type":"string","example":"EUR"},"value_wt":{"description":"Order total including VAT.","type":"number","format":"float","example":7.13},"value_ex_wt":{"description":"Order total excluding VAT.","type":"number","format":"float","example":5.89},"discount_value_wt":{"description":"Discount total including VAT.","type":"number","format":"float","example":1.5},"discount_value_ex_wt":{"description":"Discount total excluding VAT.","type":"number","format":"float","example":1.24},"customer_name":{"description":"Assembled from the billing address; `\u0027\u0027` when it carries no name.","type":"string","example":"Klaas Bakker"},"customer_email":{"description":"From the billing address\u0027s contact set; `\u0027\u0027` when it carries none.","type":"string","example":"klaas.bakker@example.com"},"backoffice_url":{"description":"Deep link into the merchant\u0027s own backoffice.","type":"string","example":"https:\/\/biju.storekeepercloud.com\/#order\/details\/34"}},"type":"object"},"ReportScope":{"description":"What the figures in a report response actually cover.","required":["resource","binding_mode","requested_location_id","covers_all_locations","location_ids","description"],"properties":{"resource":{"description":"The scoped resource type. `location` is the only one in the registry today.","type":"string","example":"location"},"binding_mode":{"description":"`all`, `only` or `except` \u2014 how the caller\u0027s binding is expressed.","type":"string","example":"only"},"requested_location_id":{"description":"The `location_id` this call asked for, or null on routes that take no such parameter.","type":"integer","example":1,"nullable":true},"covers_all_locations":{"description":"True when the figures cover the whole account.","type":"boolean","example":false},"location_ids":{"description":"The locations covered, or **null for an open set** \u2014 both `all` and `except` are open, so null never means \u0022unrestricted\u0022.","type":"array","items":{"type":"integer"},"nullable":true},"description":{"description":"A sentence naming the coverage, for putting straight in front of a human.","type":"string","example":"These figures cover location 1 only \u2014 not the whole account."}},"type":"object"},"DiscountOrderList":{"description":"The `GET \/api\/discount-orders` envelope.","required":["from","to","shop_id","only_full","include_negative","count","total","data","scope"],"properties":{"from":{"description":"Echo of the `from` date parameter.","type":"string","example":"2026-09-01"},"to":{"description":"Echo of the `to` date parameter.","type":"string","example":"2026-09-30"},"shop_id":{"description":"Echo of the `shop_id` filter, or null when unfiltered.","type":"integer","example":4,"nullable":true},"only_full":{"description":"Echo of `only_full`.","type":"boolean","example":false},"include_negative":{"description":"Echo of `include_negative`.","type":"boolean","example":false},"count":{"description":"Rows in `data`.","type":"integer","example":1},"total":{"description":"Equal to `count`: this route drains every page and filters rows against your scope itself, so the figure reported is what you were shown. The account-wide total is deliberately not disclosed \u2014 a scoped caller learning how many orders it may not see is the enumeration D10 closes.","type":"integer","example":1},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/DiscountOrder"}},"scope":{"$ref":"#\/components\/schemas\/ReportScope"}},"type":"object"},"InclVatTotal":{"description":"A single money figure reported only including VAT.","required":["incl_vat"],"properties":{"incl_vat":{"description":"Including VAT.","type":"number","format":"float","example":1284.5}},"type":"object"},"MoneyTotal":{"description":"One money figure, split three ways.","required":["incl_vat","excl_vat","vat"],"properties":{"incl_vat":{"description":"Including VAT.","type":"number","format":"float","example":1284},"excl_vat":{"description":"Excluding VAT.","type":"number","format":"float","example":1061.16},"vat":{"description":"The VAT itself.","type":"number","format":"float","example":222.84}},"type":"object"},"Margin":{"description":"Turnover excluding VAT minus the cost of goods sold.","required":["value","percent"],"properties":{"value":{"description":"Turnover excluding VAT minus cost of goods sold, in euros.","type":"number","format":"float","example":612.34},"percent":{"description":"The same figure as a percentage of turnover excluding VAT. Null when turnover was zero \u2014 a margin percentage of nothing is undefined, not 0.","type":"number","format":"float","example":47.7,"nullable":true}},"type":"object"},"NonRevenue":{"description":"Money that moved but is not turnover.","required":["gift_card_sales","tips","payments_on_invoice","prepayments","total"],"properties":{"gift_card_sales":{"description":"Gift cards sold \u2014 a liability, not a sale.","type":"number","format":"float","example":75,"nullable":true},"tips":{"description":"Tips taken in the period; null when the upstream report carried no figure.","type":"number","format":"float","example":18.5,"nullable":true},"payments_on_invoice":{"description":"Paid against an existing invoice rather than for a new sale.","type":"number","format":"float","example":0,"nullable":true},"prepayments":{"description":"Taken before the goods were delivered.","type":"number","format":"float","example":0,"nullable":true},"total":{"description":"The four above, summed over whichever were present; null when none were.","type":"number","format":"float","example":93.5,"nullable":true}},"type":"object"},"RevenueBlock":{"description":"What was sold, and what it cost.","required":["total_sales","total_returns","total_differences","total_turnover","total_cost_of_goods_sold","margin","non_revenue"],"properties":{"total_sales":{"$ref":"#\/components\/schemas\/InclVatTotal"},"total_returns":{"$ref":"#\/components\/schemas\/InclVatTotal"},"total_differences":{"$ref":"#\/components\/schemas\/InclVatTotal"},"total_turnover":{"$ref":"#\/components\/schemas\/MoneyTotal"},"total_cost_of_goods_sold":{"description":"Cost of the goods behind that turnover. Zero when no product had a cost price \u2014 see `products_without_tax_rate` before trusting a margin.","type":"number","format":"float","example":671.82},"margin":{"$ref":"#\/components\/schemas\/Margin"},"non_revenue":{"$ref":"#\/components\/schemas\/NonRevenue"}},"type":"object"},"VatBreakdownRow":{"description":"Turnover at one VAT rate.","required":["tax_rate_id","rate","rate_percent","excl_vat","vat","incl_vat"],"properties":{"tax_rate_id":{"description":"Resolve with `GET \/api\/tax-rates`. Null when upstream reported the row without one.","type":"integer","example":2,"nullable":true},"rate":{"description":"The rate as a decimal, e.g. `0.21`. Derived from the row itself when the id could not be resolved.","type":"number","format":"float","example":0.21,"nullable":true},"rate_percent":{"description":"The same rate as a whole percentage, e.g. `21`.","type":"integer","example":21,"nullable":true},"excl_vat":{"description":"Turnover at this rate, excluding VAT.","type":"number","format":"float","example":1061.16},"vat":{"description":"The VAT itself.","type":"number","format":"float","example":222.84},"incl_vat":{"description":"Turnover at this rate, including VAT.","type":"number","format":"float","example":1284}},"type":"object"},"PaymentLine":{"description":"One payment method\u0027s takings, as the payments report and the daily close both emit it.","required":["provider_method_type_id","type_alias","name","value"],"properties":{"provider_method_type_id":{"description":"Resolve with `GET \/api\/payment-methods`.","type":"integer","example":4},"type_alias":{"description":"The stable alias to branch on; null when the id resolves to no known method.","type":"string","example":"pin","nullable":true},"name":{"description":"Display label, or `Method #\u003Cid\u003E` when the alias is unknown. For humans, not for logic.","type":"string","example":"PIN"},"value":{"description":"Takings for this method, including VAT, rounded to cents.","type":"number","format":"float","example":842.15}},"type":"object"},"BalanceCheck":{"description":"Whether the payment lines add up to the paid turnover.","required":["payments_match_incl_vat","difference"],"properties":{"payments_match_incl_vat":{"description":"True when the two sides agree to within a cent.","type":"boolean","example":true},"difference":{"description":"Signed difference between the payment lines and the paid turnover.","type":"number","format":"float","example":0}},"type":"object"},"DirectPaidBlock":{"description":"What was paid at the point of sale.","required":["vat_breakdown","total","payments","payments_total","balance_check","non_revenue"],"properties":{"vat_breakdown":{"description":"Turnover split by VAT rate.","type":"array","items":{"$ref":"#\/components\/schemas\/VatBreakdownRow"}},"total":{"$ref":"#\/components\/schemas\/MoneyTotal"},"payments":{"description":"Takings split by payment method.","type":"array","items":{"$ref":"#\/components\/schemas\/PaymentLine"}},"payments_total":{"description":"The payment lines summed.","type":"number","format":"float","example":1284},"balance_check":{"$ref":"#\/components\/schemas\/BalanceCheck"},"non_revenue":{"$ref":"#\/components\/schemas\/NonRevenue"}},"type":"object"},"NotPaidNowBlock":{"description":"Turnover invoiced on account rather than paid at the counter.","required":["vat_breakdown","total"],"properties":{"vat_breakdown":{"description":"Invoiced turnover split by VAT rate.","type":"array","items":{"$ref":"#\/components\/schemas\/VatBreakdownRow"}},"total":{"$ref":"#\/components\/schemas\/MoneyTotal"}},"type":"object"},"ShopTurnover":{"description":"One sales channel\u0027s share of the turnover.","required":["shop_id","name","total_incl_vat"],"properties":{"shop_id":{"description":"Resolve with `GET \/api\/shops`.","type":"integer","example":4},"name":{"description":"Shop name, or `Shop \u003Cid\u003E` when it could not be resolved.","type":"string","example":"Winkel Amsterdam"},"total_incl_vat":{"description":"Turnover through this shop, including VAT.","type":"number","format":"float","example":842.15}},"type":"object"},"PerShopValue":{"description":"One shop\u0027s share of a single turnover-group row.","required":["shop_id","name","value_incl_vat"],"properties":{"shop_id":{"description":"Resolve with `GET \/api\/shops`.","type":"integer","example":4},"name":{"description":"Shop name, or `Shop \u003Cid\u003E` when it could not be resolved.","type":"string","example":"Winkel Amsterdam"},"value_incl_vat":{"description":"This shop\u0027s part of the row, including VAT.","type":"number","format":"float","example":318.4}},"type":"object"},"TurnoverGroupRow":{"description":"Turnover for one product group at one VAT rate.","required":["product_group_id","product_group_code","name","total_incl_vat","tax_rate_id","tax_rate","tax_rate_percent","excl_vat","vat","cost_of_goods_sold","per_shop"],"properties":{"product_group_id":{"description":"Resolve with `GET \/api\/turnover-groups`. `0` is the synthetic \u0022Without turnover group\u0022 bucket.","type":"integer","example":7},"product_group_code":{"description":"Your own group code, where one is set \u2014 what ledger mapping keys on.","type":"string","example":"8000","nullable":true},"name":{"description":"Group title, or `Group \u003Cid\u003E` when it could not be resolved.","type":"string","example":"Dranken"},"total_incl_vat":{"description":"Turnover for this group at this rate, including VAT.","type":"number","format":"float","example":842.15},"tax_rate_id":{"description":"Resolve with `GET \/api\/tax-rates`. Null when the group has no resolvable rate.","type":"integer","example":2,"nullable":true},"tax_rate":{"description":"The rate as a decimal, e.g. `0.21`.","type":"number","format":"float","example":0.21,"nullable":true},"tax_rate_percent":{"description":"The same rate as a whole percentage, e.g. `21`.","type":"integer","example":21,"nullable":true},"excl_vat":{"description":"Turnover excluding VAT. Zero when the rate could not be resolved \u2014 not a statement that VAT was zero.","type":"number","format":"float","example":696},"vat":{"description":"The VAT itself. Zero on the same condition as `excl_vat`.","type":"number","format":"float","example":146.15},"cost_of_goods_sold":{"description":"Cost of the goods behind this row, or null when no product in the group had a cost price.","type":"number","format":"float","example":441.3,"nullable":true},"per_shop":{"description":"This row split by shop. `[]` when the row is not split across shops \u2014 the key is always present, so the row shape never depends on the data.","type":"array","items":{"$ref":"#\/components\/schemas\/PerShopValue"}}},"type":"object"},"TurnoverByGroup":{"description":"Turnover broken down by product group \u2014 the block a ledger export reads.","required":["total_incl_vat","shops","rows"],"properties":{"total_incl_vat":{"description":"Turnover across every group, including VAT. Null when the upstream matrix could not be read \u2014 which is not zero, and must not be reported as zero.","type":"number","format":"float","example":1284,"nullable":true},"shops":{"description":"The same total split by sales channel.","type":"array","items":{"$ref":"#\/components\/schemas\/ShopTurnover"}},"rows":{"description":"One row per product group per VAT rate.","type":"array","items":{"$ref":"#\/components\/schemas\/TurnoverGroupRow"}}},"type":"object"},"ProductWithoutGroup":{"description":"One product that earned revenue but sits in no turnover group.","required":["id","name","revenue_incl_vat"],"properties":{"id":{"description":"The **inner** product id \u2014 the namespace `PUT \/api\/product-prices` takes.","type":"integer","example":29},"name":{"description":"Product name, or `Product #\u003Cid\u003E` when it could not be resolved.","type":"string","example":"Koffie zwart"},"revenue_incl_vat":{"description":"What this product earned in the period, including VAT \u2014 the size of the problem, so the worst offender is first.","type":"number","format":"float","example":121}},"type":"object"},"ProductWithoutTaxRate":{"description":"One product that earned revenue but has no resolvable tax rate.","required":["id","name","product_group_id","product_group_name","revenue_incl_vat"],"properties":{"id":{"description":"The **inner** product id \u2014 the namespace `PUT \/api\/product-prices` takes.","type":"integer","example":29},"name":{"description":"Product name, or `Product #\u003Cid\u003E` when it could not be resolved.","type":"string","example":"Koffie zwart"},"product_group_id":{"description":"Turnover group, where the product has one \u2014 a product can have a group and still have no usable rate.","type":"integer","example":7,"nullable":true},"product_group_name":{"description":"Name of that group, where it resolved.","type":"string","example":"Dranken","nullable":true},"revenue_incl_vat":{"description":"What this product earned in the period, including VAT \u2014 the size of the problem, so the worst offender is first.","type":"number","format":"float","example":118.8}},"type":"object"},"FinancialReport":{"description":"`GET \/api\/financial-report` \u2014 the structured financial report: revenue, VAT, and turnover by product group.","required":["from","to","location_id","scope","currency","revenue","direct_paid","not_paid_now","turnover_by_group","products_without_group","products_without_tax_rate"],"properties":{"from":{"description":"Echo of the `from` date.","type":"string","example":"2026-09-01"},"to":{"description":"Echo of the `to` date.","type":"string","example":"2026-09-30"},"location_id":{"description":"The location filter that was applied, or null when the report covers the caller\u0027s whole scope. What was *asked for* is `scope.requested_location_id`.","type":"integer","example":1,"nullable":true},"scope":{"$ref":"#\/components\/schemas\/ReportScope"},"currency":{"description":"ISO-4217 the figures are in.","type":"string","example":"EUR"},"revenue":{"$ref":"#\/components\/schemas\/RevenueBlock"},"direct_paid":{"$ref":"#\/components\/schemas\/DirectPaidBlock"},"not_paid_now":{"$ref":"#\/components\/schemas\/NotPaidNowBlock"},"turnover_by_group":{"$ref":"#\/components\/schemas\/TurnoverByGroup"},"products_without_group":{"description":"Products that earned revenue but sit in no turnover group, worst first. They land in the synthetic group `0`, so a ledger export cannot place them until they are fixed in Storekeeper.","type":"array","items":{"$ref":"#\/components\/schemas\/ProductWithoutGroup"}},"products_without_tax_rate":{"description":"Products that earned revenue but have no resolvable tax rate, worst first. Their turnover cannot be split into VAT, which is why `excl_vat` and `vat` can read zero on a row that clearly sold something.","type":"array","items":{"$ref":"#\/components\/schemas\/ProductWithoutTaxRate"}}},"type":"object"},"LocationAddress":{"description":"The address record attached to a location.","required":["id","relation_data_id","country_iso2","published","deleted","date_created","date_updated"],"properties":{"id":{"description":"Address record id \u2014 the same value the location carries as `address_id`.","type":"integer","example":2},"relation_data_id":{"description":"The relation this address belongs to.","type":"integer","example":2},"country_iso2":{"description":"ISO-3166-1 alpha-2. **This is the only place a location\u0027s country is reported.**","type":"string","example":"NL","nullable":true},"published":{"description":"Whether the address is published.","type":"boolean","example":false},"deleted":{"description":"Whether the address has been deleted upstream.","type":"boolean","example":false},"date_created":{"description":"When the address record was created.","type":"string","example":"2026-05-11 17:39:00+02:00","nullable":true},"date_updated":{"description":"When the address record last changed.","type":"string","example":"2026-05-11 17:39:00+02:00","nullable":true}},"type":"object"},"Location":{"description":"One physical location, as `GET \/api\/locations` and `GET \/api\/locations\/{id}` emit it.","required":["id","name","is_default","is_active","address_id","relation_data_id","object_instance_id","use_as_billing_address","date_created","date_updated","address"],"properties":{"id":{"description":"Location id \u2014 the namespace the `location` scope binding uses, and what `\/api\/shops` rows carry as `location_id`. Not a product-side id.","type":"integer","example":1},"name":{"description":"Location name.","type":"string","example":"Hoofdlocatie"},"is_default":{"description":"True for the account\u0027s default location.","type":"boolean","example":true},"is_active":{"description":"False for a location that has been taken out of use.","type":"boolean","example":true},"address_id":{"description":"The address record\u0027s id \u2014 the same value as `address.id`.","type":"integer","example":2,"nullable":true},"relation_data_id":{"description":"The relation this location belongs to.","type":"integer","example":2,"nullable":true},"object_instance_id":{"description":"Storekeeper\u0027s internal object instance id.","type":"integer","example":6,"nullable":true},"use_as_billing_address":{"description":"Whether this location\u0027s address doubles as the billing address.","type":"boolean","example":false},"date_created":{"description":"When the location was created.","type":"string","example":"2026-05-11 17:39:00+02:00","nullable":true},"date_updated":{"description":"When the location last changed.","type":"string","example":"2026-05-11 17:39:00+02:00","nullable":true},"address":{"description":"The location\u0027s address record. Null when it carries none.","nullable":true,"oneOf":[{"$ref":"#\/components\/schemas\/LocationAddress"}]}},"type":"object"},"LocationList":{"description":"The `GET \/api\/locations` envelope.","required":["count","total","data"],"properties":{"count":{"description":"Rows in `data`, after the location scope filter.","type":"integer","example":1},"total":{"description":"Equal to `count` \u2014 the route is unpaged, and the account-wide total is deliberately not disclosed to a scoped caller.","type":"integer","example":1},"data":{"description":"The locations you may see.","type":"array","items":{"$ref":"#\/components\/schemas\/Location"}}},"type":"object"},"MeToken":{"description":"The bearer token you are holding, as `GET \/api\/me` reports it.","required":["expires_at","expires_in","kind"],"properties":{"expires_at":{"description":"Unix timestamp the token stops being accepted at.","type":"integer","example":1789660000},"expires_in":{"description":"Seconds remaining, floored at 0. Exchange your API key again when it runs out \u2014 there is no refresh endpoint and you do not need one.","type":"integer","example":1740},"kind":{"description":"Always `access_token`, and deliberately still emitted: it tells you which envelope you are holding, so a client that branches on it keeps working if a second kind is ever added.","type":"string","example":"access_token","enum":["access_token"]}},"type":"object"},"MeAuthorization":{"description":"What you are allowed to do \u2014 the answer to ask instead of collecting 403s.","required":["gated","role","role_id","capabilities","requires_bindings","bindings","note"],"properties":{"gated":{"description":"Always true: the capability gate applies to every caller, with no exemption for any shape of credential.","type":"boolean","example":true},"role":{"description":"The role your key\u0027s assignment carries; null when it has none.","type":"string","example":"manager","nullable":true},"role_id":{"description":"Numeric id of the role named in `role`.","type":"integer","example":12,"nullable":true},"capabilities":{"description":"capability name =\u003E metadata. **Always an object, never null** \u2014 a role that holds nothing answers `{}`, which is a real answer and not \u0022unknown\u0022. It is cast to an object so an empty map serialises as `{}` and not `[]`; PHP\u0027s two empty values collapse on the wire, and an SDK that typed the field off a `[]` would break the first time a caller held a capability.","type":"object","additionalProperties":true},"requires_bindings":{"description":"Resource types your capabilities are scoped by, derived by the backend from what you actually hold. Read it; never assume it.","type":"array","items":{"type":"string"}},"bindings":{"description":"resource type =\u003E `{mode: all|only|except, ids: [...]}`. Always an object; `{}` is a session that carried no bindings.","type":"object","additionalProperties":true},"note":{"description":"Reserved for a human-readable caveat; always null today.","type":"string","example":null,"nullable":true}},"type":"object"},"MeUser":{"description":"Who the token belongs to, from `RelationsModule.getMyProfile`.","required":["subuser_id","name","display_name","email"],"properties":{"subuser_id":{"description":"The subuser this token belongs to.","type":"integer","example":7,"nullable":true},"name":{"description":"The subuser\u0027s own name field.","type":"string","example":"anna","nullable":true},"display_name":{"description":"Assembled from the contact person\u0027s name parts; null when none are set.","type":"string","example":"Anna de Vries","nullable":true},"email":{"description":"Subuser email, falling back to the contact person\u0027s, then the profile\u0027s.","type":"string","example":"anna@example.com","nullable":true}},"type":"object"},"MeLockedLocation":{"description":"The single store this user is locked to, when they are locked to one.","required":["id","name"],"properties":{"id":{"description":"Location id \u2014 the same namespace `GET \/api\/locations` and `scope.location_ids` use.","type":"integer","example":1},"name":{"description":"Display name.","type":"string","example":"Hoofdlocatie"}},"type":"object"},"MeRole":{"description":"One per-module role, from the profile\u0027s `products[].membership`.","required":["module","alias","level","location_id"],"properties":{"module":{"description":"Storekeeper module the role applies to.","type":"string","example":"ProductsModule"},"alias":{"description":"Role alias within that module.","type":"string","example":"manager","nullable":true},"level":{"description":"Role level within that module.","type":"integer","example":50,"nullable":true},"location_id":{"description":"The location the membership is bound to; null means account-wide.","type":"integer","example":1,"nullable":true}},"type":"object"},"Me":{"description":"`GET \/api\/me` \u2014 the introspection endpoint.","required":["account","subaccount","token","authorization","user","locked_location","roles","profile_error"],"properties":{"account":{"description":"The Storekeeper account the token was issued for.","type":"string","example":"biju"},"subaccount":{"description":"The subaccount, or `\u0027\u0027` when the token names none. Never null \u2014 the empty string is the signal, and a client branching on `subaccount === null` is writing dead code.","type":"string","example":""},"token":{"$ref":"#\/components\/schemas\/MeToken"},"authorization":{"$ref":"#\/components\/schemas\/MeAuthorization"},"user":{"description":"Null when the profile lookup failed \u2014 see `profile_error`.","nullable":true,"oneOf":[{"$ref":"#\/components\/schemas\/MeUser"}]},"locked_location":{"description":"Null when this user is not locked to a single store. Not a statement about what they may see; that is `authorization.bindings`.","nullable":true,"oneOf":[{"$ref":"#\/components\/schemas\/MeLockedLocation"}]},"roles":{"description":"Storekeeper\u0027s own per-module memberships. Empty when the profile lookup failed, which is not the same as \u0022holds no roles\u0022.","type":"array","items":{"$ref":"#\/components\/schemas\/MeRole"}},"profile_error":{"description":"Why the profile lookup failed, or null when it did not. The key is always present so the shape never changes under you.","type":"string","example":null,"nullable":true}},"type":"object"},"Order":{"description":"One order header row, as `GET \/api\/orders` and `GET \/api\/orders\/delta` emit it.","required":["id","order_number","date_purchased","date_updated","pickup_date","status","sub_status","shop_id","location_id","currency","value_wt","value_ex_wt","discount_value_wt","discount_value_ex_wt","is_internal","is_paid","is_shipped","is_delivered","customer_reference","relation_data_id","customer_name","customer_email","backoffice_url"],"properties":{"id":{"description":"Storekeeper order id \u2014 the `{id}` of `GET \/api\/orders\/{id}`.","type":"integer","example":34},"order_number":{"description":"Human-facing number: upstream `number`, else `shop_order_number`, else the id as a string.","type":"string","example":"S4-000000013-4"},"date_purchased":{"description":"`YYYY-MM-DD HH:MM:SS\u00b1TZ`. `\u0027\u0027` when upstream carried none.","type":"string","example":"2026-09-16 17:14:11+02:00"},"date_updated":{"description":"Null on rows never touched since creation \u2014 those never match the delta feed\u0027s filter.","type":"string","example":"2026-09-16 17:27:40+02:00","nullable":true},"pickup_date":{"description":"Passed through unshaped. Untyped deliberately: absent from every captured `ShopModule.getOrder` payload, so it has only ever been emitted as null here. Capture before typing.","nullable":true},"status":{"description":"Upstream order status, e.g. `new`.","type":"string","example":"new"},"sub_status":{"description":"Passed through unshaped; absent from every captured payload. See `pickup_date`.","nullable":true},"shop_id":{"description":"Sales channel \u2014 resolve with `GET \/api\/shops`.","type":"integer","example":4},"location_id":{"description":"Physical location \u2014 resolve with `GET \/api\/locations`. Null on orders that carry none (a webshop order does).","type":"integer","example":1,"nullable":true},"currency":{"description":"ISO-4217, from upstream `currency_iso3`; defaults to `EUR`.","type":"string","example":"EUR"},"value_wt":{"description":"Order total **including** VAT (Dutch *met*), from upstream `value_wt`.","type":"number","format":"float","example":7.13},"value_ex_wt":{"description":"Order total **excluding** VAT, from upstream `value`.","type":"number","format":"float","example":5.89},"discount_value_wt":{"description":"Discount total including VAT.","type":"number","format":"float","example":1.5},"discount_value_ex_wt":{"description":"Discount total excluding VAT, from upstream `discount_value`.","type":"number","format":"float","example":1.24},"is_internal":{"description":"True for an order the merchant raised itself rather than a customer.","type":"boolean","example":false},"is_paid":{"description":"True once the order has been paid in full.","type":"boolean","example":true},"is_shipped":{"description":"True once the order has been shipped.","type":"boolean","example":true},"is_delivered":{"description":"True once the order has been delivered.","type":"boolean","example":false},"customer_reference":{"description":"Passed through unshaped; absent from every captured payload. See `pickup_date`.","nullable":true},"relation_data_id":{"description":"The customer relation \u2014 the id `GET \/api\/customers\/{id}` takes.","type":"integer","example":16,"nullable":true},"customer_name":{"description":"Assembled from the billing address\u0027s contact person; `\u0027\u0027` when it carries no name.","type":"string","example":"Klaas Bakker"},"customer_email":{"description":"From the billing address\u0027s contact set; `\u0027\u0027` when it carries none.","type":"string","example":"klaas.bakker@example.com"},"backoffice_url":{"description":"Deep link into the merchant\u0027s own backoffice; `\u0027\u0027` when the id is unusable.","type":"string","example":"https:\/\/biju.storekeepercloud.com\/#order\/details\/34"}},"type":"object"},"OrderList":{"description":"The `GET \/api\/orders` page envelope.","required":["from","to","start","limit","count","total","data"],"properties":{"from":{"description":"Echo of the `from` query parameter; `\u0027\u0027` when it was not given.","type":"string","example":"2026-09-01"},"to":{"description":"Echo of the `to` query parameter, which defaults to `from`.","type":"string","example":"2026-09-30"},"start":{"description":"Zero-based offset of the first row \u2014 echo of the `start` parameter.","type":"integer","example":0},"limit":{"description":"Page size \u2014 echo of the `limit` parameter.","type":"integer","example":50},"count":{"description":"Rows in `data` \u2014 **after** the location scope filter, so it can be lower than `total`.","type":"integer","example":1},"total":{"description":"The **upstream** total for the query, not the scoped count. It is what drives your paging, and there is no scoped total to replace it with short of draining every page. A scoped caller can therefore see `total` larger than the rows it is allowed to read.","type":"integer","example":1},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/Order"}}},"type":"object"},"OrderDelta":{"description":"The `GET \/api\/orders\/delta` page envelope.","required":["count","data","next_cursor","has_more"],"properties":{"count":{"description":"Rows in `data`, after the location scope filter.","type":"integer","example":1},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/Order"}},"next_cursor":{"description":"Opaque \u2014 pass it back as `cursor` on the next call. An empty page returns the incoming cursor unchanged. Never parse it.","type":"string","example":"eyJ2IjoxLCJ0IjoiMjAyNi0wOS0xNiAxNDowMjoxMSswMjowMCIsImkiOlsyOV19"},"has_more":{"description":"True when the page came back full. Delivery is **at-least-once**: the cursor re-enters at the last seen timestamp, so a row can appear on more than one page. Dedupe or upsert on `id`.","type":"boolean","example":false}},"type":"object"},"OrderAddress":{"description":"One lightly-shaped order address, as `GET \/api\/orders\/{id}` emits it.","required":["name","email","phone","address"],"properties":{"name":{"description":"Contact person\u0027s assembled name, or the contact address\u0027s name; `\u0027\u0027` when neither is present.","type":"string","example":"Koffie zwart"},"email":{"description":"Contact set\u0027s email; `\u0027\u0027` when the address carries none.","type":"string","example":"klaas.bakker@example.com"},"phone":{"description":"Contact set\u0027s `phone`, else `telephone`, else null. Passed through **uncast**, so no type is published: the captured value is a string (`0633333333`) but upstream is free to send a number, and declaring `string` would be a promise this code does not keep.","example":"0633333333","nullable":true},"address":{"description":"The upstream `contact_address` sub-object, **passed through verbatim** \u2014 street\/zip\/city\/country. Deliberate: it is the only place a country reaches the caller, and a projection would drop fields.","type":"object","nullable":true,"additionalProperties":true}},"type":"object"},"OrderWithAddresses":{"description":"One order plus its lightly-shaped addresses, as `GET \/api\/orders\/{id}`, `POST \/api\/orders` and `PATCH \/api\/orders\/{id}` emit it.","required":["id","order_number","date_purchased","date_updated","pickup_date","status","sub_status","shop_id","location_id","currency","value_wt","value_ex_wt","discount_value_wt","discount_value_ex_wt","is_internal","is_paid","is_shipped","is_delivered","customer_reference","relation_data_id","customer_name","customer_email","backoffice_url","billing_address","shipping_address"],"properties":{"id":{"description":"Storekeeper order id \u2014 the `{id}` of `GET \/api\/orders\/{id}`.","type":"integer","example":34},"order_number":{"description":"Human-facing number: upstream `number`, else `shop_order_number`, else the id as a string.","type":"string","example":"S4-000000013-4"},"date_purchased":{"description":"`YYYY-MM-DD HH:MM:SS\u00b1TZ`. `\u0027\u0027` when upstream carried none.","type":"string","example":"2026-09-16 17:14:11+02:00"},"date_updated":{"description":"Null on rows never touched since creation \u2014 those never match the delta feed\u0027s filter.","type":"string","example":"2026-09-16 17:27:40+02:00","nullable":true},"pickup_date":{"description":"Passed through unshaped. Untyped deliberately: absent from every captured `ShopModule.getOrder` payload, so it has only ever been emitted as null here. Capture before typing.","nullable":true},"status":{"description":"Upstream order status, e.g. `new`.","type":"string","example":"new"},"sub_status":{"description":"Passed through unshaped; absent from every captured payload. See `pickup_date`.","nullable":true},"shop_id":{"description":"Sales channel \u2014 resolve with `GET \/api\/shops`.","type":"integer","example":4},"location_id":{"description":"Physical location \u2014 resolve with `GET \/api\/locations`. Null on orders that carry none (a webshop order does).","type":"integer","example":1,"nullable":true},"currency":{"description":"ISO-4217, from upstream `currency_iso3`; defaults to `EUR`.","type":"string","example":"EUR"},"value_wt":{"description":"Order total **including** VAT (Dutch *met*), from upstream `value_wt`.","type":"number","format":"float","example":7.13},"value_ex_wt":{"description":"Order total **excluding** VAT, from upstream `value`.","type":"number","format":"float","example":5.89},"discount_value_wt":{"description":"Discount total including VAT.","type":"number","format":"float","example":1.5},"discount_value_ex_wt":{"description":"Discount total excluding VAT, from upstream `discount_value`.","type":"number","format":"float","example":1.24},"is_internal":{"description":"True for an order the merchant raised itself rather than a customer.","type":"boolean","example":false},"is_paid":{"description":"True once the order has been paid in full.","type":"boolean","example":true},"is_shipped":{"description":"True once the order has been shipped.","type":"boolean","example":true},"is_delivered":{"description":"True once the order has been delivered.","type":"boolean","example":false},"customer_reference":{"description":"Passed through unshaped; absent from every captured payload. See `pickup_date`.","nullable":true},"relation_data_id":{"description":"The customer relation \u2014 the id `GET \/api\/customers\/{id}` takes.","type":"integer","example":16,"nullable":true},"customer_name":{"description":"Assembled from the billing address\u0027s contact person; `\u0027\u0027` when it carries no name.","type":"string","example":"Klaas Bakker"},"customer_email":{"description":"From the billing address\u0027s contact set; `\u0027\u0027` when it carries none.","type":"string","example":"klaas.bakker@example.com"},"backoffice_url":{"description":"Deep link into the merchant\u0027s own backoffice; `\u0027\u0027` when the id is unusable.","type":"string","example":"https:\/\/biju.storekeepercloud.com\/#order\/details\/34"},"billing_address":{"description":"Null when the order carries no billing address.","nullable":true,"oneOf":[{"$ref":"#\/components\/schemas\/OrderAddress"}]},"shipping_address":{"description":"Null when the order carries no shipping address.","nullable":true,"oneOf":[{"$ref":"#\/components\/schemas\/OrderAddress"}]}},"type":"object"},"OrderItem":{"description":"One order line item, as `GET \/api\/orders\/{id}\/items` emits it.","required":["id","order_id","kind","sku","name","quantity","ppu","ppu_wt","price","price_wt","tax_rate_id","product_id","shop_product_id","pickup_date"],"properties":{"id":{"description":"Order-item id. Not a product id in either namespace.","type":"integer","example":771},"order_id":{"description":"The order this line belongs to.","type":"integer","example":34,"nullable":true},"kind":{"description":"Derived from the upstream `is_*` flags, first match wins; `other` when none is set.","type":"string","example":"product","enum":["discount","other","payment","product","shipping"]},"sku":{"description":"Article number, passed through **uncast** \u2014 captured values are strings (`8712051220918`, an EAN, and `737167`), but upstream is free to send a number, so no type is published.","example":"8712051220918","nullable":true},"name":{"description":"Display name.","type":"string","example":"Koffie zwart"},"quantity":{"description":"Units on this line \u2014 fractional for weighed goods.","type":"number","format":"float","example":2},"ppu":{"description":"Price per unit, excluding VAT.","type":"number","format":"float","example":2.48,"nullable":true},"ppu_wt":{"description":"Price per unit, including VAT.","type":"number","format":"float","example":3,"nullable":true},"price":{"description":"Line total, excluding VAT.","type":"number","format":"float","example":4.96,"nullable":true},"price_wt":{"description":"Line total, including VAT.","type":"number","format":"float","example":6,"nullable":true},"tax_rate_id":{"description":"Resolve with `GET \/api\/tax-rates`.","type":"integer","example":2,"nullable":true},"product_id":{"description":"The **inner** product id \u2014 what `PUT \/api\/product-prices` takes. Not the `{id}` of `GET \/api\/products\/{id}`.","type":"integer","example":29,"nullable":true},"shop_product_id":{"description":"The **shop_product_id** \u2014 the `{id}` of `GET`\/`PATCH \/api\/products\/{id}`. Not the id in the `product_id` field above.","type":"integer","example":26,"nullable":true},"pickup_date":{"description":"Passed through unshaped. See `Order::$pickup_date`.","nullable":true}},"type":"object"},"OrderItemList":{"description":"The `GET \/api\/orders\/{id}\/items` envelope.","required":["order_id","count","truncated","data"],"properties":{"order_id":{"description":"Echo of the `{id}` path parameter.","type":"integer","example":34},"count":{"description":"Rows in `data`.","type":"integer","example":1},"truncated":{"description":"True when the drain hit its page cap and stopped early \u2014 `data` is then a prefix of the order\u0027s items, not all of them.","type":"boolean","example":false},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/OrderItem"}}},"type":"object"},"PaymentMethod":{"description":"One payment method type, as `GET \/api\/payment-methods` emits it.","required":["provider_method_type_id","type_alias","name"],"properties":{"provider_method_type_id":{"description":"Method type id \u2014 what the payments report carries as `provider_method_type_id`. **Not** a payment id.","type":"integer","example":4},"type_alias":{"description":"The stable alias to branch on; prefer it over `name`, which is a display label.","type":"string","example":"pin"},"name":{"description":"Display label derived from the alias \u2014 for humans, not for logic.","type":"string","example":"PIN"}},"type":"object"},"PaymentMethodList":{"description":"The `GET \/api\/payment-methods` envelope, alphabetical by label.","required":["count","data"],"properties":{"count":{"description":"Rows in `data`.","type":"integer","example":1},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/PaymentMethod"}}},"type":"object"},"Product":{"description":"One product, as `GET \/api\/products`, `GET \/api\/products\/{id}`, `GET \/api\/products\/delta`, `POST \/api\/products` and `PATCH \/api\/products\/{id}` all emit it.","required":["id","sku","title","product_group_id","stock","shop_product_id"],"properties":{"id":{"description":"The **inner** product id \u2014 what `PUT \/api\/product-prices` takes. Not the `{id}` of this route. **Null when it could not be resolved**, which is a real answer: it used to fall back to the shop_product_id, so a client feeding `id` to the price route priced whatever inner product happened to carry that number (ADR-0043).","type":"integer","example":29,"nullable":true},"sku":{"description":"Your article number, where the product carries one.","type":"string","example":"LS00121","nullable":true},"title":{"description":"Product title; `\u0027\u0027` when no captured nesting carried one.","type":"string","example":"Koffie zwart"},"product_group_id":{"description":"Turnover group \u2014 resolve with `GET \/api\/turnover-groups`.","type":"integer","example":7,"nullable":true},"stock":{"description":"Stock level, or **null when you may not be shown it** \u2014 which is deliberately indistinguishable from a product that carries no stock figure at all, because a read filters rather than announces (D10). The key never disappears, so the shape does not change under you.","type":"number","format":"float","example":45,"nullable":true},"shop_product_id":{"description":"The **shop_product_id** \u2014 the `{id}` of `GET`\/`PATCH \/api\/products\/{id}`.","type":"integer","example":26,"nullable":true}},"type":"object"},"ProductList":{"description":"The `GET \/api\/products` search page envelope.","required":["q","start","limit","count","total","data"],"properties":{"q":{"description":"Echo of the `q` search parameter.","type":"string","example":"koffie"},"start":{"description":"Zero-based offset of the first row \u2014 echo of the `start` parameter.","type":"integer","example":0},"limit":{"description":"Page size \u2014 echo of the `limit` parameter.","type":"integer","example":50},"count":{"description":"Rows in `data`.","type":"integer","example":1},"total":{"description":"The upstream total for the query \u2014 what drives your paging.","type":"integer","example":1},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/Product"}}},"type":"object"},"ProductDeltaRow":{"description":"One row of `GET \/api\/products\/delta`: the {@see Product} shape plus the `date_updated` the feed is keyed on.","required":["id","sku","title","product_group_id","stock","shop_product_id","date_updated"],"properties":{"id":{"description":"The **inner** product id \u2014 what `PUT \/api\/product-prices` takes. Not the `{id}` of this route. **Null when it could not be resolved**, which is a real answer: it used to fall back to the shop_product_id, so a client feeding `id` to the price route priced whatever inner product happened to carry that number (ADR-0043).","type":"integer","example":29,"nullable":true},"sku":{"description":"Your article number, where the product carries one.","type":"string","example":"LS00121","nullable":true},"title":{"description":"Product title; `\u0027\u0027` when no captured nesting carried one.","type":"string","example":"Koffie zwart"},"product_group_id":{"description":"Turnover group \u2014 resolve with `GET \/api\/turnover-groups`.","type":"integer","example":7,"nullable":true},"stock":{"description":"Stock level, or **null when you may not be shown it** \u2014 which is deliberately indistinguishable from a product that carries no stock figure at all, because a read filters rather than announces (D10). The key never disappears, so the shape does not change under you.","type":"number","format":"float","example":45,"nullable":true},"shop_product_id":{"description":"The **shop_product_id** \u2014 the `{id}` of `GET`\/`PATCH \/api\/products\/{id}`.","type":"integer","example":26,"nullable":true},"date_updated":{"description":"`flat_product.product.date_updated` \u2014 what the feed sorts and filters on. Null on products never touched since creation; those never match the filter and appear only in `GET \/api\/products`.","type":"string","example":"2026-09-16 14:02:11+02:00","nullable":true}},"type":"object"},"ProductDelta":{"description":"The `GET \/api\/products\/delta` page envelope.","required":["count","data","next_cursor","has_more"],"properties":{"count":{"description":"Rows in `data`.","type":"integer","example":1},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/ProductDeltaRow"}},"next_cursor":{"description":"Opaque \u2014 pass it back as `cursor` on the next call. Never parse it.","type":"string","example":"eyJ2IjoxLCJ0IjoiMjAyNi0wOS0xNiAxNDowMjoxMSswMjowMCIsImkiOlsyOV19"},"has_more":{"description":"True when the page came back full. Delivery is **at-least-once**: the cursor re-enters at the last seen timestamp, so a row can appear on more than one page. Dedupe or upsert on `id`.","type":"boolean","example":false}},"type":"object"},"ProductPrice":{"description":"One price row, as `GET \/api\/product-prices` emits it.","required":["id","product_id","product_price_type_id","is_sale_price","ppu","ppu_wt","tax_rate_id","currency","from_qty"],"properties":{"id":{"description":"The price row\u0027s own id. Not a product id in either namespace.","type":"integer","example":881},"product_id":{"description":"The **inner** product id this price belongs to \u2014 the same namespace `PUT \/api\/product-prices` takes.","type":"integer","example":29,"nullable":true},"product_price_type_id":{"description":"Upstream price type. `1` is the sale price, which is what `is_sale_price` reports.","type":"integer","example":1,"nullable":true},"is_sale_price":{"description":"Derived: true when `product_price_type_id` is `1`. Branch on this rather than on the raw id.","type":"boolean","example":true},"ppu":{"description":"Price per unit, excluding VAT.","type":"number","format":"float","example":2.48,"nullable":true},"ppu_wt":{"description":"Price per unit, including VAT.","type":"number","format":"float","example":3,"nullable":true},"tax_rate_id":{"description":"Resolve with `GET \/api\/tax-rates`.","type":"integer","example":2,"nullable":true},"currency":{"description":"ISO-4217, from upstream `currency_iso3`.","type":"string","example":"EUR","nullable":true},"from_qty":{"description":"Quantity this tier starts at, for tiered pricing \u2014 passed through unshaped.","example":1,"nullable":true}},"type":"object"},"ProductPriceList":{"description":"The `GET \/api\/product-prices` page envelope.","required":["start","limit","count","total","data"],"properties":{"start":{"description":"Zero-based offset of the first row \u2014 echo of the `start` parameter.","type":"integer","example":0},"limit":{"description":"Page size \u2014 echo of the `limit` parameter.","type":"integer","example":50},"count":{"description":"Rows in `data`.","type":"integer","example":1},"total":{"description":"The upstream total for the query \u2014 what drives your paging.","type":"integer","example":1},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/ProductPrice"}}},"type":"object"},"PaymentsReport":{"description":"`GET \/api\/reports\/payments` \u2014 what came in, split by payment method.","required":["from","to","location_id","scope","currency","payments","payments_total","paid_turnover_incl_vat","balance_check"],"properties":{"from":{"description":"Echo of the `from` date.","type":"string","example":"2026-09-01"},"to":{"description":"Echo of the `to` date.","type":"string","example":"2026-09-30"},"location_id":{"description":"Echo of the `location_id` filter, or null when the report covers the caller\u0027s whole scope.","type":"integer","example":1,"nullable":true},"scope":{"$ref":"#\/components\/schemas\/ReportScope"},"currency":{"description":"ISO-4217 the figures are in.","type":"string","example":"EUR"},"payments":{"description":"Takings split by payment method. A method with no takings does not appear.","type":"array","items":{"$ref":"#\/components\/schemas\/PaymentLine"}},"payments_total":{"description":"The `payments` lines summed.","type":"number","format":"float","example":842.15},"paid_turnover_incl_vat":{"description":"Turnover actually paid for in the period, including VAT.","type":"number","format":"float","example":842.15},"balance_check":{"description":"Whether `payments_total` reconciles against `paid_turnover_incl_vat`; null when the upstream report carried no such figure.","nullable":true,"oneOf":[{"$ref":"#\/components\/schemas\/BalanceCheck"}]}},"type":"object"},"DailyClose":{"description":"`GET \/api\/reports\/daily-close` \u2014 the Z-report for one day.","required":["date","location_id","scope","currency","turnover","non_revenue","payments","payments_total","balance_check","order_count"],"properties":{"date":{"description":"The day being closed, `YYYY-MM-DD`.","type":"string","example":"2026-09-16"},"location_id":{"description":"Echo of the `location_id` filter, or null when the report covers the caller\u0027s whole scope.","type":"integer","example":1,"nullable":true},"scope":{"$ref":"#\/components\/schemas\/ReportScope"},"currency":{"description":"ISO-4217 the figures are in.","type":"string","example":"EUR"},"turnover":{"description":"Turnover for the day; null when the upstream report carried none.","nullable":true,"oneOf":[{"$ref":"#\/components\/schemas\/MoneyTotal"}]},"non_revenue":{"description":"Money that moved without being a sale; null when the upstream report carried none.","nullable":true,"oneOf":[{"$ref":"#\/components\/schemas\/NonRevenue"}]},"payments":{"description":"Takings split by payment method. A method with no takings does not appear.","type":"array","items":{"$ref":"#\/components\/schemas\/PaymentLine"}},"payments_total":{"description":"The `payments` lines summed.","type":"number","format":"float","example":842.15},"balance_check":{"description":"Whether the payment lines reconcile; null when the upstream report carried no such figure.","nullable":true,"oneOf":[{"$ref":"#\/components\/schemas\/BalanceCheck"}]},"order_count":{"description":"Orders counted in the day.","type":"integer","example":128}},"type":"object"},"ProductSalesRow":{"description":"One product\u0027s sales in the period, as `GET \/api\/reports\/product-sales` emits it.","required":["product_id","name","sku","quantity","revenue_incl_vat"],"properties":{"product_id":{"description":"The **inner** product id \u2014 the same namespace `PUT \/api\/product-prices` takes, not the `{id}` of `GET \/api\/products\/{id}`.","type":"integer","example":29},"name":{"description":"Product name, or `Product #\u003Cid\u003E` when it could not be resolved.","type":"string","example":"Koffie zwart"},"sku":{"description":"Article number, where the product carries one.","type":"string","example":"LS00121","nullable":true},"quantity":{"description":"Units sold, to three decimals \u2014 fractional for weighed goods.","type":"number","format":"float","example":128},"revenue_incl_vat":{"description":"Revenue including VAT, to cents.","type":"number","format":"float","example":384}},"type":"object"},"ProductSalesReport":{"description":"`GET \/api\/reports\/product-sales` \u2014 what sold, per product, in the period.","required":["from","to","location_id","scope","sort","count","total","distinct_products","truncated","data"],"properties":{"from":{"description":"Echo of the `from` date.","type":"string","example":"2026-09-01"},"to":{"description":"Echo of the `to` date.","type":"string","example":"2026-09-30"},"location_id":{"description":"Echo of the `location_id` filter, or null when the report covers the caller\u0027s whole scope.","type":"integer","example":1,"nullable":true},"scope":{"$ref":"#\/components\/schemas\/ReportScope"},"sort":{"description":"Echo of the `sort` parameter.","type":"string","example":"revenue"},"count":{"description":"Rows in `data`, capped by `limit`. Fewer than `total` whenever more distinct products sold than `limit` allows \u2014 which is the ordinary case and is **not** what `truncated` reports.","type":"integer","example":1},"total":{"description":"Distinct products with sales in the period. Equal to `distinct_products`.","type":"integer","example":1},"distinct_products":{"description":"Distinct products with sales in the period. The same number as `total`, kept because both names shipped.","type":"integer","example":37},"truncated":{"description":"**The figures are incomplete, not merely paged.** True when the upstream invoice-row drain hit its page cap, so every `quantity`, every `revenue_incl_vat`, `total` and `distinct_products` are under-counted. A reconciliation client should refuse these numbers rather than page for more. It does not mean there are further rows \u2014 that is `count` versus `total`.","type":"boolean","example":false},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/ProductSalesRow"}}},"type":"object"},"Shop":{"description":"One sales channel, as `GET \/api\/shops` emits it.","required":["id","name","alias","location_id","relation_data_id"],"properties":{"id":{"description":"Sales channel id \u2014 what report rows carry as `shop_id`.","type":"integer","example":4},"name":{"description":"Upstream `name`, falling back to `alias`, then `Shop \u003Cid\u003E`; never empty.","type":"string","example":"Winkel Amsterdam"},"alias":{"description":"Upstream alias; null when the shop carries none.","type":"string","example":"winkel-amsterdam","nullable":true},"location_id":{"description":"Physical location \u2014 resolve with `GET \/api\/locations`.","type":"integer","example":1,"nullable":true},"relation_data_id":{"description":"The relation this shop belongs to.","type":"integer","example":2,"nullable":true}},"type":"object"},"ShopList":{"description":"The `GET \/api\/shops` envelope, alphabetical by name.","required":["count","total","data"],"properties":{"count":{"description":"Rows in `data`.","type":"integer","example":1},"total":{"description":"The upstream total for the query. The route is unpaged, so it is equal to `count` unless upstream reported a larger set.","type":"integer","example":1},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/Shop"}}},"type":"object"},"StockLevel":{"description":"One per-product, per-location stock row, as `GET \/api\/stock` emits it.","required":["id","shop_product_id","product_id","location_id","orderable_stock_value","unfulfilled_stock_value","value","in_stock","unlimited"],"properties":{"id":{"description":"The row\u0027s own top-level id from `ShopModule.listShopProductStocks`. **Which namespace this is has not been settled by a capture**, and the captured row is consistent with it being the shop_product_id rather than a stock-row id \u2014 so do not join on it. Use `product_id` for the inner product and `shop_product_id` for the shop one. Settling this needs `\/dump-storekeeper-api`, not a closer reading.","type":"integer","example":12},"shop_product_id":{"description":"The shop_product_id \u2014 the `{id}` of `GET`\/`PATCH \/api\/products\/{id}`. **Null on every captured row**: no `ShopModule.listShopProductStocks` payload on file carries this key, so treat a non-null value as unverified rather than expected.","type":"integer","nullable":true},"product_id":{"description":"The **inner** product id \u2014 what `PUT \/api\/product-prices` takes.","type":"integer","example":29,"nullable":true},"location_id":{"description":"Physical location. Rows that carry none are never returned \u2014 they cannot be placed against the caller\u0027s scope.","type":"integer","example":1,"nullable":true},"orderable_stock_value":{"description":"What may still be ordered.","type":"number","format":"float","example":42,"nullable":true},"unfulfilled_stock_value":{"description":"Ordered but not yet fulfilled.","type":"number","format":"float","example":3,"nullable":true},"value":{"description":"The stock level itself.","type":"number","format":"float","example":45,"nullable":true},"in_stock":{"description":"Upstream\u0027s own in-stock verdict.","type":"boolean","example":true,"nullable":true},"unlimited":{"description":"True for a product that never runs out; `value` is then not a limit.","type":"boolean","example":false,"nullable":true}},"type":"object"},"StockList":{"description":"The `GET \/api\/stock` page envelope.","required":["start","limit","count","total","data"],"properties":{"start":{"description":"Zero-based offset of the first row \u2014 echo of the `start` parameter.","type":"integer","example":0},"limit":{"description":"Page size \u2014 echo of the `limit` parameter.","type":"integer","example":50},"count":{"description":"Rows in `data`, **after** the location scope filter \u2014 so it can be lower than `total`.","type":"integer","example":1},"total":{"description":"The upstream total, **pre-filter**: it drives your paging, and there is no scoped total short of draining every page.","type":"integer","example":1},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/StockLevel"}}},"type":"object"},"TaxRate":{"description":"One tax rate, as `GET \/api\/tax-rates` emits it.","required":["id","name","value","country_iso2","alias"],"properties":{"id":{"description":"Tax rate id \u2014 what report rows carry as `tax_rate_id`.","type":"integer","example":2},"name":{"description":"Human-readable name; `\u0027\u0027` when upstream carried none.","type":"string","example":"BTW hoog"},"value":{"description":"The rate as a **decimal fraction**, e.g. `0.21` for 21% \u2014 not a percentage. Every captured value is a fraction (`0.2`, `0.09`, `0`), and the financial report divides by `1 + value` to strip VAT. Multiplying by `value \/ 100` under-charges VAT a hundredfold. Null when upstream carried none.","type":"number","format":"float","example":0.21,"nullable":true},"country_iso2":{"description":"ISO-3166-1 alpha-2 the rate applies in.","type":"string","example":"NL","nullable":true},"alias":{"description":"Stable alias, where upstream defines one.","type":"string","example":"high","nullable":true}},"type":"object"},"TaxRateList":{"description":"The `GET \/api\/tax-rates` envelope, ordered by country then rate.","required":["count","total","data"],"properties":{"count":{"description":"Rows in `data`.","type":"integer","example":4},"total":{"description":"The upstream total for the query. The route is unpaged, so it is equal to `count` unless upstream reported a larger set.","type":"integer","example":4},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/TaxRate"}}},"type":"object"},"TurnoverGroup":{"description":"One product (turnover) group, as `GET \/api\/turnover-groups` emits it.","required":["id","code","title","parent_id"],"properties":{"id":{"description":"Group id \u2014 what the financial report\u0027s `turnover_by_group.rows[].product_group_id` carries.","type":"integer","example":7},"code":{"description":"Your own group code, where one is set \u2014 what ledger mapping keys on.","type":"string","example":"8000","nullable":true},"title":{"description":"Group title; `\u0027\u0027` when upstream carried none.","type":"string","example":"Dranken"},"parent_id":{"description":"Parent group id when nested, else null.","type":"integer","example":null,"nullable":true}},"type":"object"},"TurnoverGroupList":{"description":"The `GET \/api\/turnover-groups` envelope, alphabetical by title.","required":["count","data"],"properties":{"count":{"description":"Rows in `data`.","type":"integer","example":1},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/TurnoverGroup"}}},"type":"object"},"User":{"description":"One back-office user, as `GET \/api\/users` emits it.","required":["id","email","lastLogin","Profile","language"],"properties":{"id":{"description":"Subuser id.","type":"integer","example":7},"email":{"description":"As Storekeeper stores it, in the original case \u2014 the lower-casing upstream is only used to drop pin-login and self-scan rows. Always contains an `@`. Compare case-insensitively.","type":"string","example":"Anna@Example.com"},"lastLogin":{"description":"Last sign-in timestamp, or null if they never have.","type":"string","example":"2026-09-16 08:12:44+02:00","nullable":true},"Profile":{"description":"The human profile label, e.g. `Admin`, falling back to the role alias.","type":"string","example":"Admin","nullable":true},"language":{"description":"Preferred UI language, where one is set.","type":"string","example":"nl","nullable":true}},"type":"object"},"UserList":{"description":"The `GET \/api\/users` page envelope.","required":["start","limit","count","total","total_upstream","data"],"properties":{"start":{"description":"Zero-based offset of the first row \u2014 echo of the `start` parameter.","type":"integer","example":0},"limit":{"description":"Page size \u2014 echo of the `limit` parameter.","type":"integer","example":50},"count":{"description":"Real users in `data`, after pin-login and self-scan rows are dropped.","type":"integer","example":1},"total":{"description":"What this endpoint returns \u2014 page by this, not by `total_upstream`. It used to be the upstream figure, computed before the pseudo-user rows were dropped, so paging by it walked past the end into empty pages (known-defect A2).","type":"integer","example":1},"total_upstream":{"description":"Storekeeper\u0027s own total, which still counts the pseudo-users this endpoint drops. Named rather than discarded because the gap between it and `total` is how much is being filtered.","type":"integer","example":3},"data":{"description":"This page\u0027s rows.","type":"array","items":{"$ref":"#\/components\/schemas\/User"}}},"type":"object"}},"securitySchemes":{"Bearer":{"type":"http","bearerFormat":"opaque","scheme":"bearer"}}},"security":[{"Bearer":[]}],"tags":[{"name":"Auth","description":"Introspect your bearer token (`\/api\/me`)."},{"name":"Orders","description":"Orders, single order with addresses, and line items."},{"name":"Products","description":"Catalog search, single product, and price rows."},{"name":"Stock","description":"Per-product, per-location stock levels."},{"name":"Customers","description":"Customer relations and customer segments."},{"name":"Reports","description":"Payment breakdown, daily close (Z-report), and per-product sales."},{"name":"Financial Report","description":"Structured financial report: revenue, VAT, turnover by group."},{"name":"Discount Orders","description":"Orders carrying a discount, for a date range."},{"name":"Shops","description":"Sales channels: resolves report `shop_id`."},{"name":"Tax Rates","description":"Tax rates: resolves report `tax_rate_id`."},{"name":"Turnover Groups","description":"Product groups: resolves report `product_group_id`."},{"name":"Payment Methods","description":"Payment method types: resolves `provider_method_type_id`."},{"name":"Locations","description":"Physical locations."},{"name":"Users"}]}