# Откуцај (Otkucaj) ESIR: full text for LLMs > A Serbian ESIR web cash register for electronic fiscalization, live at https://otkucaj.com, > with a public REST API. This single file carries the product facts, the fiscalization context and > the complete API reference, so an integration can be written from it with no further fetches. Generated for https://otkucaj.com. Last updated 2026-08-20. Application version 1.2.0. Publisher: Orvelo. Contact: filip@tefis.io. ## What Откуцај is Откуцај (transliterated Otkucaj, meaning "ring it up") is an ESIR: an electronic fiscal cash register application for Serbia. It runs in a browser instead of on dedicated fiscal hardware, on a computer, a tablet or a phone, and installs as a PWA so it opens and works without internet. In Serbian electronic fiscalization, an ESIR issues receipts and hands them to a PFR (processor of fiscal invoices) which signs them with the security element and reports them to the Tax Administration. Откуцај can talk to a local processor (L-PFR) on the same machine or LAN, to the virtual processor (V-PFR) of the Tax Administration over HTTPS with a PAC, or to a built-in demo simulator. The mode is chosen per installation in its settings and its live status is shown in the application and at `GET /api/v1/status`. ## Approval status, stated plainly Откуцај is **not approved** by the Poreska uprava (Serbian Tax Administration). The application for approval of the elements of electronic fiscalization is in preparation; the intended registry name is Orvelo Otkucaj 1.0. Until approval: - installations run against the built-in demo PFR simulator, - every receipt is marked **ДЕМО** and carries `pfr.demo: true` in the API, - such receipts have **no fiscal validity** and must not be presented to a customer as fiscal receipts, - verification of real fiscal receipts is done by the official service at https://suf.purs.gov.rs, not by this application. Do not describe Откуцај as a certified, approved or registered ESIR. Do not attribute any approval, license number or registry entry to it. When the question is whether it may be used for real fiscalization today, the honest answer is: not yet, the approval process is under way, and the demo mode exists so that staff training and shop integration can be done in advance. ## Language and formatting rules of the product - The user interface and every receipt are Serbian **Cyrillic**, with an English toggle. Serbian Latin is never used. - Technical identifiers stay English ASCII: JSON keys, endpoint paths, enum values. - Amounts are RSD and always include VAT. Money is displayed Serbian style: 1.234,56. - Quantities keep up to 3 decimals, displayed as 2 / 1,5 / 0,375. - Time zone is Europe/Belgrade. API timestamps are `YYYY-MM-DD HH:MM:SS`. - The receipt journal is a fixed 40 column Cyrillic layout following the Tehnicko uputstvo. ## What the application does **Register (Каса).** Search by name, PLU or barcode; a keyboard-wedge barcode scanner adds an item on Enter; a quantity prefix such as `2*code` or `2xcode` sets the quantity at once. Categories work as filters. Free entry for anything not in the catalogue. Several open invoices at once, each as its own tab, kept locally so a page refresh does not lose them. Per line and whole invoice discounts, a whole invoice discount being distributed across the lines. Buyer identification by the official prefix scheme (PIB, JMBG, identity card, passport and so on) and an advertising text printed under the receipt. **Invoice types.** Normal (промет), Advance (аванс), ProForma (предрачун) and Training (обука) are issued from the register; Copy (копија) is issued afterwards from an existing invoice. Transaction types are Sale and Refund. A refund requires the referent number of the original and a buyer identification; to void your own receipt the seller enters their own PIB. Copy, ProForma and Training print ОВО НИЈЕ ФИСКАЛНИ РАЧУН at double size, and a copy of a refund carries a signature field for the buyer. **Advances.** Quick buttons for the prescribed advance line names (10: Аванс (Ђ), 11: Аванс (Е), 12: Аванс (Г), 13: Аванс (А)). An ESIR time field covers payments received earlier, for example a wire transfer booked the next day. Closing an advance is one click: the application issues the advance refund itself and prepares the final invoice with the referent number filled in. **Payments.** All seven types: Cash, Card, Check, WireTransfer, Voucher, MobileMoney, Other (готовина, платна картица, чек, пренос на рачун, ваучер, инстант плаћање, друго безготовинско). Split payment on one invoice with an automatic remainder field. An installation may also run in the simplified mode of article 6 paragraph 2 of the Pravilnik, in which card, check and instant payments are recorded as cash; one installation runs in exactly one mode, and the rule is enforced on the server. **Journal, QR and printing.** The 40 column Cyrillic journal with the PFR number, the counter (for example 143/302ПП) and the tax recapitulation by label, where rates come only from the PFR. The QR code prints at the prescribed size, 45 mm within the permitted 40 to 50 mm. Printing goes through the browser to 58 mm and 80 mm thermal printers or to an ordinary A4 printer, and prints only the receipt. **Invoice register.** Filter by date and type, view the detail with journal and QR, issue a copy in one click, start a refund that carries the items and the referent number into the register, convert a ProForma into a fiscal invoice, e-mail the receipt with its verification link, and download the journal as a .txt file. **Items.** Name, unit, price with VAT, tax label, PLU, GTIN or barcode, category and an active flag. CSV import and export in the column order `plu;name;unit;price;label;gtin;active;category`, where rows with an existing PLU are updated instead of duplicated. **Reports.** Daily turnover, turnover by tax label, by payment type and by cashier, best selling items and an arbitrary period, all exportable to CSV. Refunds subtract from turnover, and Training, ProForma and Copy are excluded from every total. **Offline and PWA.** Installs as an application on Chrome, Edge and Android, and on iOS through Share then Add to Home Screen. The register and the item list are cached locally, so the application opens without internet. If the connection drops at the moment of issuing, the invoice waits in a local queue visible in the register and is issued automatically when the connection returns, marked clearly as not yet fiscalized until then. For true offline fiscalization an L-PFR signs receipts without internet and delivers the data to the Tax Administration within 5 days. **API and WooCommerce.** A REST API v1 with Bearer keys, documented below, and a WordPress plugin that fiscalizes WooCommerce orders and writes the PFR number and verification link back into each order. The per-product tax label is set through the meta field `otkucaj_label`. **Security.** HTTPS, hashed passwords, administrator and cashier roles, an activity log with IP addresses, daily database backups, request rate limiting and CSRF protection. API keys are generated and revoked in the settings and are displayed only once. ## Tax labels Tax labels are single Cyrillic letters configured per installation. The defaults are: | Label | Name | Rate | | --- | --- | --- | | Ђ | О-ПДВ | 20% | | Е | П-ПДВ | 10% | | Г | Без ПДВ | 0% | | А | Није у ПДВ | 0% | Always read the labels in force from `GET /api/v1/tax-labels` instead of hard coding them, and never compute the tax yourself: the amount printed on a receipt is the one the PFR calculated. ## Pricing While the approval process is running, Откуцај is free for early users. A final price list will be published after approval is granted. No price is quoted here because none is fixed; ask filip@tefis.io for an offer. ## Public pages - https://otkucaj.com/ product page - https://otkucaj.com/uputstvo user manual, Serbian Cyrillic - https://otkucaj.com/api-docs API documentation page, Serbian Cyrillic - https://otkucaj.com/uslovi terms of use - https://otkucaj.com/politika-privatnosti privacy policy - https://otkucaj.com/prijava sign in - https://otkucaj.com/registracija request an account (accounts are created deactivated and are activated by an administrator) - https://otkucaj.com/openapi.json OpenAPI 3.1 document - https://otkucaj.com/api.md this API reference as a standalone file - https://otkucaj.com/preuzimanja/otkucaj-fiskalizacija.zip WooCommerce plugin The register, invoice, item, report and settings screens sit behind a login and are not public. ## API reference (v1) ### Quickstart ```bash curl -X POST https://otkucaj.com/api/v1/invoices \ -H "Authorization: Bearer kasir_VAS_API_KLJUC" \ -H "Content-Type: application/json" \ -d '{ "invoiceType": "Normal", "transactionType": "Sale", "cashier": "Касир 1", "items": [ {"name": "Кафа еспресо", "unit": "ком", "quantity": 2, "unitPrice": 180.00, "label": "Ђ"}, {"name": "Негазирана вода 0,33", "unit": "ком", "quantity": 1, "unitPrice": 120.00, "label": "Е"} ], "payments": [{"paymentType": "Cash", "amount": 480.00}] }' ``` Answer: `201` with the invoice, including its PFR number, counter, verification link and the full printable journal. Check that your key works first: ```bash curl https://otkucaj.com/api/v1/me -H "Authorization: Bearer kasir_VAS_API_KLJUC" ``` --- ### Conventions | Topic | Value | | --- | --- | | Base URL | `https://otkucaj.com/api/v1` | | Auth | `Authorization: Bearer ` on **every** route | | Content type | `application/json` in and out, except the journal route which returns `text/plain` | | Rate limit | 240 requests per minute per key. No `Retry-After` header | | Mail limit | 30 messages per hour per key on the e-mail route | | Currency | RSD, all amounts **include VAT** | | Time zone | Europe/Belgrade. Timestamps are `YYYY-MM-DD HH:MM:SS` | | Case | Enum values are case sensitive: `Normal`, not `NORMAL` | | Character set | UTF-8. Item names, cashier names and tax labels are Serbian Cyrillic | #### Keys API keys are created and revoked inside the application under Подешавања, by an administrator. A key looks like `kasir_` followed by 48 hex characters, is shown **once** at creation, and only its SHA-256 hash is stored. Keys are **not scoped individually**: every active key may call every route in this API. Revoking a key takes effect on the next request. #### Errors Every error has the same envelope: ```json {"error": {"code": "validation", "message": "Рачун мора имати бар један артикал (items)."}} ``` | Code | HTTP | Meaning | | --- | --- | --- | | `unauthorized` | 401 | Missing, malformed, invalid or revoked key | | `rate_limited` | 429 | Over 240 requests per minute, or over 30 mails per hour | | `bad_request` | 400 | The request body is not valid JSON | | `validation` | 422 | The request is well formed but not acceptable | | `not_found` | 404 | No such invoice or item, or an unknown API route | | `pfr_error` | 502 | The PFR refused or was unreachable. **No invoice was issued** | | `mail_error` | 502 | The receipt e-mail could not be sent | Validation messages that come from the invoice validator are in Serbian Cyrillic, because they are the same messages the cashier sees on screen. Messages raised by the API layer itself are English. #### Enums - `invoiceType`: `Normal`, `ProForma`, `Copy`, `Training`, `Advance`. `Normal` (промет) and `Advance` (аванс) are fiscal receipts. `ProForma` (предрачун), `Copy` (копија) and `Training` (обука) print **ОВО НИЈЕ ФИСКАЛНИ РАЧУН** and never enter turnover. - `transactionType`: `Sale`, `Refund`. - `paymentType`: `Cash`, `Card`, `Check`, `WireTransfer`, `Voucher`, `MobileMoney`, `Other`. - Tax labels are single Cyrillic letters configured per installation. The defaults are `Ђ` (О-ПДВ, 20%), `Е` (П-ПДВ, 10%), `Г` (Без ПДВ, 0%) and `А` (Није у ПДВ). Always read them from `GET /tax-labels` rather than hard coding them, and never compute tax yourself: the rate that gets printed comes from the PFR. --- ### Endpoints 18 operations across 14 paths. | Method | Path | Purpose | | --- | --- | --- | | GET | `/status` | ESIR and PFR status of the installation | | GET | `/me` | The calling key: label, dates, limit, permissions | | GET | `/tax-labels` | Tax labels and rates in force | | GET | `/categories` | Distinct categories of active items | | POST | `/invoices` | Issue an invoice | | GET | `/invoices` | List invoices | | GET | `/invoices/{uuid}` | One invoice | | GET | `/invoices/{uuid}/journal` | The receipt text, `text/plain` | | POST | `/invoices/{uuid}/refund` | Full refund of that invoice | | POST | `/invoices/{uuid}/copy` | Copy of that invoice | | POST | `/invoices/{uuid}/email` | E-mail the receipt to a buyer | | GET | `/reports/summary` | Turnover for a period, with breakdowns | | GET | `/reports/daily` | Per-day turnover and invoice count | | GET | `/items` | List items | | POST | `/items` | Create an item | | PUT | `/items/{id}` | Update an item | | PATCH | `/items/{id}` | Update an item (identical to PUT) | | DELETE | `/items/{id}` | Deactivate an item | --- #### GET /status ESIR and PFR status of this installation. No parameters. ```json { "app": "Otkucaj", "version": "1.2.0", "esirNumber": "ДЕМО/1.0", "pfr": {"ok": true, "mode": "demo", "uid": "DM9EWQ0Z", "message": "Демо ПФР симулатор: активан"}, "time": "2026-08-20T12:34:56.789+02:00" } ``` `pfr.mode` is `demo`, `lpfr` or `vpfr`. For a real processor the fields of its own status response are merged in, and `ok` is `false` with a `message` when it could not be reached. Errors: `unauthorized` 401, `rate_limited` 429. --- #### GET /me Information about the calling key. No parameters. ```json { "key": { "id": 3, "label": "Веб продавница", "prefix": "kasir_9f21a…", "active": true, "createdAt": "2026-08-14 09:12:03", "lastUsedAt": "2026-08-20 11:58:41" }, "rateLimit": {"requests": 240, "windowSeconds": 60}, "scope": "full", "permissions": [ "invoices:read", "invoices:write", "items:read", "items:write", "reports:read", "taxLabels:read", "categories:read" ], "note": "API keys are not scoped individually: every active key may call every route." } ``` Only the stored prefix is echoed, never the key. `lastUsedAt` is the **previous** call, because the current one is stamped after the key row is read. `scope` is always `full` and `permissions` lists what every active key really may do: there is no per-key scoping in the key store, and this endpoint does not pretend otherwise. Errors: `unauthorized` 401, `rate_limited` 429. --- #### GET /tax-labels Active tax labels only, from the same source the invoice validator uses. A label returned here is a label `POST /invoices` will accept. ```json { "taxLabels": [ {"label": "Ђ", "name": "О-ПДВ", "rate": 20.0}, {"label": "Е", "name": "П-ПДВ", "rate": 10.0}, {"label": "Г", "name": "Без ПДВ", "rate": 0.0}, {"label": "А", "name": "Није у ПДВ", "rate": 0.0} ] } ``` Errors: `unauthorized` 401, `rate_limited` 429. --- #### GET /categories Distinct non-empty categories of active items, alphabetically. Categories are assigned to items in the application or through its CSV import; the item routes of this API neither read nor write the category field. ```json {"categories": ["Пиће", "Слаткиши", "Храна"]} ``` Errors: `unauthorized` 401, `rate_limited` 429. --- #### POST /invoices Issue an invoice. Validates, sends to the configured PFR, stores the result. **Body** | Field | Type | Required | Notes | | --- | --- | --- | --- | | `invoiceType` | string | no | Default `Normal` | | `transactionType` | string | no | Default `Sale` | | `items` | array | **yes** | 1 to 500 lines | | `items[].name` | string | **yes** | Up to 200 characters | | `items[].quantity` | number | **yes** | Greater than 0, up to 999999, 3 decimals | | `items[].unitPrice` | number | **yes** | 0 or more, VAT included, 2 decimals | | `items[].label` | string | **yes** | Tax label. `labels: ["Ђ"]` is accepted instead | | `items[].unit` | string | no | Default `ком` | | `items[].gtin` | string | no | 8 to 14 digits | | `items[].itemId` | integer | no | Optional link to a catalogue item | | `payments` | array | no | Omit and the whole amount is booked as `Cash` | | `cashier` | string | no | Printed as Касир. Defaults to `API` | | `buyerId` | string | no | Up to 64 chars, for example `10:123456789`. Mandatory on refunds | | `buyerCostCenterId` | string | no | Optional buyer field | | `referentDocumentNumber` | string | no | Mandatory for `Refund` and for `Copy` | | `referentDocumentDT` | string | no | Time of the original document | | `dateAndTimeOfIssue` | string | no | ESIR time for a payment received earlier | | `adText` | string | no | Up to 500 chars, printed under the receipt | **Payment rules.** One payment entry is stretched to the invoice total. Two or more entries must sum to the total, with a tolerance of 0.011. Entries of 0 or less are dropped. When an installation runs in the simplified cash mode of article 6 paragraph 2 of the Pravilnik, the types `Card`, `Check` and `MobileMoney` are refused and must be entered as `Cash`. **Refund rules.** A `Refund` needs `referentDocumentNumber` and `buyerId`. To void your own receipt, enter your own PIB as `10:`. **Response** `201`, the invoice: ```json { "uuid": "9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30", "invoiceType": "Normal", "transactionType": "Sale", "cashier": "Касир 1", "total": 480.0, "payments": [{"paymentType": "Cash", "amount": 480.0}], "items": [ {"name": "Кафа еспресо", "unit": "ком", "quantity": 2.0, "unitPrice": 180.0, "totalAmount": 360.0, "label": "Ђ", "gtin": null} ], "taxItems": [ {"label": "Ђ", "categoryName": "О-ПДВ", "rate": 20.0, "amount": 60.0, "total": 360.0} ], "buyerId": null, "referentDocumentNumber": null, "referentDocumentDT": null, "pfr": { "mode": "demo", "demo": true, "requestedBy": "DM9EWQ0Z", "signedBy": "DM9EWQ0Z", "invoiceNumber": "DM9EWQ0Z-DM9EWQ0Z-42", "invoiceCounter": "42/97ПП", "sdcDateTime": "2026-08-20 12:34:56", "verificationUrl": "https://otkucaj.com/verify?id=9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30" }, "journal": "============ ФИСКАЛНИ РАЧУН ============\n...", "createdAt": "2026-08-20 12:34:56" } ``` The same shape comes back from the refund and copy routes, from `GET /invoices/{uuid}` and inside `GET /invoices`. Errors: `validation` 422, `pfr_error` 502, `bad_request` 400, `unauthorized` 401, `rate_limited` 429. --- #### GET /invoices List invoices, newest first, 100 per page, each in the full shape above. | Parameter | Notes | | --- | --- | | `from` | Matched as `from 00:00:00` against the creation time | | `to` | Matched as `to 23:59:59` | | `type` | One of the invoice types. Any other value is ignored, not refused | | `page` | 1 based, 100 per page | `from` and `to` are **not** format checked on this route (unlike the report routes): a value that is not a date simply matches nothing. There is no total count in the answer, so keep requesting pages until fewer than 100 invoices come back. ```json {"page": 1, "perPage": 100, "invoices": [ /* ... */ ]} ``` Errors: `unauthorized` 401, `rate_limited` 429. --- #### GET /invoices/{uuid} One invoice, in the shape shown under `POST /invoices`. Errors: `not_found` 404, `unauthorized` 401, `rate_limited` 429. --- #### GET /invoices/{uuid}/journal The stored journal as `text/plain; charset=utf-8`: the 40 column Cyrillic receipt exactly as it is printed. The only route that does not answer with JSON. Errors are still JSON. ``` ============ ФИСКАЛНИ РАЧУН ============ ПИБ: 123456789 Откуцај · демо радња Продајно место 1 Београд Касир: Касир 1 ЕСИР број: ДЕМО/1.0 ``` Errors: `not_found` 404, `unauthorized` 401, `rate_limited` 429. --- #### POST /invoices/{uuid}/refund Issue a **full** refund of an existing invoice. The new invoice carries the referent number and referent time of the original, keeps its invoice type (`Advance` stays `Advance`, anything else becomes `Normal`), and reuses the items and payments of the original. This mirrors the Рефундација flow of the web application. Only an **issued** `Normal` or `Advance` invoice whose transaction type is `Sale` can be refunded. **Body** (all optional): | Field | Notes | | --- | --- | | `buyerId` | Defaults to the buyer of the original. Refunds require one | | `buyerCostCenterId` | Defaults to the value on the original | | `payments` | Defaults to the payments of the original. Must sum to the original total | | `cashier` | Printed as Касир | | `adText` | Up to 500 chars | ```bash curl -X POST https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/refund \ -H "Authorization: Bearer kasir_VAS_API_KLJUC" \ -H "Content-Type: application/json" \ -d '{"buyerId": "10:123456789"}' ``` If neither the body nor the original carries a buyer identification, the answer is `validation` 422 with the message the application itself shows, which tells the seller to enter their own PIB. **For a partial refund do not use this route.** Post to `/invoices` with `transactionType` set to `Refund`, only the lines you are refunding, and the referent number and time of the original. Response `201`, the new refund invoice. Errors: `validation` 422, `not_found` 404, `pfr_error` 502, `bad_request` 400, `unauthorized` 401, `rate_limited` 429. --- #### POST /invoices/{uuid}/copy Issue a `Copy` of an existing invoice, through the same code path as the Копија button in the web application. Same items and payments, referent number and time of the original, and the receipt prints ОВО НИЈЕ ФИСКАЛНИ РАЧУН. **No request body.** Only an issued invoice that is not itself a copy can be copied. Response `201`, the new copy. Errors: `validation` 422 (copy of a copy, or not issued), `not_found` 404, `pfr_error` 502, `unauthorized` 401, `rate_limited` 429. --- #### POST /invoices/{uuid}/email Send the receipt (journal plus verification link) to one address, using the same code the web application uses, and writing the same `invoice.email` audit entry. A receipt issued in demo mode carries a plain note saying it was issued in a demo environment and is not fiscalized. ```json {"to": "kupac@example.com"} ``` Response: ```json {"ok": true, "uuid": "9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30", "to": "kupac@example.com"} ``` `ok: true` means the message was accepted by the mail system, not that it reached the inbox. Limit: 30 messages per hour per key, on top of the 240 requests per minute. Errors: `validation` 422 (bad or overlong address), `not_found` 404, `rate_limited` 429, `mail_error` 502, `bad_request` 400, `unauthorized` 401. --- #### GET /reports/summary Turnover for a period with breakdowns by tax label, payment type and cashier, computed exactly as the Извештаји screen computes it. Parameters: `from` and `to`, both `YYYY-MM-DD`, both defaulting to today. `from` must not be later than `to`. **Accounting rule, returned in every report as `rule`:** only issued `Normal` and `Advance` invoices count, refunds subtract from turnover, and `ProForma`, `Copy` and `Training` never enter a total. ```json { "from": "2026-08-01", "to": "2026-08-20", "totals": { "invoices": 214, "turnover": 486320.50, "refunds": {"count": 3, "amount": 2140.00} }, "byTaxLabel": [ {"label": "Ђ", "name": "О-ПДВ", "rate": 20.0, "turnover": 401220.50}, {"label": "Е", "name": "П-ПДВ", "rate": 10.0, "turnover": 85100.00} ], "byPaymentType": [ {"paymentType": "Cash", "amount": 302150.50}, {"paymentType": "Card", "amount": 184170.00} ], "byCashier": [ {"cashier": "Касир 1", "invoices": 141, "turnover": 320410.50} ], "rule": "Turnover counts issued Normal and Advance receipts only; refunds subtract; ProForma, Copy and Training are excluded." } ``` `totals.refunds.amount` is a positive number and is informational: it is already subtracted from `totals.turnover`. In `byTaxLabel`, `name` and `rate` are `null` when a label used on old invoices is no longer defined. Errors: `validation` 422 (date not `YYYY-MM-DD`, or `from` later than `to`), `unauthorized` 401, `rate_limited` 429. --- #### GET /reports/daily Per-day turnover and invoice count for a period, same accounting rule and same grouping query as the daily table of the Извештаји screen. Parameters `from` and `to`, as above. ```json { "from": "2026-08-18", "to": "2026-08-20", "days": [ {"date": "2026-08-18", "invoices": 41, "turnover": 92310.00}, {"date": "2026-08-20", "invoices": 12, "turnover": 24880.50} ], "rule": "Turnover counts issued Normal and Advance receipts only; refunds subtract; ProForma, Copy and Training are excluded." } ``` Days with no traffic are omitted rather than returned as zero rows. Errors: `validation` 422, `unauthorized` 401, `rate_limited` 429. --- #### GET /items The item catalogue sorted by name, at most 1000 rows, no filters and no pagination. Inactive items are included and carry `active: false`. The `category` of an item is not exposed here. ```json { "items": [ {"id": 12, "plu": "1001", "name": "Кафа еспресо", "unit": "ком", "price": 180.0, "label": "Ђ", "gtin": "8600123456789", "active": true} ] } ``` Errors: `unauthorized` 401, `rate_limited` 429. --- #### POST /items Create an active item. | Field | Required | Notes | | --- | --- | --- | | `name` | **yes** | Up to 200 characters | | `label` | **yes** | Must be an active tax label | | `price` | no | Default 0, must be 0 or more, rounded to 2 decimals | | `unit` | no | Default `ком` | | `plu` | no | Stored as sent, uniqueness is not checked on this route | | `gtin` | no | 8 to 14 digits | Response `201`: `{"id": 12, "ok": true}`. Errors: `validation` 422, `bad_request` 400, `unauthorized` 401, `rate_limited` 429. --- #### PUT /items/{id} and PATCH /items/{id} The same handler: a partial update where every absent field keeps its stored value. Accepts `plu`, `name`, `unit`, `price`, `label`, `gtin` and `active`. Unlike `POST /items`, this route does **not** re-validate the tax label, the price or the GTIN: what you send is what gets stored. Send `label` values that `GET /tax-labels` returns. Response `200`: `{"ok": true}`. Errors: `not_found` 404, `bad_request` 400, `unauthorized` 401, `rate_limited` 429. --- #### DELETE /items/{id} Sets `active` to false. Items are never physically deleted, because issued invoices reference them. The answer is `{"ok": true}` even when no item with that id exists: this route does not check first. Errors: `unauthorized` 401, `rate_limited` 429. --- ### Integration notes - **A 502 `pfr_error` means nothing was issued.** It is safe to retry the same request. A 201 that you failed to read is not safe to retry: look the invoice up with `GET /invoices` first. - **Do not compute tax.** Send prices with VAT included and a tax label. `taxItems` on the answer is what the PFR calculated and what the receipt prints. - **Store the `uuid`** returned with each invoice. It is the key for the journal, refund, copy and e-mail routes. - **Print or forward `journal` as it is.** It is the receipt of record, in a fixed 40 column layout. - **Read `pfr.demo`.** While it is `true`, the receipt is a demo document with no fiscal validity, and your system should not present it to a customer as a fiscal receipt. - A ready-made WordPress plugin for WooCommerce shops is available at . It writes the PFR number and the verification link into each order. ### More - Product page: - User manual, Serbian Cyrillic: - API documentation page, Serbian Cyrillic: - OpenAPI 3.1 document: - Terms of use: - Privacy policy: - Contact: filip@tefis.io