Skip to content

Invoices

Fiscalize a sale

POST /v1/invoices

Synchronous. Validates locally, computes the ZKI, signs the XML, sends it to the Tax Administration and waits. Typically 1–3 s; worst case about 9 s.

Idempotency-Key is required. See idempotency.

The walkthrough is in fiscalizing a sale.

Request

FieldTypeRequiredNotes
device_idUUIDyesResolves company, premises code, OIB, certificate and ISU number.
occurred_atISO 8601 with offsetyesThe sale moment. Starts the 30-day clock.
totaldecimal stringyesIznosUkupno, including all taxes and fees.
payment_methodG | K | O | TyesCash, card, other, bank transfer. O covers mixed. T is attended-only — see payment methods.
sale_typeunattended | attendednoWhich rules apply. Defaults to unattended. See attended receipts.
numberdigits, no leading zerosnoBrOznRac. Send it if you keep the sequence; omit it and FiskHub allocates the next one for that selling point. See below.
currency3 lettersnoDefaults to EUR, the only accepted value.
vattax line[]noPdv. Rates must be 0.00, 5.00, 13.00 or 25.00. Omitted: derived from total using the company’s default_vat_rate, which can only describe one rate.
consumption_taxtax line[]noPnp. Rate 0.003.00.
other_taxesnamed tax line[]noOstaliPor. Each line may carry a name (NazivPor, ≤ 100).
vat_exempt_amountdecimal stringnoIznosOslobPdv.
margin_amountdecimal stringnoIznosMarza.
non_taxable_amountdecimal stringnoIznosNePodlOpor.
fees{ name, amount }[]noNaknade, already included in total. name ≤ 100.
special_purposestring ≤ 1000noSpecNamj. Free text.
late_deliverybooleannoNakDost. Set automatically on every re-send; sending true forces it on the first attempt too.
vat_registeredbooleannoUSustPdv. Defaults to the company’s own registration flag.

Attended receipts (sale_type: "attended") accept five more. Each is rejected on an unattended sale, not ignored:

FieldTypeNotes
sequence_markP | NOznSlijed. Per premises, or per device.
register_markdigits, no leading zerosOznNapUr — the cash register’s mark. Not an ISU number.
operator_oib11 digitsOibOper — the operator who issued the receipt.
recipient_oib11 digitsOibPrimateljaRacuna — the buyer, for a B2B receipt.
paragon_numberstring ≤ 100ParagonBrRac — a hand-written receipt being copied up.

A tax line is { "rate": "25.00", "base": "3.60", "amount": "0.90" }. A named tax line adds "name": "Porez na luksuz".

Every field except device_id, occurred_at, total and payment_method is optional, and Oib and OznPosPr are deliberately not accepted at all: the OIB must equal the signing certificate’s or the Tax Administration answers s005, and the premises code is part of the invoice’s uniqueness key. Both come from the device you named.

Omitted is not the same as null

Optional fields accept null, and the two mean different things:

You sendWhat happens
nothingResolve down the chain: request → the device’s device_config → the company’s fiscal_config → the specification default.
nullExplicitly none. The element is omitted from the message and no configured default is consulted.

The distinction is the point of the chain. A device whose device_config sets special_purpose must not have that text reappear on the one sale that deliberately cleared it:

{ "device_id": "", "occurred_at": "", "total": "4.50", "payment_method": "K",
"special_purpose": null }

Configuration keys use the same snake_case spelling as these fields, so what an operator types into a device’s configuration is literally the field name they would otherwise have sent. A configured value of the wrong type is ignored rather than coerced — a JSON number where a decimal string belongs must never reach a signed fiscal message.

null inside device_config masks fiscal_config in exactly the same way.

Payment methods

G cashK cardO other / mixedT bank transfer
Unattended (§2.2)yesyesyesno
Attended (§2.1)yesyesyesyes

There is nobody at a self-service device to take a bank transfer, so T on an unattended sale is hr.invalid_payment_method. On an attended receipt T is valid, but it may not be combined with recipient_oib — restrictive error 181.

The receipt number

number is BrOznRac, and you may either supply it or let FiskHub allocate it. Both are supported so that the decision belongs to whoever already owns the sequence:

  • Supply number and it is used exactly as sent. Use this when your own system is the book of record for receipt numbering.
  • Omit number and FiskHub allocates the next one for that selling point and calendar year, inside the same transaction that stores the invoice. Allocating and inserting in one transaction is what makes it safe when several devices at the same premises sell at once.

