Zpět

Profi API

Contract version 2026-09-17

One REST API for the whole Profi family. Your integration drives the same actions a person clicks in the app — so it inherits rights, numbering, workflow and stock valuation.

Portál je záměrně anglicky: integraci staví vývojář, který česky umět nemusí. Česky zůstává nápověda přímo v aplikaci — jak si založit klíč, co je sandbox a kde najdete log volání.

Create a key in the app

Quickstart

Everything below works with a test key against the shadow demo account, so nothing here touches production data.

  1. 1. Create a key

    In the app go to Settings → API and create a key. Start with a test key (pb_test_…): it runs against the shadow demo account and every response carries livemode: false. The secret is shown once — put it in an environment variable, never in a URL or your repository.

  2. 2. Pick scopes

    Scopes are per module and have two levels, read and write. Take a preset (E-shop, Accounting, Warehouse, Read-only) unless you know exactly what you need. Effective scopes are the key's scopes intersected with the modules the account subscribes to today, so dropping an add-on drops the scope without anyone editing the key. Deleting records through the API is never possible.

  3. 3. Call the API

    Send the key as a bearer token. Every response carries X-Request-Id — quote it when you ask us about a specific call.

    export K=pb_test_…
    
    curl -s https://www.profibrew.com/api/v1/me \
      -H "Authorization: Bearer $K"
  4. 4. Receive events

    Register a webhook endpoint instead of polling. Every event stays readable through GET /events as well, so you can catch up after an outage without losing anything.

Concepts

The nine things worth knowing before you write the first line of integration code.

Authentication

A bearer token in the Authorization header. Keys are either live (pb_live_…) or test (pb_test_…); both work on every host of the Profi family. Keys can be restricted to a list of addresses and can carry an expiry — an expiring key is announced by e-mail before it stops working.

Sandbox

A test key talks to the shadow demo account of the same brewery: real structure, disposable data. Requests carry X-Profi-Sandbox: 1 and responses livemode: false. Build the whole integration against it — the recipes below are written to run there unchanged.

Quantities and units

Every quantity travels with its unit and the API NEVER converts it. On reads you get quantity plus a unit object (id, code, symbol, dimension, decimals); on writes you send quantity plus exactly one of unit_id or unit_code. Document lines and stock levels are in the unit of the item.

{
  "item_id": "…",
  "quantity": "24",
  "unit_code": "ks"
}

Ask GET /units first — it returns the dictionary this account may use (system units for its unit system plus its own). Sending a unit the account does not have gives 422 unit_not_available, not a silent conversion.

Pagination and incremental sync

Lists take limit (1–200, default 50) and an opaque cursor, and return data, next_cursor and has_more. For incremental sync pass updated_since or created_since instead of walking every page — both take an ISO 8601 instant.

curl -s "https://www.profibrew.com/api/v1/items?limit=100&updated_since=2026-09-01T00:00:00Z" \
  -H "Authorization: Bearer $K"

Idempotency

Send an Idempotency-Key header on writes. Repeating the same key with the same body replays the stored response for 24 hours, so a network timeout can be retried without creating a document twice. The same key with a different body is refused with 422 idempotency_key_reused; a request still in flight gives 409 idempotency_in_progress.

curl -s -X POST https://www.profibrew.com/api/v1/partners \
  -H "Authorization: Bearer $K" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8c2e5a8e-1b2f-4c3d-9e0f-1a2b3c4d5e6f" \
  -d '{"name":"Hostinec U Dvou koček","external_ref":"eshop:10547","is_customer":true}'

Your own identifiers

Instead of keeping a mapping table, stamp records with external_ref in the form <system>:<id>, for example eshop:10547. It is unique per account, so a duplicate create is refused with 409 external_ref_conflict and the response names the record that won.

Concurrent edits

Reads return an ETag. Send it back as If-Match on an update and a change someone made meanwhile is refused with 412 stale_version instead of quietly overwriting their work.

Errors

Every error is an RFC 9457 problem document with content type application/problem+json. Branch on the machine-readable code, never on title or detail. Validation failures list the offending rows in errors[], and type links straight to the matching entry in the Errors table below.

Rate limits

Every response carries the IETF RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and RateLimit-Policy headers. Over the limit you get 429 rate_limited with Retry-After in seconds — wait that long rather than retrying immediately. The ceiling comes from the account's plan.

Versioning

The contract is dated, not semver. The version travels in the X-Profi-Api-Version response header and in the api_version field of every event. Additive changes — a new field, a new endpoint, a new event type — happen without a new version, so parse defensively and ignore fields you do not know.

Scopes

A key carries the scopes below. Reading needs :read, creating, updating and running workflow actions need :write. In the OpenAPI document, x-scopes lists the scopes of which the key needs at least one; x-additional-scopes lists scopes it needs on top — recording a POS sale also posts the payment, so it needs finance:write, and stock:write when the cash desk moves stock.

  • core:read
  • core:write
  • stock:read
  • stock:write
  • sales:read
  • sales:write
  • finance:read
  • finance:write
  • brewery:read
  • brewery:write
  • pos:read
  • pos:write
  • events:read
  • webhooks:manage
  • workflow:write

Webhooks

Register an https endpoint and we deliver domain events to it as they happen. Delivery is at-least-once and unordered — treat both as given.

Verifying the signature

Every delivery is signed per the Standard Webhooks specification, the same scheme Svix and Resend use, so an off-the-shelf library verifies it. Three headers travel with the request: webhook-id, webhook-timestamp and webhook-signature. The signed content is id.timestamp.body — HMAC-SHA256 with the decoded part of your whsec_ secret, base64 encoded, sent as v1,<signature>.

import { Webhook } from "standardwebhooks";

// Ověřuj SUROVÉ tělo, teprve pak ho parsuj.
app.post("/profi-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const wh = new Webhook(process.env.PROFI_WEBHOOK_SECRET); // whsec_…
  let event;
  try {
    event = wh.verify(req.body, {
      "webhook-id": req.header("webhook-id"),
      "webhook-timestamp": req.header("webhook-timestamp"),
      "webhook-signature": req.header("webhook-signature"),
    });
  } catch {
    return res.sendStatus(400);
  }

  res.sendStatus(204);        // potvrď hned…
  void enqueue(event);        // …a teprve pak dělej práci
});

Verify against the RAW body before parsing it, reject a timestamp more than five minutes from now (that is the replay window we allow), and compare in constant time. During a rotation the header can carry several space-separated signatures — one match is enough.

Retries

A delivery counts as successful on any 2xx. Anything else is retried: the first attempt runs immediately, then seven more with growing gaps, about 58 hours in total, after which the delivery is marked dead and your admins are notified. A redirect is never followed and 410 Gone retires the endpoint immediately.

1 min · 5 min · 30 min · 2 h · 8 h · 24 h · 24 h

50 consecutive failures disable the endpoint, and the brewery sees a card about it in the app. Re-enable it in Settings → API once your receiver is healthy again.

Writing the receiver

Answer 2xx as soon as you have stored the event and do the real work afterwards — we time out after 10 seconds. Deduplicate on webhook-id, because at-least-once means the same event can arrive twice. Order is not guaranteed either: use the sequence field, which is monotonic per account, to decide what is newer.

Avoiding loops

An endpoint can be told to skip events its own key caused (exclude own events). Without it, writing through the API produces an event that your own receiver picks up and acts on again.

Catching up

Events are readable through GET /events with since_sequence, so a receiver that was down does not need a redelivery from us. Going back further than your plan's retention returns 410 events_expired — do a full sync rather than silently skipping the gap.

curl -s "https://www.profibrew.com/api/v1/events?since_sequence=4812&types=invoice.*" \
  -H "Authorization: Bearer $K"

