Skip to content

Status lifecycle

An invoice has exactly one of five statuses. The same five values appear in API responses, in the database and in the dashboard — there is no separate internal vocabulary.

They are kept distinct on purpose. Conflating “not yet tried” with “tried and will retry” is how an invoice gets silently stranded until it passes its 30-day deadline and becomes permanently unfiscalizable.

The five statuses

StatusMeaningTerminalRetried automaticallySeen in a POST response
pendingPersisted, not yet attempted. Exists only inside the request.nonono
confirmedJIR received and its response signature verified.yesnoyes
retryingTransient failure. The sweep keeps trying until the deadline.noyesyes
failedValidation or certificate error. Needs a human.nonoyes
expiredPast the 30-day deadline. Permanently unfiscalizable.yesnono

In every one of these states the sale is valid and the receipt printed from the ZKI is compliant. Status describes the state of the report to the Tax Administration, not the state of the sale. See handling the no-JIR response.

How an invoice moves

Where a POST lands:

POST /v1/invoices
│ persist the invoice, compute the ZKI, sign, send, await the response
├── JIR received and signature verified ────▶ confirmed TERMINAL
├── transient failure ─────────────────────▶ retrying
│ (network, timeout, upstream s006)
└── validation or certificate error ───────▶ failed
(rejected message, bad or missing cert)
All three are HTTP 200, with a ZKI and a usable QR payload.
All three mean the sale is valid and the device must complete it.

Where it goes afterwards:

retrying ──── a sweep retry succeeds, tried every 5 min ────▶ confirmed TERMINAL
└─── deadline_at passes ─────────────────────────-─▶ expired TERMINAL
failed ────── a human fixes the cause,
then POST /v1/invoices/{id}/retry ────────────▶ confirmed TERMINAL
└─── deadline_at passes ──────────────────────────▶ expired TERMINAL
confirmed ─── nothing. Terminal.
expired ───── nothing. Terminal.
deadline_at = occurred_at + 30 days

pending appears in neither diagram as a destination because nothing ever arrives there: it is the state an invoice is in for the few hundred milliseconds between being persisted and being sent.

Two statuses are terminal — confirmed and expired. Nothing moves out of either, ever.

Each status in detail

pending

The invoice is stored and the ZKI is computed, but the Tax Administration has not been contacted yet. It exists for the few hundred milliseconds between the durability boundary and the first attempt.

You will never see it in a response. It is listed here because it appears in the database and in the status filter on GET /v1/invoices, and because its existence is what keeps “not yet tried” from being confused with retrying.

confirmed — terminal

The Tax Administration returned a JIR and the signature on their response verified. This is the end state; nothing moves out of it.

jir is populated and qr_payload carries jir=. Retrying a confirmed invoice is refused with a 409 — a second JIR for one sale would break the first invariant of the system.

retrying

The attempt failed for a reason that might succeed later: a network error, a timeout, or the Tax Administration’s own s006 system error. fiscal_error.retry_scheduled is true.

A sweep runs every five minutes, selects invoices in this state whose deadline has not passed, orders them by deadline ascending — the most endangered first — and resends them sequentially with late delivery (NakDost) set and a fresh message id. The business data does not change, so the ZKI does not change.

Nothing is required of the integrator. Most outages clear within one or two passes.

An invoice leaves retrying for confirmed when a retry succeeds, or for expired if it is still here when the deadline arrives.

failed

The attempt failed for a reason that will keep failing: a certificate problem, a rejected message, a validation error the local checks did not catch. fiscal_error.retry_scheduled is false, and the sweep deliberately leaves it alone.

Retrying a bad certificate every five minutes for thirty days does not fix the certificate. It fills the logs while the company remains unable to fiscalize, and it hides the alert that would have told someone.

fiscal_error.code names the fix. Common causes:

CodeFix
hr.certificate_oib_mismatchUpload the certificate belonging to this company’s OIB.
certificate_expiredUpload a current certificate.
company_missing_certificateUpload one.
upstream_rejectedCorrect the data the Tax Administration objected to.
upstream_signature_invalidContact support — this one is ours.

Once the cause is fixed, POST /v1/invoices/{id}/retry submits it again. failed is not terminal: a fixed invoice can still reach confirmed, as long as it is inside its 30-day window.

expired — terminal

The invoice passed deadline_at, which is occurred_at plus 30 days. The Tax Administration rejects a sale older than that outright and permanently, with no late-delivery path afterwards.

This is a compliance incident, and FiskHub treats it as one: the invoice is kept forever as a record, and the operator is alerted. It is not something an integration can recover from.

Escalation runs well before the deadline: a dashboard warning at day 21, an email at day 25, and a prominent alert at day 28.

Checking what actually happened

There are no webhooks. Read the invoice:

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

Or reconcile a day in one call:

Terminal window
curl -sS "https://api.fiskhub.com/v1/invoices?status=retrying&from=2026-08-27T00:00:00%2B02:00&to=2026-08-28T00:00:00%2B02:00" \
-H "Authorization: Bearer $FISKHUB_API_KEY"

A daily job that lists retrying and failed invoices is a reasonable amount of monitoring for an integrator to own. Anything more urgent than that is FiskHub’s job.

The error class behind the status

GET responses also carry last_error_class, which is why the sweep treats retrying and failed differently:

ClassCausesRetried in-requestSwept
transientNetwork errors, timeouts, upstream s006yesyes
validationRejected message, restrictive errorsnono
certificateUntrusted, expired or mismatched certificatenono

Only transient is ever retried automatically. It is persisted alongside the code so the sweep can filter on it without re-parsing error strings.