Either way the number that was used comes back in the response, and it is the number that must be printed on the receipt.

A supplied number must be digits with no leading zeros. 0 and 007 are rejected with hr.invalid_receipt_number before anything is sent.

Sending the same number twice for the same selling point, device and year is refused with 409 invoice_already_exists rather than silently accepted. That is the business key doing its job: a sale is never fiscalized twice. It is a different thing from replaying an Idempotency-Key, which returns the original response — see idempotency.

Attended receipts

FiskHub is built for unattended sales — a self-service device, specification §2.2 — and that is what sale_type defaults to. An integration that has never sent the field behaves exactly as it always did.

sale_type: "attended" selects the ordinary receipt rules of §2.1: a receipt issued by a person, at a till, to a customer standing there. The message is a different object, and these are the differences that matter:

unattended (§2.2)attended (§2.1)
OznSlijedP only — per business premisesN or P
OznNapUrthe device’s ISU number, issued through ePoreznathe cash register’s mark, which you assign
NacinPlacG, K, OG, K, O, T
OibOperthe company’s own OIB — there is no cashierthe operator’s OIB
OibPrimateljaRacunanot in the message at allpermitted, for B2B
ParagonBrRacexplicitly unusedpermitted
Restrictive errors176–179, 182–184, v185those, plus 180 and 181
Terminal window
# An attended B2B receipt: numbered per device, issued by a named operator,
# attributed to the buyer, and paid by card because a recipient OIB may not
# be combined with T.
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-27T14:07:33+02:00",
"total": "150.00",
"payment_method": "K",
"sale_type": "attended",
"sequence_mark": "N",
"register_mark": "1",
"operator_oib": "69435151530",
"recipient_oib": "25089460814",
"vat": [{ "rate": "25.00", "base": "120.00", "amount": "30.00" }]
}'

The attended fields also participate in the configuration chain. A till that always reports the same register mark can set register_mark once in the device’s configuration and stop sending it; sale_type: "attended" itself can be configured the same way, so an attended till never has to send the discriminator at all.

Two restrictive errors that are dormant for unattended sales become live here:

RuleError
recipient_oib passes the ISO 7064 check digit (180)hr.invalid_oib
recipient_oib is not combined with payment_method: "T" (181)hr.invalid_payment_method
Terminal window
curl -sS https://api.fiskhub.com/v1/invoices \
-H "Authorization: Bearer $FISKHUB_API_KEY" \
-H "Idempotency-Key: 8f0b4e2a-6c31-4d9f-9a75-2b8e1c0d47f3" \
-H "Content-Type: application/json" \
-d '{
"device_id": "6f1c0f9e-2b7a-4a56-9d3e-1f0b8a4c7d21",
"occurred_at": "2026-08-27T21:15:02+02:00",
"total": "28.85",
"currency": "EUR",
"payment_method": "O",
"vat": [{ "rate": "25.00", "base": "18.00", "amount": "4.50" }],
"consumption_tax": [{ "rate": "3.00", "base": "18.00", "amount": "0.54" }],
"non_taxable_amount": "3.31",
"fees": [{ "name": "Povratna naknada", "amount": "2.50" }],
"special_purpose": "Automat 12 — kiosk"
}'

Response — 200

FieldAlways presentNotes
idyesThe handle for retrieval and retry.
statusyesconfirmed, retrying or failed. Branch on this.
jiryes, may be nullnull means not confirmed yet, not an error.
zkiyes32 lowercase hex. Print it.
qr_payloadyesEncode verbatim. jir= or zki= form.
numberyesThe receipt number used — yours if you sent one, otherwise the allocated one. Print it.
occurred_at, total, currencyyesEchoed back.
fiscal_erroronly without a JIR{ code, message, retry_scheduled }. Does not invalidate the sale.
{
"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=2885",
"number": "1041",
"occurred_at": "2026-08-27T21:15:02+02:00",
"total": "28.85",
"currency": "EUR"
}

The Tax Administration was unreachable — still HTTP 200, still a valid sale:

{
"id": "inv_01J9Z4T7M2K8Q6R3V5X1Y0B2C4",
"status": "retrying",
"jir": null,
"zki": "e4d909c290d0fb1ca068ffaddf22cbd0",
"qr_payload": "https://porezna.gov.hr/rn?zki=e4d909c290d0fb1ca068ffaddf22cbd0&datv=20260827_1032&izn=2885",
"number": "1041",
"occurred_at": "2026-08-27T21:15:02+02:00",
"total": "28.85",
"currency": "EUR",
"fiscal_error": {
"code": "upstream_unavailable",
"message": "Tax Administration did not respond. Will retry automatically.",
"retry_scheduled": true
}
}

