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_SECURITY_ELEMENT_UNAVAILABLEorPFR_SUBMISSION_CANCELLED_BEFORE_SEND). Wait a few seconds and send the same request with the same key: the same operation is sent again, and the attempt that issues the receipt answers 201 with the sameid. The same goes for a 503 without a document (for exampleCURRENT_TAX_CONFIGURATION_UNAVAILABLE), where nothing was even reserved. 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 |
All seven payment types are available on every premise; a premise cannot restrict the choice. One receipt may carry several payments, and their sum must equal the total amount.
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 additionally returns the buyer's name and address from the NBS register in buyerDetails and uses them to prefill the operator's screen. The receipt itself prints only ИД купца and the buyer's optional field; the register data is not printed.
| 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.