Event types

Subscribe to exact types, to a whole prefix such as invoice.*, or to everything with *.

  • partner.created
  • partner.updated
  • item.created
  • item.updated
  • order.created
  • order.confirmed
  • order.shipped
  • order.delivered
  • order.invoiced
  • order.cancelled
  • stock_issue.created
  • stock_issue.confirmed
  • stock_issue.cancelled
  • stock_issue.reverted_to_draft
  • goods_receipt.created
  • goods_receipt.confirmed
  • goods_receipt.cancelled
  • goods_receipt.reverted_to_draft
  • stock_level.changed
  • invoice.created
  • invoice.issued
  • invoice.paid
  • invoice.payment_reverted
  • invoice.cancelled
  • received_invoice.created
  • received_invoice.confirmed
  • received_invoice.paid
  • received_invoice.payment_reverted
  • received_invoice.cancelled
  • cashflow.created
  • cashflow.paid
  • cashflow.payment_reverted
  • cashflow.cancelled
  • inbox.submission_received
  • workflow.step_called
  • workflow.step_waiting_external
  • workflow.step_completed_external
  • batch.phase_changed
  • batch.measurement_added

Recipes

Complete integrations end to end, with the call order, the error codes you will actually hit and runnable curl examples.

E-shop → objednávka → výdejka → faktura

Tenhle recept popisuje, jak napojit e-shop na ProfiBrew: synchronizaci katalogu a partnerů, založení objednávky z e-shopové sady a její doběhnutí až k zaplacené faktuře — přesně tou cestou, kterou by klikal člověk v aplikaci, jen přes API.

1. Cíl

Po dokončení receptu umí vaše integrace:

  1. udržovat katalog položek a partnerů v ProfiBrew v souladu s e-shopem (obousměrné mapování přes external_ref),
  2. založit objednávku z e-shopové objednávky se správnými množstvími a jednotkami,
  3. nechat ji projít workflow (potvrzení → výdejka → faktura → úhrada) pomocí akcí z katalogu, stejně jako by to udělal obchodník v UI,
  4. zjistit dostupnost zboží před přijetím objednávky.

2. Klíč, sandbox a scopes

Než začnete integrovat proti ostrým datům, založte si v /settings/api testovací klíč (pb_test_…) — běží nad stínovým DEMO tenantem, takže si můžete recept vyzkoušet bez rizika pro produkční data. Testovací a ostrý klíč fungují na obou hostech rodiny Profi stejně, jen s hlavičkou X-Profi-Sandbox: 1 navíc a livemode: false v odpovědích.

Zvolte předdefinovanou sadu scopes E-shop:

core:read, core:write, stock:read, sales:read, sales:write, events:read

Tahle sada NEobsahuje finance:write — přesto stačí na fakturaci, protože POST /invoices a actions/create_invoice přijímají i sales:write (faktura vzniklá z objednávky patří pod Obchod). Vyžadovaný scope u každého volání je v hlavičce odpovědi OpenAPI dokumentu (x-scopes) a v tabulkách níže.

Ukládejte klíč do proměnné prostředí, nikdy ne do URL:

export K=pb_test_…

3. Základní URL

https://www.profibrew.com/api/v1

(zákazníci na značce ProfiEkonom používají https://app.profifirma.cz/api/v1 — stejný klíč funguje na obou hostech).

4. Krok 0 — kdo jsem a v čem tenant měří

curl -s -H "Authorization: Bearer $K" \
  https://www.profibrew.com/api/v1/me | jq '{tenant, scopes, modules, measure_profile}'

curl -s -H "Authorization: Bearer $K" \
  https://www.profibrew.com/api/v1/units | jq '.data[] | {id, code, dimension, decimals}'

GET /v1/units vrací číselník jednotek dostupných tomuto tenantovi (systémové podle jeho měrné soustavy + vlastní). Každé množství v API se na jednu z nich odkazuje přes unit_id nebo unit_code — API množství NIKDY nepřevádí, takže znát nabídku tenanta předem se vyplatí.

5. Krok 1 — synchronizace katalogu

Pro každou položku e-shopu nejdřív zkuste najít existující záznam podle vaší reference:

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/items/by-external-ref/shoptet/10234"

404 not_found → položka ještě neexistuje, založte ji s external_ref a ?on_conflict=return (souběžný druhý pokus se stejnou referencí tak nespadne na 409, ale vrátí existující záznam):

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: 8f14e45f-ceea-467e-bd3f-b7a1f1a2a3b4" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ležák 12° 0,5 l",
    "item_type": "purchased_sale_item",
    "unit": { "unit_code": "ks" },
    "sale_price": "34.90",
    "external_ref": { "system": "shoptet", "id": "10234" }
  }' \
  "https://www.profibrew.com/api/v1/items?on_conflict=return" | jq '{id, unit, external_ref}'

Na existující položku pak posílejte jen PATCH se změněnými poli (tělo je striktní — neznámé pole vrátí 422 unknown_field, nikdy se tiše neignoruje):

curl -s -X PATCH -H "Authorization: Bearer $K" \
  -H "Content-Type: application/json" \
  -H "If-Match: $ETAG" \
  -d '{"sale_price": "36.90"}' \
  "https://www.profibrew.com/api/v1/items/$ITEM_ID"

If-Match (ETag z předchozího GET, hlavička ETag: W/"…") je nepovinný — bez něj platí last-write-wins jako v UI; s ním chyba nesouhlasu vrátí 412 stale_version a integrace ví, že si má záznam znovu načíst.

Ceny pro konkrétního partnera a množství (kaskáda partner → ceník → sleva → základní cena, stejná jako v UI) zjistíte přes:

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/items/$ITEM_ID/prices?partner_id=$PARTNER_ID&quantity=6" \
  | jq '{unit_price, currency, price_source}'

quantity je vždy v jednotce položky — endpoint jednotku pro dotaz nepřijímá (jednotka je odvozená z položky samotné).

6. Krok 2 — synchronizace partnerů

Stejný vzor jako u položek — nejdřív by-external-ref, pak POST s ?on_conflict=return:

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: 3d3f0b3a-8b0b-4e9a-9a3a-2d3f0b3a8b0b" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Hospoda U Zeleného stromu",
    "is_customer": true,
    "ico": "12345678",
    "email": "objednavky@hospoda.cz",
    "external_ref": { "system": "shoptet", "id": "cust-9981" }
  }' \
  "https://www.profibrew.com/api/v1/partners?on_conflict=return" | jq '{id, external_ref}'

Pokud jde partner dohledávat i podle IČO (bez vaší reference), počítejte s 409 duplicate_ico, když v ProfiBrew už existuje partner se stejným IČO pod jiným external_ref — odpověď nese existingName. V tom případě dohledejte partnera přes GET /v1/partners?search=<ičo> a napárujte external_ref sami (PATCH partnera).

7. Krok 3 — dostupnost zboží

Než objednávku přijmete, ověřte sklad:

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/stock-levels?item_id=$ITEM_ID&warehouse_id=$WAREHOUSE_ID" \
  | jq '.data[] | {quantity, available_quantity, unit}'

available_quantity = quantity − reserved_quantity (nikdy záporně). Množství je vždy v jednotce položky, stejně jako všude v API.

8. Krok 4 — založení objednávky

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: 3fa85f64-5717-4562-b3fc-2c963f66afa6" \
  -H "Content-Type: application/json" \
  -d '{
    "partner_id": "'"$PARTNER_ID"'",
    "warehouse_id": "'"$WAREHOUSE_ID"'",
    "external_ref": { "system": "shoptet", "id": "ORD-2044" },
    "lines": [
      { "item_id": "'"$ITEM_ID"'", "quantity": "6", "unit": { "unit_id": "'"$UNIT_ID"'" } }
    ]
  }' \
  "https://www.profibrew.com/api/v1/orders?on_conflict=return" | jq '{id, number, status, allowed_actions}'

