API documentation
Errors
Every failure has an HTTP status, a code and a clear rule about what may be retried. Here is the catalogue of codes an integrator meets, the V-PFR rejection codes and the recommendations for robust handling.
Updated: 29 August 2026 · Contract version 1.0.0
Response shapes
{ "code": "..." }with an optionalmessageand extra fields (invalidLabels,module,operationId): a BokaPOS rule, licence or environment.- An RFC 9457 problem with
errorsper field (application/problem+json): validation of the request shape, always 422. - A fiscal document with
fiscalized: false(503): the V-PFR unavailable or an unknown outcome;status,failureCodeandretryablesay what next. - A workflow (refund, proforma, advance) with its own
status/stateand embedded documents: the error is read from the workflow state.
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.21",
"title": "One or more validation errors occurred.",
"status": 422,
"errors": {
"totals": [
"The item and payment totals must match after the mandated two-decimal currency rounding."
]
}
}{
"code": "MODULE_NOT_LICENSED",
"module": "advance",
"message": "This module is not included in the organization's licence. Contact BokaPOS Administration to enable it."
}{
"code": "TAX_LABEL_NOT_CURRENT",
"invalidLabels": ["Ђ"],
"message": "Every tax label must be present in the freshly fetched current PFR configuration."
}{
"id": "9f8e7d6c-5b4a-4321-8765-0fedcba98761",
"taxpayerId": "3f9c2a8e-6b1d-4e5a-9c47-1d2b8e6f0a11",
"businessPremiseId": "b7d4e2c1-9a3f-4c8e-8f21-6e5a0c9d3b22",
"idempotencyKey": "order-4127-sale-1",
"clientReference": "ORDER-4127",
"invoiceType": "NORMAL",
"transactionType": "SALE",
"cashierId": "web-shop",
"buyerId": null,
"buyerDetails": null,
"status": "NOT_FISCALIZED",
"fiscalized": false,
"failureCode": "PFR_SUBMISSION_FAULT",
"retryable": true,
"pfr": null,
"receipt": null,
"createdAt": "2026-09-01T08:15:31.902Z",
"updatedAt": "2026-09-01T08:15:32.611Z"
}Code catalogue
The Retry column: *same key* means the same request with the same Idempotency-Key makes sense; *fix the request* means new content and a new key; *later* means the state changes outside your system (licence, settings); *never* means a retry cannot help.
| HTTP | Code | Meaning | What to do | Retry |
|---|---|---|---|---|
| 401 | 401 | The token is missing, expired (valid 300 seconds) or was not issued for this API. | Obtain a new token with the client-credentials grant and repeat the call. | same key |
| 403 | CREDENTIAL_ENVIRONMENT_MISMATCH | A sandbox credential is trying to use a production security element or the other way round. Rare: the other environment's taxpayers, premises and documents are invisible to your key (404), so this appears only when an element and its premise disagree. | Check which credential is configured; the environment is decided by the credential, not the URL. | fix the request |
| 403 | CREDENTIAL_ENVIRONMENT_REQUIRED | The credential carries no environment (boka_env), so it cannot register a taxpayer. Credentials issued by BokaPOS always carry one. | Use a credential issued by BokaPOS Administration; if you have one and still see this, contact support. | never |
| 403 | LICENSE_REQUIRED | The organization has no licence and the call targets a production element. | Sandbox keeps working. For production contact BokaPOS Administration. | later |
| 403 | LICENSE_NOT_STARTED | The licence exists but starts later. | Read GET /v1/license for the start date. | later |
| 403 | LICENSE_EXPIRED | The licence has expired. | Contact BokaPOS Administration. Refunds and advance cancellations keep working. | later |
| 403 | LICENSE_SUSPENDED | BokaPOS suspended the licence. | Contact BokaPOS Administration. | later |
| 403 | MODULE_NOT_LICENSED | The base licence is valid but the module in module (advance or email) is not enabled. | Enable the module through BokaPOS Administration or do not use that feature in production. | later |
| 404 | TAXPAYER_NOT_FOUND | The taxpayer does not exist in your organization and environment, or is not active. | Read GET /v1/taxpayers with the same key and use the exact id; a taxpayer of the other environment is invisible. | fix the request |
| 404 | BUSINESS_PREMISE_NOT_FOUND | The premise does not exist in your environment or does not belong to the given taxpayer. | Read GET /v1/taxpayers/{taxpayerId}/business-premises. | fix the request |
| 404 | FISCAL_DOCUMENT_NOT_FOUND | The document does not exist in your organization and environment. | Check the identifier; documents of another organization or of the other environment are invisible. | fix the request |
| 409 | IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_REQUEST | The same Idempotency-Key was already used with different request content. The response carries the original's operationId. | Read the original through GET /v1/operations/{operationId}. A new sale needs a new key. | never |
| 409 | IDEMPOTENCY_KEY_REUSED_IN_OTHER_ENVIRONMENT | Your organization already used this Idempotency-Key with a credential of the other environment (for example during sandbox testing). Keys are unique per organization across both environments. | Send the request again with a new key. Nothing was issued. | never |
| 422 | LPFR_REQUIRED_FOR_IN_PERSON_SALES | The premise is not a distance-commerce premise. BokaPOS fiscalizes only distance commerce through the V-PFR. | In-person sales need an L-PFR (for example BokaLPFR). | never |
| 422 | BUSINESS_PREMISE_INACTIVE | The premise is suspended or closed. | Activate it in the portal or use another one. | later |
| 422 | PAYMENT_TYPE_NOT_ALLOWED_ON_PREMISE | The premise runs in the restricted payment mode (OTHER, CASH, WIRE_TRANSFER, VOUCHER) and the request carries another type. | Change the payment type or the premise's payment mode in the portal. | fix the request |
| 422 | ACTIVE_SECURITY_ELEMENT_REQUIRED | The premise has no active security element. | In sandbox BokaPOS assigns an element; in production the Owner uploads one in the portal and BokaPOS activates it. | later |
| 422 | TAX_LABEL_NOT_CURRENT | At least one tax label is not in the fresh V-PFR configuration (field invalidLabels). | Read GET /v1/tax-rates and use only the labels it returns; sandbox and production have different sets. | fix the request |
| 422 | TAX_LABEL_NOT_ALLOWED_OUTSIDE_VAT | The taxpayer is marked as outside the VAT system and an item carries a VAT label. | Use the non-VAT label or correct the taxpayer's VAT status in the portal. | fix the request |
| 422 | REFERENCE_DOCUMENT_NOT_FOUND | The referenced document (reference.fiscalDocumentId) does not exist. | Check the identifier from the original receipt's response. | fix the request |
| 422 | REFERENCE_DOCUMENT_NOT_FISCALIZED | The referenced document is not fiscalized, so it cannot be a reference. | A reference may point only to a FISCALIZED document. | fix the request |
| 422 | REFERENCE_DOCUMENT_SCOPE_MISMATCH | The referenced document belongs to another taxpayer or premise. | A reference must stay on the same taxpayer and premise. | fix the request |
| 422 | REFERENCE_DOCUMENT_TYPE_NOT_ALLOWED | This invoice and transaction type may not reference that kind of source document (the official reference matrix). | See the reference table on the Conventions page. | fix the request |
| 422 | REFUND_QUANTITY_EXCEEDS_ORIGINAL | The returned quantity is larger than the quantity on the original line. | Lower the quantity; a partial refund is allowed. | fix the request |
| 422 | REFUND_CUMULATIVE_QUANTITY_EXCEEDED | The sum of every refund of that line so far would exceed the original quantity. | Check the earlier refunds in the journal. | never |
| 422 | REFUND_ITEM_MUST_MATCH_ORIGINAL_LINE | Name, price, labels or GTIN do not match the original line originalLineIndex. | Copy the item from the original receipt (GET /v1/fiscal-documents/{id}/representations/canonical-json). | fix the request |
| 422 | REFUND_ORIGINAL_LINE_NOT_FOUND | originalLineIndex does not exist on the original receipt. | Indexes start at zero, in the order of the original's items. | fix the request |
| 422 | COPY_SOURCE_NOT_COPYABLE | Copy, Proforma and Training documents cannot be copied. | Copy only a Normal or Advance document. | never |
| 422 | COPY_SOURCE_NOT_FISCALIZED | The copy's source is not fiscalized. | A copy exists only for a FISCALIZED document. | never |
| 409 | ADVANCE_CASE_NOT_OPEN | The case is closed, failed or waiting for an outcome to be resolved. | Read GET /v1/advance-cases/{id} and the state field. | never |
| 422 | ADVANCE_CANCELLATION_TARGET_NOT_LATEST | Only the latest fiscalized Advance Sale can be cancelled. | Send the id of the last advanceSales entry not listed in cancelledAdvanceSaleIds. | fix the request |
| 422 | ADVANCE_CLOSE_TOTAL_MISMATCH | remainingPayments does not equal the final amount less the stored advance. | Compute the difference from advanceSales and send it; when zero, one element with amount 0. | fix the request |
| 422 | ADVANCE_CASE_HAS_NO_FISCALIZED_SALE | A case without any fiscalized Advance Sale cannot be closed (unless it carries externalAdvance). | Fiscalize a payment first. | never |
| 409 | ADVANCE_PAYMENT_SUPERSEDED | A replayed request targets a payment that is no longer the latest in the chain. | Read the case and continue from the current state. | never |
| 409 | ADVANCE_CASE_CLOSE_ALREADY_RESERVED | A closure was already reserved with a different key. | Replay the closure with the same Idempotency-Key it started with. | same key |
| 503 | ADVANCE_REFUND_NOT_FISCALIZED | The Tax Administration rejected the closing Advance Refund: the case is FAILED with the rejection code, and the reserved final receipt was never sent; it carries this failureCode, NOT_FISCALIZED, retryable: false. | Read the case failureCode and advanceRefund.pfrRejection, fix the data and open a new case. Replaying the same closure sends nothing. | never |
| 409 | PROFORMA_TRAINING_WORKFLOW_RESERVATION_CONFLICT | The source document already has an unresolved or completed refund. | Read the workflow from the workflowId in the response. | never |
| 409 | PROFORMA_TRAINING_SOURCE_ALREADY_REFUNDED | The Proforma or Training document was already refunded in full. | No further action. | never |
| 409 | CATALOGUE_SKU_ALREADY_EXISTS | The SKU already exists for that taxpayer. | Update the existing product (PUT) or use another SKU. | fix the request |
| 422 | CATALOGUE_IMPORT_TOO_MANY_ROWS | The CSV has more than 1,000 rows. | Split the import into several files. | fix the request |
| 422 | CATALOGUE_IMPORT_HEADERS_INVALID | The CSV header lacks the expected columns. | Download GET /v1/products/export as a template. | fix the request |
| 422 | JOURNAL_EXPORT_RESULT_LIMIT_EXCEEDED | More than 10,000 rows match the export filters. | Narrow the period (createdFrom, createdTo) and export in parts. | fix the request |
| 422 | RECEIPT_DELIVERY_DISABLED | The taxpayer has not enabled e-mail delivery in the portal settings. | Enable delivery in the portal (Documents, E-mail receipts) or send the receipt from your own system. | later |
| 422 | RECEIPT_DELIVERY_DOCUMENT_NOT_FISCALIZED | The document is not fiscalized, so there is nothing to deliver. | Send only documents with fiscalized: true. | never |
| 422 | RECEIPT_DELIVERY_DOCUMENT_NOT_ISSUED_TO_BUYER | An advance refund is never issued to the buyer, so BokaPOS does not send it to a buyer address. The same applies to a copy of one. | Send the final receipt of the advance case (the Normal Sale). The advance refund stays available for printing and in the electronic journal. | never |
| 503 | RECEIPT_DELIVERY_UNAVAILABLE | The platform mail transport is not configured or unavailable. Nothing was queued. | Retry later with the same key or send the receipt from your own system; fiscalization is already complete. | same key |
| 503 | PFR_SANDBOX_NOT_CONFIGURED | The fiscal adapter is not configured on this deployment. The response is a document with fiscalized: false. | Does not happen on api.bokapos.rs; appears only on local installations without the adapter. | same key |
| 503 | PFR_ENVIRONMENT_DISABLED | Fiscal traffic is disabled for this element's environment (production until it is enabled). | Sandbox works; production is enabled by BokaPOS decision. | later |
| 503 | CURRENT_TAX_CONFIGURATION_UNAVAILABLE | The V-PFR did not return a fresh tax configuration, so the request was not even reserved. | Retry later with the same key. | same key |
| 503 | PFR_SUBMISSION_FAULT | The V-PFR was unavailable before submission; the document is NOT_FISCALIZED, retryable: true. | Repeat the same request with the same Idempotency-Key after a short pause. | same key |
| 503 | OUTCOME_UNKNOWN | The request may have reached the V-PFR but the response did not come back. Status OUTCOME_UNKNOWN, retryable: false. Not a receipt, but it may become one. | Do not send a new request for the same sale. Poll GET /v1/operations/{id}; BokaPOS resolves the outcome by reading, never by resubmitting. | never |
| 422 | REJECTED | The V-PFR rejected the request. The document has status REJECTED and GET /v1/fiscal-documents/{id} returns pfrRejection with the property path and code (2310 unknown label; 2800 to 2808 required, length, range, value, format, list size). | Correct the request and send it with a new key. | fix the request |
V-PFR rejection codes
When the V-PFR rejects a request, the document gets status REJECTED, failureCode: PFR_VALIDATION_REJECTED, and a single read (GET /v1/fiscal-documents/{id}) carries pfrRejection.items with the property path from your request and the code:
| Code | Meaning | Typical cause |
|---|---|---|
2310 | unknown tax label | the label is not in the current group (BokaPOS usually catches this earlier as TAX_LABEL_NOT_CURRENT) |
2800 | required field missing | empty name, cashier, payment |
2801 | length | a name, cashier or reference too long |
2802 | range | a quantity or amount outside the allowed range |
2803 | value | a value outside the enumeration |
2804 | format | more than two decimals in a price or amount, a wrong time format |
2805 to 2808 | list size and related checks | an empty item or payment list |
A robust integration
- Derive the
Idempotency-Keyfrom the order and persist it before sending, so you can repeat the same request after an interruption. - On
503withretryable: true, retry with the same key and exponential back-off (for example 2, 5, 15 seconds), a few times at most; then flag the order for an operator. - On
OUTCOME_UNKNOWNnever send a new request: remember theidand pollGET /v1/operations/{id}until the status becomesFISCALIZEDor an operator decides. - On
422andREJECTEDrecorderrorsorpfrRejectionwith the order, fix the data source and send with a new key. - On
401get a new token and repeat the same request. On403readGET /v1/license. - Refresh tax labels when you get
TAX_LABEL_NOT_CURRENT, not before every receipt. - Set a generous HTTP timeout on fiscal calls (for example 60 seconds): the V-PFR usually answers in about two seconds, but a connection dropped on your side turns a certain outcome into an unknown one.
- Log
id,status,failureCodeandpfr.invoiceNumber; never log the token or the secret.