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
| Status | Meaning | Terminal | Retried automatically | Seen in a POST response |
|---|---|---|---|---|
pending | Persisted, not yet attempted. Exists only inside the request. | no | no | no |
confirmed | JIR received and its response signature verified. | yes | no | yes |
retrying | Transient failure. The sweep keeps trying until the deadline. | no | yes | yes |
failed | Validation or certificate error. Needs a human. | no | no | yes |
expired | Past the 30-day deadline. Permanently unfiscalizable. | yes | no | no |
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 dayspending 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:
| Code | Fix |
|---|---|
hr.certificate_oib_mismatch | Upload the certificate belonging to this company’s OIB. |
certificate_expired | Upload a current certificate. |
company_missing_certificate | Upload one. |
upstream_rejected | Correct the data the Tax Administration objected to. |
upstream_signature_invalid | Contact 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:
curl -sS "https://api.fiskhub.com/v1/invoices/inv_01J9Z4V2P8N5H1D7S9F3G6J0K2" \ -H "Authorization: Bearer $FISKHUB_API_KEY"Or reconcile a day in one call:
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:
| Class | Causes | Retried in-request | Swept |
|---|---|---|---|
transient | Network errors, timeouts, upstream s006 | yes | yes |
validation | Rejected message, restrictive errors | no | no |
certificate | Untrusted, expired or mismatched certificate | no | no |
Only transient is ever retried automatically. It is persisted alongside the code so the sweep
can filter on it without re-parsing error strings.