Poznámky:

  • unit na řádku musí být jednotka POLOŽKY — jiná vrátí 422 unit_mismatch s expected_unit v těle chyby. API množství nikdy nepřevádí (§3.4 specu S107); pošlete quantity rovnou v jednotce, kterou vrátila položka v kroku 1.
  • unit_price na řádku můžete vynechat — dopočítá se stejnou cenovou kaskádou jako v kroku 1.
  • warehouse_id je povinné, jen když má účet víc než jednu aktivní provozovnu (jinak 422 shop_required — provozovna objednávky se odvozuje ze skladu).
  • Objednávka vzniká jako KONCEPT (status: "draft") s origin: "api" — v UI se u ní zobrazí odznak „přes API (název klíče)".

9. Krok 5 — průchod workflow přes akce

Stavové přechody NEJDOU přímo přes PATCH status — jen přes POST …/actions/{key}, s klíči z téhož katalogu, který pohání tlačítka v UI. Aktuální doklad vždy nese allowed_actions — na ně se spolehněte místo natvrdo zapsané posloupnosti:

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  "https://www.profibrew.com/api/v1/orders/$ORDER_ID/actions/confirm" \
  | jq '{status, allowed_actions}'

Vyskladnění (vytvoří výdejku v konceptu, objednávka zůstává ve stejném stavu — potvrzení výdejky je samostatný krok):

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  "https://www.profibrew.com/api/v1/orders/$ORDER_ID/actions/create_stock_issue" \
  | jq '.created'
# → { "object": "stock_issue", "id": "…" }

STOCK_ISSUE_ID=…  # z odpovědi výše

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  "https://www.profibrew.com/api/v1/stock-issues/$STOCK_ISSUE_ID/actions/confirm" \
  | jq '{status}'

Zásoba se odepíše až tímhle potvrzením (CLAUDE.md: žádný pohyb bez potvrzeného dokladu) — do té doby je výdejka jen koncept.

Fakturace (přepne objednávku na invoiced):

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  "https://www.profibrew.com/api/v1/orders/$ORDER_ID/actions/create_invoice" \
  | jq '.created'
# → { "object": "invoice", "id": "…" }

INVOICE_ID=…

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  "https://www.profibrew.com/api/v1/invoices/$INVOICE_ID/actions/issue" \
  | jq '{status, number, allowed_actions}'

(Ekvivalentní zkratka bez objednávkové akce: POST /v1/invoices s tělem { "order_id": "…" } — vytvoří i vystaví ve dvou krocích stejně.)

Úhrada je MIMO katalog workflow (platební engine S89), ale připojuje se stejně přes actions/:

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"paid_at": "2026-09-15"}' \
  "https://www.profibrew.com/api/v1/invoices/$INVOICE_ID/actions/mark_paid" \
  | jq '{status, amount_paid, amount_remaining}'

Bez account_id se použije výchozí účet tenanta pro platební metodu faktury — pokud žádný není nastavený, vrátí se 422 account_required a je potřeba poslat account_id. Dostupné účty vypíšete přes:

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/finance-accounts" \
  | jq '.data[] | {id, type, name, currency}'

10. Inkrementální synchronizace

Pro pravidelné dotahování změn (ceny, sklad, stavy objednávek) používejte updated_since + kurzor, ne plný re-scan:

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/orders?updated_since=2026-09-14T00:00:00Z&limit=100" \
  | jq '{next_cursor, has_more}'

Když je has_more: true, zavolejte znovu se stejným updated_since a cursor=<next_cursor>. Uložte si poslední zpracovaný updated_at (případně přímo next_cursor) jako checkpoint pro příští běh.

11. Chyby, na které v tomhle receptu narazíte

KódKdyCo s tím
401 unauthenticatedchybí/špatný klíčzkontrolujte hlavičku Authorization: Bearer …
403 insufficient_scopesada nemá potřebný scope (required_scopes v těle)přidejte scope klíči v /settings/api, nebo přepněte na sadu Vlastní
403 module_not_enabledtenant nemá modul v předplatnémřešení je na straně zákazníka (upgrade)
404 not_foundšpatné id, nebo cizí tenantověřte, že id patří stejnému livemode (test/live)
409 external_ref_conflictexternal_ref už je použitýzavolejte znovu s ?on_conflict=return, nebo použijte existing_id z těla chyby / hlavičku Location
409 duplicate_icopartner se stejným IČO existujedohledejte partnera přes GET /partners?search=, napárujte referenci
409 invalid_transitionakce mimo allowed_actionspřečtěte allowed_actions z těla chyby — někdo mezitím doklad posunul jinam
412 stale_versionIf-Match nesedí s aktuální verzínačtěte záznam znovu, zopakujte PATCH s novým ETagem
422 unit_mismatchjednotka řádku ≠ jednotka položkypošlete quantity v jednotce z GET /items/{id}
422 quantity_scalevíc desetinných míst, než dovoluje jednotkazaokrouhlete na decimals jednotky sami — server nezaokrouhluje mlčky
422 shop_requiredvíc aktivních provozoven, chybí warehouse_idpošlete warehouse_id
422 account_requiredmark_paid bez výchozího účtuvypište GET /finance-accounts a pošlete account_id
429 rate_limitedpřekročen limit tarifupočkejte podle Retry-After, sledujte hlavičky RateLimit-*

12. Checklist před nasazením do produkce

  • Testováno na pb_test_… proti sandboxu (DEMO tenant), ne jen lokálně.
  • Přechod na pb_live_… klíč se STEJNOU sadou scopes, jakou jste testovali — ověřte GET /v1/me po přepnutí (scopes, modules).
  • Každý POST nese unikátní Idempotency-Key (UUID na request, ne na typ operace) — bezpečný retry bez duplicit.
  • Sync položek a partnerů běží přes external_ref + ?on_conflict=return, ne přes hledání podle jména.
  • Množství se posílá v jednotce, kterou vrátil GET /items/{id} — žádný lokální převod jednotek před odesláním.
  • Přechody stavů čtou allowed_actions z odpovědi, nemají napevno zadrátovanou posloupnost kroků.
  • Ošetřen 429 s exponenciálním backoffem podle Retry-After.
  • X-Request-Id z hlaviček se loguje u každého volání — usnadní to podporu při hlášení problému.
  • Ověřeno chování při zrušení scope/modulu (odebraný addon = okamžitě odebraný effective scope, i když ho klíč pořád má uložený).

Updated 2026-09-15

Účetní software — vydané a přijaté faktury, peněžní deník

Tenhle recept popisuje, jak napojit účetní/ekonomický systém na ProfiBrew jako čtenáře dokladů (vydané faktury, přijaté faktury, peněžní deník) a zapisovatele úhrad — tedy typický tok pro externí účtárnu nebo účetní kancelář, která si doklady stahuje a zpětně hlásí, co je zaplacené.

1. Cíl

Po dokončení receptu umí vaše integrace:

  1. pravidelně stahovat nové a změněné vydané faktury, přijaté faktury a doklady peněžního deníku (updated_since + kurzor),
  2. založit přijatou fakturu ručně (dorazila poštou, ne skenem),
  3. zapsat úhradu dokladu s konkrétním účtem,
  4. naimportovat naskenovanou nebo ISDOC fakturu přes přílohu a digitální podatelnu (nebo zkratkou pro malé soubory rovnou).

2. Klíč, sandbox a scopes

Založte si testovací klíč (pb_test_…) a sadu Účetnictví:

core:read, core:write, finance:read, finance:write, sales:read, events:read

sales:read je v sadě proto, že vydané faktury (S82) patří u tenantů s modulem Obchod pod něj — bez tohoto scope by GET /invoices u takového tenanta vrátil 403 insufficient_scope, i když má klíč finance:read. core:write je potřeba na nahrávání přílohy a podatelnu v kroku 3 (POST /attachments, /attachments/finalize, /inbox/submissions) — bez něj tahle tři volání vrátí 403 insufficient_scope, i když zbytek receptu běží jen na finance:*.

export K=pb_test_…

Základní URL: https://www.profibrew.com/api/v1 (nebo https://app.profifirma.cz/api/v1 pro účetní kanceláře pracující se zákazníky na značce ProfiEkonom — stejný klíč funguje na obou).

3. Krok 1 — stahování vydaných faktur

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/invoices?updated_since=2026-09-01T00:00:00Z&limit=100" \
  | jq '{next_cursor, has_more, count: (.data | length)}'

Seznam NENESE řádky faktur (kvůli velikosti stránky) — pro položky zavolejte detail:

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/invoices/$INVOICE_ID" \
  | jq '{number, status, totals, amount_paid, amount_remaining, lines}'

amount_remaining je vždy dopočítané read-only pole (totals.incl_vat − amount_paid) — nepočítejte si ho znovu sami, ušetříte si zaokrouhlovací rozdíly.

Pokračujte stránkováním, dokud has_more nespadne na false:

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/invoices?updated_since=2026-09-01T00:00:00Z&cursor=$NEXT_CURSOR&limit=100"

Uložte si poslední zpracovaný updated_at jako checkpoint dalšího běhu — kurzor sám o sobě mezi jednotlivými spuštěními neuchovávejte (je platný jen pro danou stránkovací sekvenci updated_since).

4. Krok 2 — stahování přijatých faktur

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/received-invoices?updated_since=2026-09-01T00:00:00Z&limit=100" \
  | jq '.data[] | {id, supplier_invoice_number, status, totals, paid_amount}'

Na rozdíl od vydané faktury zůstává přijatá faktura editovatelná i po confirm — zamyká ji až export do účetnictví nebo storno (Finance 3.0 R3). Pokud vaše integrace přijatou fakturu i zakládá ručně (doklad, který nedorazil skenem):

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: 6b2f6a2e-1c3d-4e5f-8a9b-0c1d2e3f4a5b" \
  -H "Content-Type: application/json" \
  -d '{
    "supplier_invoice_number": "2026-441",
    "partner_id": "'"$PARTNER_ID"'",
    "issue_date": "2026-09-10",
    "due_date": "2026-09-24",
    "external_ref": { "system": "pohoda", "id": "PF-441" },
    "lines": [
      { "description": "Nákup obalového materiálu", "amount": "4200.00", "vat_rate_pct": "21" }
    ]
  }' \
  "https://www.profibrew.com/api/v1/received-invoices?on_conflict=return" | jq '{id, status}'

Řádky přijaté faktury NENESOU množství ani jednotku — je to čistě účetní rozpad (description, amount bez DPH, vat_rate_pct, volitelná category_id), ne skladový pohyb.

Potvrzení (přidělí interní číslo, vyžaduje aspoň jeden řádek):

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  "https://www.profibrew.com/api/v1/received-invoices/$RECEIVED_INVOICE_ID/actions/confirm" \
  | jq '{status, number, allowed_actions}'

5. Krok 3 — import ze skenu / ISDOC

Import naskenované nebo ISDOC faktury jde jinou cestou než ruční POST /received-invoices výše — přes přílohu a digitální podatelnu.

5.1 Nahrání souboru podepsanou URL

Vyžádejte upload ticket. Přílohu zakládejte BEZ entity_type/entity_id — soubor zatím nepatří k žádné konkrétní faktuře, jen čeká v podatelně:

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Content-Type: application/json" \
  -d '{
    "file_name": "faktura-2026-441.isdoc",
    "mime_type": "application/xml",
    "file_size": 18432
  }' \
  "https://www.profibrew.com/api/v1/attachments" \
  | jq '{path, upload_url, upload_method, upload_headers, expires_at}'

Odpověď nese path (opaque, pošlete ho beze změny do finalize níže), upload_url a upload_headersid přílohy v tomhle kroku ještě neexistuje. Nahrajte bajty PUTem přesně podle vrácených hlaviček (žádná další autentizace — token v URL opravňuje přesně tuhle cestu jednou):

curl -s -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/xml" \
  -H "x-upsert: false" \
  --data-binary @faktura-2026-441.isdoc

Založte záznam přílohy (entity_type/entity_id musí sedět s tím, co jste poslali do prvního volání — tady opět oba vynechané):

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "'"$PATH"'",
    "file_name": "faktura-2026-441.isdoc",
    "mime_type": "application/xml"
  }' \
  "https://www.profibrew.com/api/v1/attachments/finalize" \
  | jq '{id, purpose, created_at}'

Teprve teď máte id přílohy (ATTACHMENT_ID níže). Skutečná velikost se ověřuje z úložiště až tady — neshoda nebo překročení kvóty plánu selže na tomhle volání, ne na prvním. GET /v1/attachments/{id}/content kdykoli vrátí 302 na aktuální podepsanou URL k bajtům.

5.2 Přímý import jako ISDOC

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "attachment_id": "'"$ATTACHMENT_ID"'",
    "file_name": "faktura-2026-441.isdoc"
  }' \
  "https://www.profibrew.com/api/v1/received-invoices/isdoc" \
  | jq '{received_invoice_id, supplier_name, document_number, pdf_attached}'

file_name musí končit .isdoc/.isdocx — podle přípony se řídí parsování, nezávisle na deklarovaném MIME typu. Duplicitní dokument (napárovaný přes ISDOC UUID) vrátí 409 duplicate; jiný důvod odmítnutí vrátí 422 s kódem jako code (unsupported_document_type, invalid_xml, no_invoice_in_archive, file_too_large, no_finance_module, …).

5.3 Nebo přes obecnou podatelnu

Stejnou přílohu můžete místo přímého importu poslat do obecné digitální podatelny — ISDOC se tam rozpozná a naimportuje automaticky, ostatní typy čekají v agendě Soubory na ruční roztřídění (nebo jdou rovnou do AI podatelny, podle nastavení tenanta):

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Content-Type: application/json" \
  -d '{"attachment_id": "'"$ATTACHMENT_ID"'"}' \
  "https://www.profibrew.com/api/v1/inbox/submissions" \
  | jq '{outcome, attachment_id, received_invoice_id, note, duplicate}'

outcome říká, co se stalo: isdoc_imported (vznikla přijatá faktura), isdoc_rejected (vypadalo to na ISDOC, ale import se nepovedl — důvod v note), files (čeká na ruční roztřídění), scan (šlo rovnou do AI podatelny). Ve zdrojových metadatech výsledného dokladu se ukáže „přes API", stejně jako u e-mailové podatelny.

5.4 Zkratka pro malé soubory

Soubor do 4 MB nemusíte nahrávat podepsanou URL — pošlete ho rovnou jako multipart přímo do podatelny a přeskočte krok 5.1:

curl -s -X POST -H "Authorization: Bearer $K" \
  -F "file=@faktura-2026-441.isdoc;type=application/xml" \
  "https://www.profibrew.com/api/v1/inbox/submissions" \
  | jq '{outcome, received_invoice_id}'

Nad 4 MB endpoint vrátí 413 payload_too_large — nad tuhle hranici jde soubor jedině cestou z 5.1 (POST /attachments → PUT → finalize).

