# Fiscomm API > Fiscomm is a fiscalization SaaS for Serbia (fiscal receipts via the Tax Administration, PURS/SUF). You send order data and get back a fiscal receipt — number (PFR), QR, PDF and a verification link. No physical device, no manual entry. This page is written for LLM agents building an integration; the machine-readable spec is at https://api.fiscomm.rs/openapi.json and the human reference at https://api.fiscomm.rs/docs. ## Basics - Base URL: https://api.fiscomm.rs (the same host for everyone). - Auth: header `Authorization: Bearer `. A key is bound to a single shop; you never send a `shopId` — the server resolves the shop, company and certificate from the key. Both older (V1) and current (V2) keys work. - Check a key: - `POST /auth/verify-api-token` → 201 `{ type, authable:{ shopId, companyId, id }, apiKey:{ id, version, name }, user }` - `GET /auth/api-key/me` → `{ apiKeyId, apiKeyName, companyId, companyName, shopId, shopName }` ## Environments — it is the ACCOUNT, not the host The host and the API are identical for testing and for production. What differs is the account you authenticate as, and specifically the certificate (security element) attached to that account's shop: - A **demo account** carries a test certificate. Its receipts are routed to the Tax Administration sandbox, are NOT legal fiscal documents, and it returns a different, test set of tax labels. Use a demo-account key while you build. - A **production account** carries your real certificate (obtained from the Tax Administration via the ePorezi portal). Its receipts are legal fiscal documents and it returns the real tax labels. The account's certificate alone decides which Tax Administration environment is used, whether receipts are real, and which tax labels apply. You do not change host or request shape between environments — you just use the right account's key. ## Training receipts (separate from the account) `invoiceType: training` issues a training receipt — a non-fiscal receipt clearly marked "ОВО НИЈЕ ФИСКАЛНИ РАЧУН" (this is not a fiscal receipt). It runs through the full flow but never produces a legal document, on any account. Use it to exercise the integration without issuing a real receipt. Only the path segment changes: - Test the flow: POST /receipt/training/sale - Real receipt: POST /receipt/normal/sale (on a production account) ## Tax labels — never hardcode; pull from GET /receipt/tax-rates Labels and rates depend on the account's tax group (returned per certificate) and differ between a demo account and a production account. - Production account: `Ђ`=20% (standard VAT), `Е`=10% (reduced VAT), `Г`=0% (VAT-exempt), `А`=0% (not in the VAT system). - Demo account (test group): `Ж`=19%, `A`=10%, `F`=11%, `B`/`N`/`C`=0%, `E`=6%, `P`=0.5%, `T`=2%. The response has `currentTaxRates` (the active group — use this for new receipts) and `allTaxRates` (historical groups, only for retroactive processing). Each item's `labels` array uses these label codes. ## Issue a receipt (minimal example) POST /receipt/{invoiceType}/sale (invoiceType: normal | training | advance | proforma | copy) Required fields: `orderNumber` (string, unique per shop) and `items[]`. `metaFields` is optional — an object of your own key/values echoed onto the archived receipt. Each item: `name`, `quantity`, `unitPrice`, `totalAmount`, `labels[]` (tax label codes). `gtin` is optional; it is validated by the Tax Administration in production, but the sandbox may not enforce it. Each payment: `type` (cash | card | wireTransfer | check | voucher | mobileMoney | virman | mobile | factoring | account | other), `amount`. The sum of items must equal the sum of payments (or set `settings.skipAmountValidation: true`). Choosing a label: always pull the current codes from `GET /receipt/tax-rates` — do not copy `Ђ` from the example blindly. Two gotchas: (1) `Ђ` (and other VAT codes) are production labels — on a demo account they are rejected with `ERR_01010` (PURS code `2310`). Not every label listed for an account is actually enabled on its security element; if you get `ERR_01010`/`2310`, pick another. On the demo account, `A`, `E` and `F` reliably fiscalize — use `A` for the examples below. (2) Cyrillic labels (`Ђ`, `Ж`, `А`…) must be sent as proper UTF-8. curl: curl -X POST https://api.fiscomm.rs/receipt/training/sale \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"orderNumber":"ORDER-1001", "items":[{"name":"Item","quantity":1,"unitPrice":6000,"totalAmount":6000,"labels":["A"]}], "payments":[{"type":"cash","amount":6000}], "metaFields":{}}' TypeScript SDK (github:Fiscomm/fiscomm-sdk-ts): import { FiscommClient } from 'fiscomm-sdk'; const client = new FiscommClient({ apiKey: process.env.KEY }); const res = await client.receipts.sale('training', { orderNumber:'ORDER-1001', items:[...], payments:[...], metaFields:{} }); PHP SDK (composer require fiscomm/sdk:dev-main): use Fiscomm\Sdk\FiscommClient; $client = new FiscommClient($apiKey); $res = $client->receipts()->sale('training', $dto); ## Request fields (reference) Top level: `orderNumber` (string, required, unique per shop), `items` (array, required), `payments` (array; required for sale/refund), `metaFields` (object, optional), `referentDocumentNumber` (string; required for refund and advance/finalize), `referentDocumentDt` (ISO date; required for refund), `buyerId` (string `"type:id"`; required for refund unless `settings.skipBuyerIdValidation`), `dateTimeOfIssue` (ISO date, optional), `settings` (object, optional). Item: `name` (string, required), `quantity` (number, required), `unitPrice` (number, required), `totalAmount` (number, required), `labels` (string[], required — codes from `/receipt/tax-rates`), `gtin` (string, optional), `discount` (number, optional). Payment: `type` (enum, required), `amount` (number, required), `advanceAmount` (number, optional — advance only, added on top of `amount`). `settings` flags (all optional booleans unless noted): `returnIfOrderNumberExists`, `skipOrderNumberValidation`, `skipInvoiceNumberValidation`, `skipAmountValidation`, `skipBuyerIdValidation`, `sendToEmail`, `isFinal`, `orderNumberUnderReceipt`, `textHeader` (string), `textFooter` (string). The server does not pre-validate tax labels against your account — an unknown or not-enabled label is forwarded to the Tax Administration and comes back as `ERR_01010`. Filter labels yourself against `/receipt/tax-rates`. The full machine-readable schema (types, every field) is at https://api.fiscomm.rs/openapi.json. ## Response (what you get, and the gotchas) `{ correlationId, receipt: { invoiceNumber, invoiceCounter, invoiceCounterExtension, sdcDateTime, verificationUrl, invoicePdfUrl, totalAmount, invoiceType, transactionType, orderNumber, taxItems[], metaFields, additional:{ mrc, signedBy, signature, tin, requestedBy, taxGroupRevision, messages }, journal } }` - `correlationId` is PER REQUEST (one per HTTP call), NOT per receipt; it is absent from the `bulk` response. - `invoiceNumber` is the PFR number (e.g. `AZM5ZCCR-Dt1Ov2o0-507308`); `invoiceCounter` looks like `468497/507302ОП`. - `sdcDateTime` is the Tax Administration timestamp, ISO 8601 in UTC (`Z`). - `verificationUrl` is the customer's verification link with the Tax Administration — store it with the order. - The QR IMAGE (`qrCodeFileUrl`) is NOT in this response. Get it from `GET /archive/receipts/{invoiceNumber}`, which returns a wrapper `{ receipt, chain, chainMetadata }` — read `receipt.qrCodeFileUrl` (the QR image is rendered via a third-party image service). - `metaFields` is echoed back on the issue response exactly as sent (the same object is also stored on the archive record). ## Idempotency (required for retry on a lost response) - `orderNumber` is unique per shop + company + invoiceType + transactionType; window ~14 days. - On a lost/timed-out response, resend the SAME request with `settings.returnIfOrderNumberExists: true` → it returns the EXISTING receipt instead of creating a duplicate. - To confirm what actually went through (without keeping state on your side): POST /archive/receipts/fiscalization-lookup {"orderNumbers":["ORDER-1001"]} → {"records":[{"orderNumber","fiscalized":true|false,"invoiceNumber","receiptId","sdcDateTime"}]} ## Refund / void - POST /receipt/{invoiceType}/refund — requires `referentDocumentNumber` (= the receipt's invoiceNumber), `referentDocumentDt`, and `buyerId`. - Whole receipt: POST /receipt/{invoiceNumber}/refund/full — requires `orderNumber`, `items`, and `buyerId`. NOTE: this path has NO `invoiceType` segment — it is literally `/receipt/{invoiceNumber}/refund/full` (e.g. `/receipt/AZM5ZCCR-…-507357/refund/full`), unlike the sibling `/receipt/{invoiceType}/…` routes. Calling `/receipt/training/{invoiceNumber}/refund/full` returns 404. - `buyerId` format `"type:identifier"` (e.g. `"10:PIB"`, `"11:JMBG"`). Natural person without an ID number → `settings.skipBuyerIdValidation: true`. ## Advance chain (two calls) It is NOT a single self-contained call. You issue an advance, then finalize it referencing that advance: 1. **Advance receipt:** `POST /receipt/advance/sale` with `items`, `payments` and `orderNumber`. The payment `amount` must equal the items total (as for any receipt); `payments[].advanceAmount` is optional and is ADDED on top of `amount`, so do not put the same value in both or the totals will not match (→ ERR_00701). The response `invoiceNumber` is the advance's counter (extension `АП`). 2. **Finalize:** `POST /receipt/advance/finalize` with `referentDocumentNumber` set to the advance's `invoiceNumber` (required — omitting it returns ERR_00201), plus `items`, `payments`, `orderNumber`. The response is `{ correlationId, refundReceipt, finalReceipt }` — it cancels the advance (a refund, extension `АР`) and issues the final sale (extension `ПП`). - Sandbox note: on a demo account, advance items must use tax label `A`, `E` or `F` (other labels return ERR_00302, "In sandbox mode, use A, E or F labels for advance items"). ## Useful settings (in the request `settings` object) - `returnIfOrderNumberExists` — idempotent retry (return existing receipt). - `skipOrderNumberValidation` / `skipInvoiceNumberValidation` — bypass uniqueness checks. - `skipAmountValidation` — allow items total ≠ payments total (rounding-driven systems only). - `skipBuyerIdValidation` — refund a natural person without an ID number. - `sendToEmail` — email the receipt to the buyer. - `textHeader` / `textFooter` / `orderNumberUnderReceipt` — receipt print options. - `isFinal` — mark the final receipt in an advance chain. ## Errors (HTTP + code) Every error is `{ status, code, message, userMessage?, details?, correlationId }`. Always send `correlationId` to support. - 400 `ERR_00202` = input validation (`details.validationErrors: [{field, message}]`). - 400 `ERR_00603` = a receipt with this `orderNumber` already exists (see Idempotency; use `settings.returnIfOrderNumberExists` to get it back). - 400 `ERR_00701` = items total does not equal payments total. - 400 `ERR_00201` = a referenced document is missing (e.g. `advance/finalize` without `referentDocumentNumber`). - 409 `ERR_00302` = tax-label rule for advance items (sandbox: use `A`, `E` or `F`). Note: this is HTTP 409, not 400. - 400 `ERR_01010` = the Tax Administration rejected the receipt (`details.pursRawResponse` has the reason, e.g. an invalid `gtin`). - 401 `ERR_00101` = missing or invalid key. - 403 = valid key without access to that shop. - 404 = not found. - 429 = a concurrent `bulk` for the same shop (bulk is serialized per shop). - 500 `ERR_00002` = internal error (retry later). ## Bulk - `POST /receipt/{invoiceType}/{transactionType}/bulk`, up to 30 receipts, one bulk per shop at a time (concurrent → 429). - Request body wraps the array: `{ "receipts": [ { …same fields as a single sale… }, … ] }` (a bare array is rejected). - Response: `{ receipts:[…], failedReceipts:[…], totalProcessed, successful, failed }` — note there is no top-level `correlationId` on bulk. ## Rate limit / async / webhook - No general rate limit. - Webhook (asynchronous flow): configured per request (in the body) or per shop; it delivers the raw fiscalReceipt. - ⚠️ The async endpoints (`/receipt/{type}/sale|refund/async`) may be unavailable and can return `500 ERR_00002` on some accounts. Prefer the synchronous endpoints unless async is confirmed enabled for your account; use `fiscalization-lookup` to reconcile long-running calls. ## Endpoint summary - POST /receipt/{invoiceType}/sale | /refund (invoiceType includes `advance` → POST /receipt/advance/sale) - POST /receipt/{invoiceType}/sale|refund/async (may be unavailable — can return 500; see Rate limit / async) - POST /receipt/advance/finalize (finalize an advance; see Advance chain) - POST /receipt/{invoiceNumber}/refund/full - POST /receipt/{invoiceType}/{transactionType}/bulk (body `{ receipts:[…] }`, up to 30) - GET /receipt/tax-rates - POST/GET/PUT/DELETE /receipt/drafts[/{id}] (note: creating a draft requires `draftName`, `orderNumber` and `items`) - POST /receipt/send-email — body `{ invoiceNumber, email, cc?[], bcc?[] }` → `{ success, correlationId }` - GET /archive/receipts | /archive/receipts/{invoiceNumber} (an unknown invoiceNumber returns 404; use fiscalization-lookup to check several at once) - POST /archive/receipts/fiscalization-lookup - POST /auth/verify-api-token | GET /auth/api-key/me ## Full reference / contact - Full OpenAPI reference: https://api.fiscomm.rs/docs (machine-readable: https://api.fiscomm.rs/openapi.json) - Support: podrska@fiscomm.rs