Errors

StatusCodes
400invalid_request, missing_idempotency_key, hr.device_missing_isu, device_inactive, company_missing_certificate, certificate_expired, hr.certificate_oib_mismatch, hr.sale_older_than_30_days, hr.date_in_future, hr.invalid_vat_rate, hr.invalid_pnp_rate, hr.invalid_payment_method, hr.amount_out_of_range, hr.invalid_oib, hr.invalid_receipt_number
401invalid_api_key, api_key_revoked
403mode_mismatch, insufficient_scope
404invalid_request — no such device
409idempotency_key_reused, invoice_already_exists — a supplied number is already used at this selling point
429rate_limited. See rate limits.
500internal_error — retry with the same key.

Retrieve an invoice

GET /v1/invoices/{id}

Returns the stored invoice, including any change made by the retry sweep since the original POST. This is how you learn that a retrying invoice has since been confirmed. There are no webhooks.

The response is the POST shape plus the record’s metadata, and it uses the full five-value status vocabulary:

{
"id": "inv_01J9Z4V2P8N5H1D7S9F3G6J0K2",
"status": "confirmed",
"jir": "8b31c705-1f4a-49de-bc0a-2f6e5d7a9c14",
"zki": "7b1de0a44f9c3e5820a6cd1477bb92ef",
"qr_payload": "https://porezna.gov.hr/rn?jir=8b31c705-1f4a-49de-bc0a-2f6e5d7a9c14&datv=20260827_1041&izn=1230",
"number": "1042",
"occurred_at": "2026-08-27T10:41:07+02:00",
"total": "12.30",
"currency": "EUR",
"company_id": "3a5f2b18-7c4d-4e91-9a2b-6d8e0f1c3a57",
"device_id": "6f1c0f9e-2b7a-4a56-9d3e-1f0b8a4c7d21",
"payment_method": "G",
"mode": "live",
"attempts": 6,
"late_delivery": true,
"deadline_at": "2026-09-26T10:41:07+02:00",
"confirmed_at": "2026-08-27T13:15:44+02:00",
"created_at": "2026-08-27T10:41:08+02:00",
"last_error_class": null
}
Extra fieldNotes
company_id, device_idWhere the sale came from. The premises code is on the device.
payment_method, modeAs fiscalized.
attemptsHow many times it has been sent.
late_deliveryNakDost — true once submitted after the sale moment.
deadline_atoccurred_at + 30 days. Past this it becomes expired.
confirmed_atWhen the JIR arrived, or null.
last_error_classtransient, validation, certificate, or null. Only transient is swept.

The raw XML

GET /v1/invoices/{id}?include_messages=true

Adds a messages array to the response: the request and response XML of every attempt made on this invoice, oldest first. This is what the dashboard’s invoice browser shows when you open a sale and ask what was actually sent.

{
"id": "inv_01J9Z4V2P8N5H1D7S9F3G6J0K2",
"status": "confirmed",
"jir": "8b31c705-1f4a-49de-bc0a-2f6e5d7a9c14",
"number": "1042",
"messages": [
{
"id": "5e1b7d90-3c42-4a08-8f61-0d29b4e7a135",
"message_uuid": "c0a8f3d2-71b4-4e6a-9d38-5f2e1b7c0a94",
"attempt_no": 1,
"http_status": null,
"error_code": "upstream_unavailable",
"error_message": "Tax Administration did not respond within 4000 ms.",
"duration_ms": 4002,
"created_at": "2026-08-27T10:41:08+02:00",
"request_xml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><tns:RacunZahtjev …>",
"response_xml": null
},
{
"id": "9d4c2a71-0f38-4b52-a6e7-1c8b3d5f0e26",
"message_uuid": "7f31c0b5-9a2e-4d17-8c60-3b5a1e9d4f82",
"attempt_no": 2,
"http_status": 200,
"error_code": null,
"error_message": null,
"duration_ms": 611,
"created_at": "2026-08-27T13:15:44+02:00",
"request_xml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><tns:RacunZahtjev …>",
"response_xml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><tns:RacunOdgovor …>"
}
]
}
FieldNotes
message_uuid<IdPoruke> for that attempt. A fresh UUID every attempt, including retries of the same sale — the spec requires it, and it is how an exchange is identified upstream.
attempt_no1 for the original send, incrementing for each retry.
http_statusThe transport status, or null when the request never got an answer.
error_code, error_messageThe classified failure for that attempt, or null.
duration_msWall-clock time of that single exchange.
request_xml, response_xmlVerbatim. response_xml is null on a timeout.