Idempotency-Key je bezpečný i na multipart nahrání — server pro otisk požadavku počítá ze syrových bajtů těla, ne z dekódovaného textu, takže binární obsah přílohy se u shodného opakovaného volání nezkreslí a POST /inbox/submissions s multipart tělem lze retryovat úplně stejně jako kterékoliv jiné zapisující volání.

6. Krok 4 — zápis úhrady

Když potřebujete konkrétní účet (tenant nemá nastavený výchozí pro platební metodu dokladu, nebo chcete platbu přiřadit jinam), vypište dostupné účty:

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/finance-accounts" \
  | jq '.data[] | {id, type, name, currency, is_active}'

Zůstatky se přes API nevystavují — endpoint slouží jen k dohledání id pro account_id níže.

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"paid_at": "2026-09-15", "account_id": "'"$ACCOUNT_ID"'"}' \
  "https://www.profibrew.com/api/v1/invoices/$INVOICE_ID/actions/mark_paid" \
  | jq '{status, paid_at: .paid_at, amount_paid}'

Stejná akce existuje i pro přijaté faktury (POST /received-invoices/{id}/actions/mark_paid) a pro doklady peněžního deníku (níže). Bez amount se použije celý zbývající zůstatek; bez account_id výchozí účet tenanta pro platební metodu dokladu — pokud žádný není nastavený, vrátí se 422 account_required.

Storno úhrady (vrátí doklad do stavu před zaplacením):

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  "https://www.profibrew.com/api/v1/invoices/$INVOICE_ID/actions/revert_payment"

7. Krok 5 — peněžní deník (Zjednodušené / Interní doklady)

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/cashflows?updated_since=2026-09-01T00:00:00Z&type=expense" \
  | jq '.data[] | {id, number, type, status, amount, paid_amount}'

Nový doklad (ve výchozím stavu rovnou potvrzený — status může vynutit draft/cancelled):

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: 1a2b3c4d-5e6f-7081-92a3-b4c5d6e7f809" \
  -H "Content-Type: application/json" \
  -d '{
    "cashflow_type": "expense",
    "amount": "1500.00",
    "date": "2026-09-15",
    "description": "Parkovné a drobný nákup",
    "payment_method": "cash"
  }' \
  "https://www.profibrew.com/api/v1/cashflows" | jq '{id, number, status}'

Peněžní deník NEMÁ external_ref (sloupec v DB je legacy jednorázový import, API ho nevystavuje ani nezapisuje), ani PATCH, ani řádky — jen hlavičku a dvě akce (mark_paid, cancel), odvozené přímo ze statusu, ne z katalogu workflow.

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  "https://www.profibrew.com/api/v1/cashflows/$CASHFLOW_ID/actions/cancel"

8. Chyby, na které v tomhle receptu narazíte

KódKdyCo s tím
401 unauthenticatedchybí/špatný klíčzkontrolujte hlavičku Authorization
403 insufficient_scopechybí sales:read u tenanta s Obchodemdoplňte scope, viz krok 2 výše
403 module_not_enabledtenant nemá modul Finance/Obchod v předplatnémmimo dosah integrace
404 not_foundšpatné id, nebo cizí livemodeověřte test/live
409 invalid_transition (EXPORTED)přijatá faktura byla už exportovaná do účetnictvíúpravy po exportu API nedovoluje — stejné jako v UI
409 invalid_transition (ostatní)akce mimo allowed_actions (např. mark_paid na draft)přečtěte allowed_actions v těle chyby
409 duplicatePOST /received-invoices/isdoc na dokument se stejným ISDOC UUIDbezpečné volat opakovaně, faktura z prvního importu už existuje
412 stale_versionIf-Match na PATCH přijaté faktury nesedíznovu načtěte a zopakujte s aktuálním ETagem
413 payload_too_largemultipart POST /inbox/submissions nad 4 MBnahrajte přes POST /attachments → PUT → finalize, pak pošlete { attachment_id }
422 account_requiredmark_paid bez výchozího účtu a bez account_idvypište GET /finance-accounts a pošlete account_id, nebo nechte zákazníka nastavit výchozí účet
422 no_linesconfirm na přijaté faktuře bez řádkůpřidejte aspoň jeden řádek přes POST …/lines
422 <reason>ISDOC/ISDOCX se nepovedlo naimportovat (unsupported_document_type, invalid_xml, no_invoice_in_archive, file_too_large, no_finance_module, …)code/detail v těle chyby popisuje důvod
422 validation_failed / unknown_fieldtělo neodpovídá schématuzkontrolujte OpenAPI dokument — tělo zápisu je striktní
429 rate_limitedlimit tarifuzpomalte podle Retry-After

9. Checklist před nasazením do produkce

  • Ověřeno na pb_test_…, teprve pak přepnuto na pb_live_… se stejnou sadou scopes.
  • Synchronizace všech tří dokladových agend (invoices, received-invoices, cashflows) běží nezávisle, každá s vlastním updated_since checkpointem — kurzory se mezi agendami nekombinují.
  • Detail faktury/přijaté faktury se dotahuje zvlášť, když integrace potřebuje řádky (seznam je nemá).
  • account_id u mark_paid je buď vynechaný (výchozí účet), nebo dohledaný přes GET /finance-accounts — ne natvrdo uhádnutý.
  • Ošetřen 409 invalid_transition s kódem EXPORTED jako trvalý stav, ne jako chyba k opakování.
  • Import ze skenu/ISDOC (krok 3) rozlišuje přímou cestu (POST /received-invoices/isdoc) od obecné podatelny (POST /inbox/submissions) a ví, který outcome/kód chyby znamená co.
  • Každý zapisující POST nese Idempotency-Key — u multipart POST /inbox/submissions klidně taky, server otisk počítá ze syrových bajtů.
  • X-Request-Id se loguje pro dohledatelnost při řešení nesrovnalostí.

Updated 2026-09-15

Skladový systém (WMS) — příjemky, výdejky, stav skladu, převody

Tenhle recept popisuje, jak napojit externí skladový systém (čtečky čárových kódů, WMS terminál) na sklad ProfiBrew: zaúčtování příjmu zboží, výdej s výběrem konkrétní šarže, průběžnou synchronizaci stavu skladu a převod mezi sklady.

1. Cíl

Po dokončení receptu umí vaše integrace:

  1. zaúčtovat příjem zboží jako koncept + řádky → potvrzení (zásoba vzniká AŽ potvrzením, nikdy dřív),
  2. zaúčtovat výdej, volitelně s ručním výběrem konkrétní FIFO vrstvy (šarže),
  3. udržovat lokální kopii stavu skladu synchronizovanou přes updated_since,
  4. provést převod zboží mezi dvěma sklady tenanta.

2. Klíč, sandbox a scopes

Testovací klíč (pb_test_…), sada Sklad / WMS:

core:read, stock:read, stock:write, sales:read, events:read
export K=pb_test_…

Základní URL: https://www.profibrew.com/api/v1.

Než začnete, zjistěte id skladů a jednotek:

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/warehouses" | jq '.data[] | {id, code, name, categories}'

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/units" | jq '.data[] | {id, code, dimension}'

3. Krok 1 — příjemka (koncept → řádky → potvrzení)

Zásoba nikdy nevzniká bez potvrzené příjemky (CLAUDE.md, S104) — dokud příjemku nepotvrdíte, je to jen koncept a na sklad se nic nepromítne, ani kdyby měla řádky.

