{"name":"BO Service Public API","version":"v1","description":"Read-mostly REST API over the BO Service data warehouse: sales, payments, card acquirer feeds, delivery orders, workforce, domain data, central-warehouse supply costs, POHODA accounting documents and Google reviews for the BO Service brand portfolio (around 320 active hospitality shops, 525 total including closed locations).","documentation_url":"https://api.boservices.cz/docs","openapi_url":"https://api.boservices.cz/openapi.json","llms_txt_url":"https://api.boservices.cz/llms.txt","llms_full_txt_url":"https://api.boservices.cz/llms-full.txt","health_url":"https://api.boservices.cz/v1/health","freshness_url":"https://api.boservices.cz/v1/meta/last-sync","authentication":{"type":"bearer","header":"Authorization: Bearer bos_live_...","how_to_get_a_key":"https://dw.boservices.cz/settings/api-keys (dashboard admins only)","scopes":"A key is either wildcard (all brands) or limited to a brand list. Writes additionally require the can_write_pairings scope.","note":"Never put the key in a query string or a log. Out-of-scope reads normally return an empty list or a 404 rather than a 403; three endpoints return 403 instead: GET /v1/card-reconciliation, when a requested shop is outside your scope, and the two wildcard-key-only endpoints GET /v1/shops/pairings and GET /v1/shops/accounting-invoices. Those two answer 403 rather than an empty list because the shop-location crosswalk is a security boundary the portal builds franchisee scope from, AND because an empty list is a real state of its data - it held nothing at all until the one-time Redis import ran on 2026-07-28, and a filtered read can still come back empty - so an empty list could not be told apart from 'your key cannot see this', not even afterwards in the audit log, where both are a 200 with row_count 0. GET /v1/domains and GET /v1/supply/places do return an empty list to a brand-scoped key: empty is not a real state of their data, so it carries no ambiguity."},"endpoint_groups":{"revenue":"https://api.boservices.cz/v1/revenue/daily","analytics":"https://api.boservices.cz/v1/analytics/payment-mix","receipts":"https://api.boservices.cz/v1/receipts","products":"https://api.boservices.cz/v1/products","dimensions":"https://api.boservices.cz/v1/shops","card":"https://api.boservices.cz/v1/card-transactions","labor":"https://api.boservices.cz/v1/labor/daily","delivery":"https://api.boservices.cz/v1/choice-orders","supply":"https://api.boservices.cz/v1/supply/orders","accounting":"https://api.boservices.cz/v1/pohoda/documents","reviews":"https://api.boservices.cz/v1/reviews/shops","domains":"https://api.boservices.cz/v1/domains"},"conventions":{"dates":"Shop-local calendar days (Europe/Prague), matching the POS daily close.","money":"Every row carries gross (incl. VAT), net (excl. VAT) and vat. Revenue is net of refunds.","currency":"One row per currency. Amounts are never FX-converted.","pagination":"page (0-based) + limit on paginated list endpoints, returned in a meta {page, limit, total} block. There is no offset parameter; see the OpenAPI spec per endpoint.","caching":"ETag and Last-Modified on cacheable reads; send If-None-Match to get a 304.","errors":"JSON envelope {\"error\":{\"code\",\"message\",\"status\"}} with a closed set of codes.","correlation":"Every response carries X-Request-Id. Quote it when reporting a problem.","query_timeout":"A single query is cancelled after 20 s and surfaces as a plain 500 SERVER_ERROR (no dedicated code) - it means the query was too expensive, and an identical retry will time out again. Narrow the date window, add shop_id/shop_ids, and page SEQUENTIALLY: every page recomputes meta.total over the whole window, so a parallel fan-out over pages pays that cost once per page, all at the same time."},"rate_limit":{"default_per_minute":60,"window":"fixed","note":"Fixed 60-second window anchored to the key's first request in it, not a rolling window - a client can burst up to roughly 2x the limit across a window boundary. Counted per serverless instance, so the effective ceiling under concurrency can exceed the nominal limit.","headers":["RateLimit","RateLimit-Policy","X-RateLimit-Limit","X-RateLimit-Remaining","X-RateLimit-Reset"],"on_exceeded":"429 with Retry-After (seconds)."},"versioning":"URI versioning. Breaking changes ship as /v2 alongside /v1; v1 then keeps running for at least 6 months with Deprecation and Sunset headers."}