Skip to content

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/v1

Everything 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_9tK2xQ7mA4vD8nR1sB6yE3wZ0pL5cH

Keys 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 endpointcistest.apis-it.hr (TEST)cis.porezna-uprava.hr (PROD)
CertificatesDemo certificatesReal certificates
JIRs returnedReal-looking, no legal effectReal fiscal records
Companies it can reachCompanies in test modeCompanies 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.

  1. A company — the fiscal identity. It holds the OIB, the VAT registration status and the mode. POST /v1/companies
  2. 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
  3. A device with a premises code and an ISU number. premises_code is the business premises code (OznPosPr) of the poslovni prostor the machine sells from, and isu_number is the self-service device identifier (OznNapUr). Both must match what was registered in ePorezna. POST /v1/devices

Your first request

Terminal window
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