Založte koncept, případně rovnou s řádky:

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: 2c3d4e5f-6a7b-8c9d-0e1f-2a3b4c5d6e7f" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": "'"$WAREHOUSE_ID"'",
    "partner_id": "'"$SUPPLIER_ID"'",
    "movement_purpose": "purchase",
    "date": "2026-09-15",
    "external_ref": { "system": "wms", "id": "GR-8831" },
    "lines": [
      {
        "item_id": "'"$ITEM_ID"'",
        "quantity": "500",
        "unit": { "unit_code": "kg" },
        "unit_price": "28.50",
        "lot_number": "2026-K37",
        "expiry_date": "2027-03-01"
      }
    ]
  }' \
  "https://www.profibrew.com/api/v1/goods-receipts?on_conflict=return" | jq '{id, number, status, allowed_actions}'

Nebo přidejte řádky později, po vzniku konceptu:

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "item_id": "'"$ITEM_ID"'",
    "quantity": "500",
    "unit": { "unit_code": "kg" },
    "unit_price": "28.50",
    "lot_number": "2026-K37"
  }' \
  "https://www.profibrew.com/api/v1/goods-receipts/$RECEIPT_ID/lines"

unit na řádku musí být jednotka POLOŽKY (jinak 422 unit_mismatch) — API množství nikdy nepřevádí. lot_number na řádku příjemky založí NOVOU šarži s tímhle označením; nechte prázdné, pokud šarže tenanta nezajímají.

Potvrzení — teprve teď se zásoba objeví na skladu:

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  "https://www.profibrew.com/api/v1/goods-receipts/$RECEIPT_ID/actions/confirm" \
  | jq '{status, allowed_actions}'

Katalog akcí příjemky má jen confirm (z draft) a cancel (z confirmed) — koncept se v API nedá smazat ani zrušit (žádné DELETE na celý doklad, na rozdíl od řádků objednávky). Nepotřebný koncept nechte prostě ležet, nebo ho potvrďte a hned stornujte.

4. Krok 2 — výdej s výběrem šarže

Nejdřív zjistěte dostupné vrstvy (zůstatek per příjmový řádek):

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/stock-levels/lots?item_id=$ITEM_ID&warehouse_id=$WAREHOUSE_ID" \
  | jq '.data[] | {id, lot_number, quantity, unit, expiry_date}'

id v odpovědi JE receipt_line_id — pošlete ho zpátky jako lot_id na řádku výdejky, abyste vydali přesně tuhle vrstvu místo výchozího FIFO:

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: 9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": "'"$WAREHOUSE_ID"'",
    "movement_purpose": "sale",
    "lines": [
      {
        "item_id": "'"$ITEM_ID"'",
        "quantity": "120",
        "unit": { "unit_code": "kg" },
        "lot_id": "'"$LOT_ID"'"
      }
    ]
  }' \
  "https://www.profibrew.com/api/v1/stock-issues" | jq '{id, number, status}'

Vynechte lot_id, když je vám FIFO vrstva jedno — potvrzení pak vybere nejstarší dostupnou vrstvu automaticky, stejně jako v UI.

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  "https://www.profibrew.com/api/v1/stock-issues/$ISSUE_ID/actions/confirm"

Stejně jako u příjemky: confirm jen z draft, cancel jen z confirmed — a stejně nejde koncept smazat.

5. Krok 3 — synchronizace stavu skladu

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/stock-levels?warehouse_id=$WAREHOUSE_ID&updated_since=2026-09-14T00:00:00Z" \
  | jq '.data[] | {item: .item.code, quantity, available_quantity, unit}'

Řádek existuje pro každou kombinaci položka × sklad, která KDY měla zásobu — i nulovou. quantity je vždy v jednotce položky (nikdy nepřevedená), available_quantity = quantity − reserved_quantity (nikdy záporně).

6. Krok 4 — převod mezi sklady

Převod je výdejka s movement_purpose: "transfer" a vyplněným target_warehouse_id:

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: 4d5e6f70-8192-a3b4-c5d6-e7f809102a3b" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": "'"$WAREHOUSE_FROM"'",
    "target_warehouse_id": "'"$WAREHOUSE_TO"'",
    "movement_purpose": "transfer",
    "lines": [
      { "item_id": "'"$ITEM_ID"'", "quantity": "50", "unit": { "unit_code": "kg" } }
    ]
  }' \
  "https://www.profibrew.com/api/v1/stock-issues" | jq '{id}'

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  "https://www.profibrew.com/api/v1/stock-issues/$TRANSFER_ID/actions/confirm"

Potvrzením se zásoba odepíše ve zdrojovém skladu A ZÁROVEŇ vznikne automatická příjemka v konceptu na CÍLOVÉM skladu (najdete ji přes GET /goods-receipts?warehouse_id=$WAREHOUSE_TO&status=draft) — tu je potřeba potvrdit zvlášť, aby se zásoba objevila i v cíli:

curl -s -H "Authorization: Bearer $K" \
  "https://www.profibrew.com/api/v1/goods-receipts?warehouse_id=$WAREHOUSE_TO&status=draft" \
  | jq '.data[0].id'

curl -s -X POST -H "Authorization: Bearer $K" \
  -H "Idempotency-Key: $(uuidgen)" \
  "https://www.profibrew.com/api/v1/goods-receipts/$INCOMING_RECEIPT_ID/actions/confirm"

Pokud cílový sklad položku vůbec neumí přijmout (např. neshoduje se kategorie skladu), vrátí potvrzení výdejky 409 transfer_blocked — ověřte kategorie skladů předem přes GET /warehouses.

7. Sdílená externí reference napříč příjemkami a výdejkami

external_ref u příjemek a výdejek sdílí JEDEN sloupec na úrovni tenanta — tatáž hodnota {system, id} proto nesmí patřit současně příjemce i výdejce. Pošlete-li external_ref, který už používá doklad OPAČNÉ rodiny, dostanete 409 external_ref_conflict s hlavičkou Location ukazující na SKUTEČNOU rodinu nalezeného dokladu (/goods-receipts/… nebo /stock-issues/…) — čtěte Location, ne jen cestu, kterou jste volali.

8. Chyby, na které v tomhle receptu narazíte

KódKdyCo s tím
401 unauthenticatedchybí/špatný klíčzkontrolujte hlavičku Authorization
403 insufficient_scopechybí stock:writedoplňte scope, viz krok 2 výše
404 not_foundid patří opačné rodině (příjemka vs. výdejka), nebo jinému livemodeGET /goods-receipts/{id} na id výdejky vrátí 404, ne doklad
409 external_ref_conflictexternal_ref používá doklad opačné rodinyčtěte hlavičku Location odpovědi, viz bod 7
409 invalid_transitionconfirm/cancel mimo aktuální stavjen draft → confirm, confirmed → cancel
409 inventory_lockedsklad je zamčený běžící inventuroupočkejte na dokončení inventury
409 excise_reportedpohyb spadá do už podaného hlášení spotřební daněúprava mimo dosah API, řeší se v aplikaci
409 transfer_blockedcílový sklad převodu položku nepřijmeověřte kategorie skladů přes GET /warehouses
409 receipt_has_allocationsstorno příjemky, jejíž řádky už byly vydánynejdřív stornujte navazující výdej
422 validation_failed (invalid_reference)item_id/warehouse_id/partner_id/lot_id neexistuje pro tenantaověřte id přes odpovídající GET
422 unit_mismatchjednotka řádku ≠ jednotka položkypošlete quantity v jednotce z GET /items/{id}
422 quantity_scalevíc desetinných míst, než dovoluje jednotkazaokrouhlete na decimals
429 rate_limitedlimit tarifuzpomalte podle Retry-After

