API documentation
Conventions
Rules that apply to every call: identifiers, idempotency, document statuses and how to read them, amounts, time, invoice types, references, payment types, buyer, pagination and the shape of errors.
Identifiers in every request
| Field | What it is | Where from |
|---|---|---|
taxpayerId | the taxpayer (legal entity with a PIB) | GET /v1/taxpayers |
businessPremiseId | the premise (Tax Administration business premise) with its own security element | GET /v1/taxpayers/{taxpayerId}/business-premises |
clientReference | your reference: order, invoice or transaction number; searchable in the journal | your system |
cashier.id | the cashier or system issuing the receipt; printed on the receipt | your system (for example web-shop) |
Idempotency-Key | the header that prevents a duplicate receipt | your system, from clientReference and the kind of action |
Idempotency-Key
Every fiscal mutation (POST to /v1/fiscal-documents, /copies, /v1/refund-workflows, /v1/advance-cases*, /v1/proforma-training-workflows, /v1/receipt-deliveries) requires an Idempotency-Key header of 16 to 200 characters. The key is unique within your organization and is bound to the canonical content of the request:
- same key, same content: the API returns the original operation (200 instead of 201) and never issues a second receipt, however many times you repeat;
- same key, different content:
409 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_REQUESTwith the original'soperationId; - new key: a new operation, and potentially a new receipt.
{
"code": "IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_REQUEST",
"operationId": "9f8e7d6c-5b4a-4321-8765-0fedcba98761"
}Document statuses
Every fiscal request becomes a durable operation with a status. The status is the truth; the HTTP code is only its summary.
| Status | Meaning | HTTP | `fiscalized` | `retryable` |
|---|---|---|---|---|
FISCALIZED | The V-PFR signed the receipt and the response is durably stored. The only status that means a receipt. | 201 (200 on replay) | true | false |
REJECTED | The V-PFR rejected the request (validation). Not a receipt. pfrRejection on a single read names the field. | 422 | false | false |
NOT_FISCALIZED | The V-PFR was unavailable before submission; nothing was sent. | 503 | false | true |
OUTCOME_UNKNOWN | The request may have been sent; the response did not arrive. Not a receipt, but it may become one. Never retry. | 503 | false | false |
RECONCILING | BokaPOS is checking with the V-PFR whether an unknown outcome exists (read-only). Transitional. | 503 | false | false |
RECEIVED, VALIDATED, SUBMITTING | Transitional statuses during the synchronous call; visible only in the journal while processing. | n/a | false | false |
Reading the response
01
201 and fiscalized: true
The receipt exists. Store
id,pfr.invoiceNumber,pfr.sdcTime,pfr.verificationUrland, if needed, the PDF. The order is fiscalized.02
503 and retryable: true
Nothing was sent (
NOT_FISCALIZED, for examplePFR_SUBMISSION_FAULTorCURRENT_TAX_CONFIGURATION_UNAVAILABLE). Wait a few seconds and send the same request with the same key. Cap the number of attempts, then hand over to an operator.03
503 and status OUTCOME_UNKNOWN
Do not send a new request for the same sale. Remember the
idand pollGET /v1/operations/{id}(every 30 seconds, then less often). BokaPOS resolves the outcome in the background purely by reading from the V-PFR; once found, the status becomesFISCALIZED. If it stays unknown, an operator decides in the portal.04
422
The request breaks a rule (ours or the V-PFR's). Nothing was issued. Fix the request and send it with a new key, because the old key is bound to the wrong content.
05
403
Environment or licence. Nothing was issued. Sandbox never gets a 403 for licensing.
06
409
A conflict: the key reused with different content, or an action not allowed in the current workflow state (advance, proforma). Read the state and continue from it.
Amounts, quantities and rounding
- All prices and amounts are gross, in dinars, with at most two decimals (
8990.00). More decimals are rejected by the V-PFR (code 2804). unitPriceis the final unit price after discount. You do not send tax: BokaPOS computes it from the tax label by the official rules and the V-PFR signs it.unitPriceBeforeDiscountis an optional pre-discount price, displayed only outside the fiscal part of the receipt; it must be greater thanunitPrice.quantityhas up to three decimals (1.5), minimum0.001.- The sum of
quantity × unitPriceover the items, rounded to two decimals, must equal the sum ofpayments. Otherwise422with an error ontotals. - Several payment types on one receipt are allowed (
paymentsis a list).
Time
- All times are ISO 8601.
createdAtandupdatedAtare UTC (Z).pfr.sdcTimeis the V-PFR signing time with the offset the V-PFR sent (+02:00or+01:00); it is the official receipt time. - The
pfrFrom/pfrTofilters in the journal and reports refer tosdcTime;createdFrom/createdToto the moment BokaPOS received the request. The lower bound is inclusive, the upper exclusive. - You do not send the receipt time. The only exception is an advance payment by wire transfer received earlier (
paymentOccurredAt), per the official rule; see Advance.
Invoice and transaction types
| `invoiceType` | `transactionType` | Official | Operation |
|---|---|---|---|
NORMAL | SALE | Промет Продаја | POST /v1/fiscal-documents |
NORMAL | REFUND | Промет Рефундација | POST /v1/refund-workflows |
COPY | SALE / REFUND | Копија | POST /v1/fiscal-documents/{id}/copies (and automatically on a cash refund) |
ADVANCE | SALE / REFUND | Аванс | /v1/advance-cases/... |
PROFORMA | SALE / REFUND | Предрачун | POST /v1/proforma-training-workflows |
TRAINING | SALE / REFUND | Обука | POST /v1/proforma-training-workflows |
POST /v1/fiscal-documents accepts only NORMAL SALE; everything else has its own workflow that BokaPOS runs on the server, with the correct references and order.
References between documents
A refund, a copy and the final receipt after an advance must reference the source document (PFR number and time). When the source is a document BokaPOS issued, you send only its fiscalDocumentId ({ "source": "BOKA", "fiscalDocumentId": "..." }) and BokaPOS fills in the exact PFR facts; it must be on the same taxpayer and premise. A reference to another ESIR's document (source: EXTERNAL) requires the exact PFR number, time and type and is supported only for a copy through POST /v1/fiscal-documents; refunds and advances do not accept external documents.
| New document | May reference |
|---|---|
| Промет Рефундација | Промет Продаја (BokaPOS source) |
| Копија | a fiscalized Промет or Аванс, sale or refund |
| Аванс Продаја (next payment) | the previous Аванс Продаја of the same case (automatic) |
| Аванс Рефундација | the latest Аванс Продаја (automatic) |
| Промет Продаја (final receipt) | the closing Аванс Рефундација (automatic) |
| Предрачун Рефундација | Предрачун Продаја (BokaPOS source) |
| Обука Рефундација | Обука Продаја (BokaPOS source) |
Payment types
| `type` | Official | Typical use |
|---|---|---|
CARD | Платна картица | card payment online |
WIRE_TRANSFER | Пренос на рачун | bank transfer, payment slip, e-banking |
INSTANT_PAYMENT | Инстант плаћање | IPS QR, instant transfer |
CASH | Готовина | cash on delivery; a cash refund requires the signed copy |
VOUCHER | Ваучер | voucher, gift card, corporate card (with buyer optional field 50:) |
CHECK | Чек | rare |
OTHER | Друго безготовинско плаћање | any other non-cash payment |
A premise may run in the restricted payment mode (only OTHER, CASH, WIRE_TRANSFER, VOUCHER); other types then return 422 PAYMENT_TYPE_NOT_ALLOWED_ON_PREMISE. The mode is the premise's paymentMode field.
Buyer identification
buyer.id is the official prefix:value. It is mandatory on every refund, on sales to companies that need the PIB on the receipt, and in the other cases the regulations list. For a domestic company (10:, 12: or 14: with a valid PIB) BokaPOS automatically prints the buyer's name and address from the NBS register under the identification, on every representation; it also returns them in buyerDetails.
| Prefix | Value |
|---|---|
10: | PIB of a domestic legal entity or entrepreneur |
11: | JMBG of a domestic natural person conducting an independent activity |
12: | PIB and JBKJS of a budget user, PIB:JBKJS |
13: | pensioner card number |
14: / 15: / 16: | PIB, JMBG or BPG of an agricultural holding |
20: | identity card number |
21: | refugee document number |
22: | EBS of a foreigner residing in Serbia |
23: | domestic passport number |
30: | foreign passport number |
31: to 36: | diplomatic and foreign identity cards per the official list |
40: | foreign tax identification number (TIN) |
buyer.optionalField is the buyer's optional field, also prefix:value: 20: SNPDV, 21: LNPDV, 30: to 33: PPO-PDV forms, 50: corporate card number (payment is VOUCHER), 60: corporate-card refund period ddMMyyyy_ddMMyyyy. The same prefix means different things in the two fields.
Pagination
Journal lists use a stable keyset: the response carries nextCursor, which you send as cursor for the next page; null is the end. pageSize goes up to 200 (default 50). The product list uses the same pattern with a UUID cursor. Ordering is by createdAt then id, descending, so a new page never skips or repeats an entry.
Error shapes
| Shape | When | Example |
|---|---|---|
{ "code": "...", "message"?: "..." } | a BokaPOS or V-PFR rule, 4xx and 5xx | { "code": "TAX_LABEL_NOT_CURRENT", "invalidLabels": ["Ђ"] } |
RFC 9457 problem (application/problem+json) | field validation, 422 | { "status": 422, "errors": { "items[0].unitOfMeasure": ["..."] } } |
{ "code": "IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_REQUEST", "operationId": "..." } | 409 | see above |
a fiscal document with fiscalized: false | 503 on a fiscal call | the status and failureCode say what happened |
The full catalogue of codes with what to do is on the Errors page.