Omit the parameter and the response is unchanged — no messages key at all.

On the fiscalization call itself

The same flag works on POST /v1/invoices and POST /v1/invoices/{id}/retry, so you can see exactly what went to the Tax Administration without a second round trip:

POST /v1/invoices?include_messages=true

On a retry the array carries every attempt, not just the latest — the sends that failed alongside the one that worked, which is usually the comparison you actually want.

Two things to know about the POST form:

  • It is a query parameter or the X-Include-Messages header — never a body field. The request body is hashed for idempotency, so a debug flag carried in the body would turn an otherwise identical resend into a 409 idempotency_key_reused. Toggling the header changes nothing about the sale, so it can never burn a key.
  • The stored idempotent response never contains the XML. Replaying a key returns the canonical response whether or not the original call asked for messages; ask again on the replay and the trail is re-read from fiscal_messages.

List invoices

GET /v1/invoices

Newest first by occurred_at. Filters combine with AND, and results are always scoped to the API key’s tenant and mode.

ParameterNotes
company_id, device_idUUID
premises_codestring, OznPosPr
statuspending, confirmed, retrying, failed, expired
from, toISO 8601 with offset. from inclusive, to exclusive.
limit1–200, default 50
cursorFrom next_cursor
Terminal window
# Today's unconfirmed invoices — the reconciliation query.
curl -sS -G "https://api.fiskhub.com/v1/invoices" \
-H "Authorization: Bearer $FISKHUB_API_KEY" \
--data-urlencode "status=retrying" \
--data-urlencode "from=2026-08-27T00:00:00+02:00" \
--data-urlencode "to=2026-08-28T00:00:00+02:00"
{
"object": "list",
"data": [
{
"id": "inv_01J9Z4V2P8N5H1D7S9F3G6J0K2",
"status": "retrying",
"jir": null,
"zki": "7b1de0a44f9c3e5820a6cd1477bb92ef",
"qr_payload": "https://porezna.gov.hr/rn?zki=7b1de0a44f9c3e5820a6cd1477bb92ef&datv=20260827_1041&izn=1230",
"number": "1042",
"occurred_at": "2026-08-27T10:41:07+02:00",
"total": "12.30",
"currency": "EUR",
"company_id": "3a5f2b18-7c4d-4e91-9a2b-6d8e0f1c3a57",
"device_id": "6f1c0f9e-2b7a-4a56-9d3e-1f0b8a4c7d21",
"payment_method": "G",
"mode": "live",
"attempts": 4,
"late_delivery": true,
"deadline_at": "2026-09-26T10:41:07+02:00",
"confirmed_at": null,
"created_at": "2026-08-27T10:41:08+02:00",
"last_error_class": "transient",
"fiscal_error": {
"code": "upstream_unavailable",
"message": "Tax Administration did not respond. Will retry automatically.",
"retry_scheduled": true
}
}
],
"has_more": false,
"next_cursor": null
}

One filtered list call is far cheaper than N single reads — see rate limits.

Retry an invoice

POST /v1/invoices/{id}/retry

Resubmits the invoice with NakDost = true and a fresh message id. The business data is unchanged, so the ZKI is unchanged.

Call it after fixing whatever made an invoice failed — the right certificate, an assigned ISU number, a replaced expired certificate. Invoices in retrying are already picked up automatically every five minutes; calling this simply retries one sooner.

Returns the same shape as POST /v1/invoices. The outcome may still be retrying if the Tax Administration is still unreachable.

Terminal window
curl -sS -X POST \
"https://api.fiskhub.com/v1/invoices/inv_01J9Z4V2P8N5H1D7S9F3G6J0K2/retry" \
-H "Authorization: Bearer $FISKHUB_API_KEY"

Terminal invoices cannot be retried

Both return 409:

  • confirmed already has a JIR. A second one would break the rule that a sale is never fiscalized twice.
  • expired is past its 30-day deadline; the Tax Administration rejects it permanently.
{
"error": {
"type": "conflict",
"code": "invalid_request",
"message": "Invoice inv_01J9Z4T7M2K8Q6R3V5X1Y0B2C4 is already confirmed with JIR 2cf55235-9470-4b5c-a539-463f52b109d2. A sale is never fiscalized twice.",
"param": "id",
"upstream": null,
"doc_url": "https://docs.fiskhub.com/errors/invalid_request"
}
}