9. Checklist před nasazením do produkce

  • Ověřeno na pb_test_…, teprve pak přepnuto na pb_live_….
  • Terminál/čtečka nikdy nezapisuje zásobu jinak než potvrzenou příjemkou/výdejkou — žádné „rychlé opravy" stavu skladu jinudy.
  • Každý POST nese Idempotency-Key — u výdejek se skenováním čárových kódů obzvlášť důležité (nestabilní síť ve skladu, opakované pokusy).
  • external_ref u příjemek a výdejek je z jednoho jmenného prostoru (vaše ID dokladu WMS) — nepoužívejte stejné číslo pro příjem i výdej.
  • Ruční výběr šarže (lot_id) čte id z GET /stock-levels/lots TĚSNĚ před založením řádku — mezitím mohla vrstva dojít.
  • Převod ověřuje i potvrzení automaticky vzniklé příjemky na cílovém skladu — bez něj zásoba v cíli chybí, i když výdejka na zdroji prošla.
  • Sync stavu skladu běží přes updated_since, ne plným výpisem všech položek při každém běhu.
  • X-Request-Id se loguje pro dohledatelnost.

Updated 2026-09-15

Reference

The full endpoint reference, generated from the same schemas the API validates against — it cannot drift from the implementation.

Open the OpenAPI document

Loading reference…

Errors

The type field of every problem document links to the matching row here. Branch on code — the wording of title and detail may change.

CodeStatusMeaning
invalid_idempotency_key400Idempotency-Key is malformed.
unauthenticated401Missing, malformed, expired or revoked API key.
account_locked403The account is locked.
addon_required403The account has no API add-on.
excise_not_enabled403The account keeps no excise tax records.
forbidden403The key's owner lacks the rights for this operation.
insufficient_scope403The key does not carry the scope this endpoint requires.
ip_not_allowed403The request came from an address outside the key's allow-list.
module_not_enabled403The module is not in the account's subscription.
not_found404No such record — also for records of another account.
unknown_action404Unknown workflow action for this document.
conflict409The request collides with the current state.
duplicate_ico409A partner with this IČO already exists.
equipment_occupied409The vessel the batch moves into is occupied by another batch.
excise_reported409The movement is already in a filed excise report.
external_ref_conflict409Another record already carries this external_ref.
idempotency_in_progress409A request with this Idempotency-Key is still running.
inventory_locked409A running inventory count locks the warehouse.
invalid_transition409The action is not allowed in the document's current status.
invoiced_period409The brew date falls into a period that was already invoiced.
item_not_in_stock409The cash desk sells only items in stock and this one is out.
no_keg_tapped409No keg of this draft beer is tapped at the cash desk.
receipt_has_allocations409Lines of the receipt were already issued.
repack_pair_cancel_required409Cancel both documents of a repack pair together.
reserved_by_other409The stock is reserved for another order.
sale_kegs_partially_returned409Some kegs of the sale were already returned.
settlement_failed409The sale was recorded but its settlement did not finish; `existing_id` carries it — do not resend.
step_handed_over409The wait timed out and the step was handed over to a person as a task.
step_not_external409The workflow step does not wait for an external system.
step_not_waiting409The workflow step is no longer waiting — it was completed, stopped or cancelled.
submission_blocked409The submission is blocked.
transfer_blocked409The transfer cannot be confirmed; `blockers` lists why.
events_expired410Events before this sequence fell out of the plan retention — do a full sync.
stale_version412If-Match does not match the current version of the record.
payload_too_large413Body or file is larger than the plan allows.
account_required422No default finance account — send account_id.
cash_desk_required422The shop has more than one cash desk (or none) — send cash_desk_id.
currency_not_supported422The currency is not supported here.
endpoint_limit422The plan's webhook endpoint limit is reached.
idempotency_key_reused422The same Idempotency-Key arrived with a different body.
invalid_amount422The amount is invalid.
invalid_url422The webhook URL is not a public https address.
item_not_available_at_pos422The item is not sold at this cash desk.
measurement_type_ambiguous422The measurement carries several values — send one, or set `type`.
measurement_value_required422The measurement has no value for its type.
missing_exchange_rate422A foreign-currency document needs an exchange rate.
no_file422No file in the request.
no_lines422The document has no lines.
overpay422The payment exceeds the remaining amount.
packaging_return_exceeds_balance422The return exceeds the partner's packaging balance.
partner_limit422The plan's partner limit is reached.
payment_amount_mismatch422The payment does not match the total of the sale; `expected_amount` carries it.
payment_method_not_supported422The payment method cannot be recorded for a POS sale.
quantity_scale422The quantity has more decimal places than the unit allows.
recipe_unit_dimension_mismatch422The recipe unit must share the dimension of the stock unit.
shop_required422The account has several shops — send warehouse_id.
storage_limit_exceeded422The plan's storage limit is exceeded.
system_warehouse422A system warehouse cannot be used on a manual document.
unit_mismatch422The unit does not match the unit of the item or line.
unit_not_available422The unit is not available to this account.
unknown_field422The body carries fields the API does not know.
unsupported_format422The file format is not supported.
validation_failed422The body failed validation; `errors[]` carries the rows.
warehouse_required422warehouse_id is required for this account.
rate_limited429Rate limit exceeded — back off per Retry-After.
internal_error500Unexpected error on our side; the request id is in `instance`.
upload_failed502Storing the file failed — retry later.
api_unavailable503The API is temporarily unavailable.
webhooks_not_configured503This deployment has no webhook signing key.

Codes inside errors[]

These describe one offending field of a validation_failed response; they never appear as the code of the response itself.

  • entity_pair_requiredThe pair of fields must be filled together.
  • exactly_one_ofExactly one of the listed fields is allowed.
  • invalid_referenceThe referenced record does not exist or is not usable.
  • not_allowedThe value is not allowed for this field.
  • requiredThe field is required.
  • unexpected_fieldThe field does not belong to this body.
  • unit_requiredThe quantity is missing its unit.
  • unknown_event_typeAn unknown event type or pattern in the subscription.
  • unknown_fieldAn unknown key among otherwise valid fields.

Glossary

The API speaks English, the app speaks Czech to the brewery. When you discuss a record with your customer, these are the two names for the same thing.

APIIn the appNote
partnerpartner (odběratel i dodavatel)One record for both roles; `is_customer` and `is_supplier` say which.
itempoložka (artikl)Materials and products share one catalogue.
unitměrná jednotkaQuantities always travel with a unit; document lines use the item unit without conversion.
orderobjednávka (přijatá)A customer order, not a purchase order.
goods receiptpříjemkaStock document with `movement_type: receipt`; its line is also the valuation layer (FIFO).
stock issuevýdejkaStock document with `movement_type: issue`; picks a layer (manual lot) or FIFO.
lotšarže (příjmová vrstva)`lot_id` on an issue line is the receipt line the quantity leaves from.
stock levelstav zásobyQuantity, reserved and available per item and warehouse, in the item unit.
invoicevydaná fakturaIssued to a customer; `issue` turns a draft into an issued document with a number.
received invoicedošlá fakturaSupplier invoice; lines carry no units.
cash flow documentzjednodušený (interní) dokladMoney movement; status is lifecycle only, payment state is `payment_status`.
warehouseskladStock location; system warehouses are not usable on manual documents.
shopprovozovnaBusiness location; derived automatically when the account has exactly one active.
excisespotřební daňReported in hectolitres and degrees Plato by law.
batchvárkaBrewery production run (public API coverage comes in F3).
inbox submissionpodání v podatelněA file entering the gateway from e-mail, upload or the API.
scan inboxschránka dokladůAI document extraction queue; needs the document scan add-on.
external referenceexterní reference`external_ref` is the integrator's own id as `<system>:<id>`, unique per account.
idempotency keyklíč idempotenceHeader on POST; the same key with the same body replays the stored response for 24 hours.
sandboxsandbox (demo účet)A `pb_test_` key works against the shadow demo account; responses carry `livemode: false`.
scopeoprávnění klíče`<module>:read` or `<module>:write`; effective scopes are the key's scopes intersected with the subscription.
domain eventdoménová událostSomething that happened in the account; ordered per account by `sequence`.
webhook endpointwebhook endpointYour https URL plus a signing secret; events are delivered there.
deliverydoručeníOne attempt trail per event and endpoint, with retries and a final dead state.

