Getting started
FiskHub fiscalizes B2C sales made at unattended, self-service devices in Croatia. A device posts a sale, FiskHub signs it and forwards it to the Tax Administration, and the JIR comes back in time to print the receipt.
Base URL
https://api.fiskhub.com/v1Everything is under /v1. The version is in the path and never changes meaning — see
versioning.
All requests and responses are JSON, UTF-8, except the two multipart uploads (certificates and device import). TLS 1.2 or better is required; plain HTTP is refused rather than redirected.
Authentication
Every request carries a bearer token:
Authorization: Bearer fh_live_9tK2xQ7mA4vD8nR1sB6yE3wZ0pL5cHKeys look like fh_live_… or fh_test_…. They are shown once, when created, and stored only as
a SHA-256 hash — a lost key is replaced, never recovered. Revoke a key the moment you suspect it
has leaked; revocation takes effect immediately and the key then returns
api_key_revoked.
Three endpoints need no key at all: GET /v1/health,
GET /v1/health/fiscal and
GET /v1/countries/{code}/schemas. The fiscal probe needs no
certificate and no tenant either — it is an unsigned echo round trip.
Test and live mode
The key prefix decides which Tax Administration endpoint your requests reach. Nothing else does.
fh_test_… | fh_live_… | |
|---|---|---|
| Tax Administration endpoint | cistest.apis-it.hr (TEST) | cis.porezna-uprava.hr (PROD) |
| Certificates | Demo certificates | Real certificates |
| JIRs returned | Real-looking, no legal effect | Real fiscal records |
| Companies it can reach | Companies in test mode | Companies in live mode |
There is deliberately no request parameter that switches modes. Swapping the key is the only
way, so a test key cannot accidentally file a real receipt and a live key cannot quietly write
test data into your books. Using a key against a company in the other mode returns
mode_mismatch.
What has to exist before a device can fiscalize
Fiscalization needs a complete chain. Missing any link produces a specific, actionable 4xx rather than a cryptic upstream failure.
- A company — the fiscal identity. It holds the OIB, the VAT registration status and the
mode.
POST /v1/companies - A certificate for that company — a PKCS#12 whose subject OIB equals the company’s OIB.
Certificates are per company, never per tenant, because the Tax Administration rejects any
message whose payload OIB differs from the signing certificate’s OIB.
POST /v1/certificates - A device with a premises code and an ISU number.
premises_codeis the business premises code (OznPosPr) of the poslovni prostor the machine sells from, andisu_numberis the self-service device identifier (OznNapUr). Both must match what was registered in ePorezna.POST /v1/devices
Your first request
curl -sS https://api.fiskhub.com/v1/invoices \ -H "Authorization: Bearer $FISKHUB_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "device_id": "6f1c0f9e-2b7a-4a56-9d3e-1f0b8a4c7d21", "occurred_at": "2026-08-27T10:32:11+02:00", "total": "4.50", "currency": "EUR", "payment_method": "K", "vat": [{ "rate": "25.00", "base": "3.60", "amount": "0.90" }] }'{ "id": "inv_01J9Z4T7M2K8Q6R3V5X1Y0B2C4", "status": "confirmed", "jir": "2cf55235-9470-4b5c-a539-463f52b109d2", "zki": "e4d909c290d0fb1ca068ffaddf22cbd0", "qr_payload": "https://porezna.gov.hr/rn?jir=2cf55235-9470-4b5c-a539-463f52b109d2&datv=20260827_1032&izn=450", "number": "1041", "occurred_at": "2026-08-27T10:32:11+02:00", "total": "4.50", "currency": "EUR"}Idempotency-Key is required on this endpoint. It is the first line of defence against
fiscalizing the same sale twice — see idempotency.
Read the walkthrough in fiscalizing a sale before writing the real integration, and then handling the no-JIR response before shipping it.
Money is a string
Every amount in this API is a JSON string with exactly two decimal places and a dot
separator: "4.50", "1525.99", "0.00", "-105.50".
Never send a JSON number. Binary floating point cannot represent cents exactly, and the ZKI is a
hash over the concatenated string form — 4.5 and 4.50 are different inputs and produce a
different, wrong ZKI. "12.5" is rejected with
invalid_request rather than silently padded.
Time is the sale moment
occurred_at is ISO 8601 with an explicit offset, and it is the moment the sale happened,
not the moment you called us.
It feeds the ZKI, it feeds the fiscal record, and it starts a 30-day clock: a sale more than 30 days old is rejected by the Tax Administration permanently. A device that has been offline sends the original sale time and FiskHub marks the submission as late delivery automatically. Sending “now” instead would produce a fiscal record that disagrees with the receipt the customer is holding.
Errors
Every 4xx and 5xx has the same shape:
{ "error": { "type": "validation_error", "code": "hr.device_missing_isu", "message": "Device VM-0043 has no ISU number assigned. Register the premises in ePorezna and add the ISU number before fiscalizing.", "param": "device_id", "upstream": null, "doc_url": "https://docs.fiskhub.com/errors/hr.device_missing_isu" }}Branch on code. It is stable forever and never renamed. message is written for humans and may
be reworded. doc_url always resolves to a page explaining what happened, whether to retry, and
the concrete fix — see the error taxonomy.
New codes may be added over time, so treat an unrecognised code as its broader type rather
than crashing.
Correlation
Send your own X-Request-Id on any request and it is echoed back and attached to every log line
we write for it. Quote it in support requests. If you omit it we generate one and return it in
the X-Request-Id response header.
Next
- Fiscalizing a sale — the endpoint in full.
- Handling the no-JIR response — the branch that decides whether your integration helps or hurts.