Versioning
The version is in the path and nowhere else. There is no version header, no date-pinning, and no account-level version setting.
https://api.fiskhub.com/v1The promise
/v1 never breaks. Once you are coding against it, changes to /v1 are additive only:
- new optional request fields, never new required ones;
- new response fields, never removed or renamed ones;
- new error
codevalues, never a renamed or repurposed one; - new enum members in fields documented as open — such as
error.code; - new endpoints.
Anything that would break a working integration — a removed field, a renamed field, a narrowed
type, a changed default, a status code that means something different — ships as /v2, at a
different path, while /v1 keeps working.
This is a commitment about the contract, not about the implementation behind it. FiskHub will
change constantly; what /v1 says will not.
Writing a client that survives additive change
Three habits, and additive changes cost you nothing.
Ignore fields you do not recognise. A response gaining a field must not break parsing. Strict schema validation that rejects unknown properties turns every future improvement into an outage.
// Fine: pick what you need, ignore the rest.const { id, status, jir, zki, qr_payload, number } = await res.json();Treat an unknown error.code as its type. New codes are added when a new failure mode is
worth naming. Fall back to the broader category rather than crashing.
function classify(error) { switch (error.code) { case "hr.device_missing_isu": return alertOperator(error); case "upstream_unavailable": return keepSelling(); default: // Unknown code: fall back to the type, which is a closed set. return error.type === "validation_error" ? alertOperator(error) : retryLater(error); }}Branch on status, never on the shape of the response. jir: null is not a missing field
and not an error — see handling the no-JIR response.
What will not change under you
These are frozen for the life of /v1:
| Frozen | Why it matters |
|---|---|
| The five invoice statuses | pending, confirmed, retrying, failed, expired. A sixth would break exhaustive handling. |
Every error code in the taxonomy | Codes are never renamed or repurposed. Your switch keeps working. |
| The error body shape | type, code, message, param, upstream, doc_url. |
POST /v1/invoices request and response fields | Frozen the moment the client started building. |
| Money as a decimal string | It will never become a JSON number. |
Payment methods G, K, O for unattended sales | |
| HTTP 200 for a sale with no JIR | The behaviour this whole contract is built around. |
Two things are explicitly not part of the contract, and you must not depend on them:
error.message— written for humans and reworded freely. Branch oncode.error.upstream— raw detail from the Tax Administration, passed through for debugging. Their codes are theirs and change on their schedule.
The regulation changes; the contract absorbs it
The Croatian fiscalization specification changed four times between June 2025 and July 2026, and will change again. Absorbing that is FiskHub’s job.
When the specification adds a field or a rule, it shows up here as an additive change: a new
optional request field, a new error code for a new restrictive rule, a new value in
country_data. Your integration keeps working; you adopt the new capability when you need it.
Where a change is genuinely visible — a new mandatory field imposed by the regulation, a deadline like PROD dropping RSA-SHA1 on 31 December 2026 — you get notice before it lands, not after.
Deprecation
If /v2 is ever needed:
/v2is published with a migration guide naming every difference./v1keeps working. It does not enter maintenance mode on the day/v2ships.- Any eventual sunset comes with at least 12 months of notice, in writing, and never during a peak trading period.
- Endpoints do not disappear from
/v1in the meantime.
No part of /v1 is deprecated today.
Things that will never be added to /v1
Not because they are hard, but because the design removed the need for them:
- Webhooks. Fiscalization is synchronous — the answer is in the response. Poll
GET /v1/invoices/{id}if you want to know that aretryinginvoice was later confirmed. - A batch endpoint. Each sale is a real-time call because the device needs the result before it can print.
- eRačun (B2B invoicing) and eIzvještavanje. Different regulations, different scope, not this product.