Changelog

Additive changes ship without a new version. A new dated version means a breaking change, and it is announced ahead of time.

2026-09-17

current

Missing domain events: goods receipts get their own event family, payment reversals cover every paid document, and reverting a stock document to draft is announced. Workflow steps can now call a webhook or wait for your system to approve, brewery data — batches, measurements, recipes, equipment and excise returns — is available, and sales from your register or e-shop can be recorded.

  • Breaking for subscribers of `stock_issue.*` who relied on it for goods receipts: a goods receipt now emits `goods_receipt.created` and `goods_receipt.cancelled` instead of `stock_issue.created` and `stock_issue.cancelled`. `goods_receipt.confirmed` is unchanged, so the whole goods receipt lifecycle is now under `goods_receipt.*`.
  • New `received_invoice.payment_reverted` and `cashflow.payment_reverted`. Until now only an issued invoice (`invoice.payment_reverted`) announced that its payment was cancelled; a received invoice or a cash flow document kept looking paid. A `*.payment_reverted` event is emitted when a payment is cancelled on the document — in the app, or through the `revert_payment` action on invoices and received invoices — and when the bank movement that paid it is deleted (for a transfer, both legs). Other changes that can leave a paid document only partially paid, such as raising the amount of a paid cash flow document, do not emit it yet; use `updated_since` on the resource as the safety net.
  • New `goods_receipt.reverted_to_draft` and `stock_issue.reverted_to_draft` when a confirmed document goes back to draft. Reverting a goods receipt also emits `stock_level.changed` for the stock it takes back; reverting a stock issue does not, because only an issue with no stock movements can be reverted.
  • `<document>.paid` is now also emitted when an existing free-standing payment is matched to a document and pays it in full. Before, only paying the document directly or in bulk emitted it.
  • Version field: events carry the contract version they were emitted under, so events emitted before this release keep `api_version: "2026-09-14"` and new ones carry `2026-09-17`. A webhook endpoint created earlier still shows `api_version: "2026-09-14"` in `GET /webhooks/endpoints` — that is the version it was created with; it does not change what it receives.
  • New `workflow.step_called`, `workflow.step_waiting_external` and `workflow.step_completed_external`, emitted by the workflow steps **Call a webhook** and **Wait for an external system**. The payload (`object: "workflow_step"`) carries the step, the workflow and the document it runs on, in the same shape as the document's GET. When the account chose a specific endpoint for the step, the event goes only to that endpoint, whether or not it subscribes to `workflow.*`. `workflow.step_waiting_external` is emitted by the system (`caused_by.kind: "system"`), so an endpoint with `exclude_own_events` still receives it when your own key moved the workflow to the waiting step.
  • New `POST /workflow-steps/{step_instance_id}/actions/complete` with `result: approved | rejected` and an optional `note` (scope `workflow:write`). `approved` continues the workflow; `rejected` stops it and shows `note` to the person as the reason. `data.object.actions.complete` in the waiting event holds the path while the step still waits. Errors: 404 `not_found`, 409 `step_not_external`, `step_not_waiting`, and `step_handed_over` once the wait timed out and the step became a task for a person, and 403 `account_locked` when the account is scheduled for deletion. The steps that follow an approval run right away under the same key: an action step (confirming an order, issuing an invoice…) needs the key's scope for its module, otherwise the workflow stops. The response carries `instance_status` so you can tell. Any key of the account with `workflow:write` can complete a waiting step; the key that did is stored on the step and returned as `completed_by_api_key_id` in the response and in the `workflow.*` event payload (`null` when the step was not completed through the API).
  • New brewery resources (scopes `brewery:read` / `brewery:write`): `GET /batches`, `GET /batches/{id}`, `GET /batches/{id}/measurements`, `POST /batches/{id}/measurements`, `POST /batches/{id}/actions/advance_phase`, `GET /recipes`, `GET /recipes/{id}` and `GET /equipment`. Physical values are canonical with the unit in the field name (`volume_l`, `og_plato`, `temperature_c`, `gravity_sg`, `pressure_bar`).
  • Record a measurement in the unit you measured in — `{ "temperature": { "value": "64.4", "unit": "f" }, "gravity": { "value": "1.048", "unit": "sg" } }` — and it is stored canonically, with the original kept in `entered`. `advance_phase` moves a batch only to its next phase (409 `invalid_transition` otherwise); writing off or blending a batch stays in the app.
  • New events `batch.phase_changed` and `batch.measurement_added`, emitted for changes made in the app (including cancelling or deleting a batch, reverting a phase and transferring a batch into its parent batch), through the API, and — for measurements — by sensor devices linked to the batch's vessel.
  • Recipe ingredients carry `quantity` with the unit of the recipe line; the API does not convert them to grams.
  • New `GET /excise/monthly-reports` (scope `brewery:read`): monthly excise tax returns, always in hectolitres and whole degrees Plato. 403 `excise_not_enabled` when the account keeps no excise records.
  • New `POST /pos/sales` and `GET /pos/sales/{id}`: record a completed sale and settle it like a POS tab — prices and VAT from the POS price list, stock written off from the cash desk's warehouse, a simplified sales document and the payment. Cash or card in CZK, one payment per sale; `payment.amount` must match the total (a difference of up to 0.50 CZK is rounding, otherwise 422 `payment_amount_mismatch` with `expected_amount`), and every line `quantity` must be greater than 0. The key needs `pos:write` and `finance:write`, plus `stock:write` when the cash desk moves stock — listed in the operation's `x-additional-scopes` (scopes needed on top of one of `x-scopes`). `external_ref` protects against recording the same sale twice. If the sale is recorded but its settlement does not finish, the response is 409 `settlement_failed` with `existing_id` and a `Location` header: the sale stays open and keeps its `external_ref`, so resending it cannot record it twice. Part of the settlement may already be written, so check the sale's documents in the app before settling it again there, or cancel it. `?on_conflict=return` then answers 200 with the sale in `status: "open"` — check `status` before treating it as done.
  • `POST /cashflows` no longer accepts `status: cancelled` — a cash flow document can only be created as `draft` or `confirmed`; cancel it with `actions/cancel`. Sending `cancelled` returns 422 `validation_failed`.

2026-09-14

First public version of the API (beta).

  • Keys, scopes and the sandbox: `pb_live_` / `pb_test_` keys, per-module scopes, test keys running against the shadow demo account.
  • Catalog and documents: partners, contacts, items, units, orders, goods receipts, stock issues, stock levels, invoices, received invoices, cash flow documents and attachments.
  • Workflow through actions: documents move with the same actions as in the app, so the API inherits rights, numbering, notifications and FIFO valuation.
  • Quantities always travel with their unit; the API never converts a quantity.
  • `external_ref` for mapping your own identifiers, `Idempotency-Key` on writes and `If-Match` on updates.
  • Domain events with a per-account `sequence`, `GET /events` and webhooks signed per Standard Webhooks.

Terms

Using the API means accepting the API terms of use; the account administrator confirms them once, when the first key is created.

Fair use in short: keep your key secret and rotate it if it leaks, respect the rate limits and back off when asked, do not use the API to mirror the whole database of an account you do not operate, and remember the customer's data stays the customer's — you process it on their behalf. The full legal terms, privacy policy and data processing agreement live with the rest of our legal documents.

API terms version v0.1

Legal documents