Documentation Index
Fetch the complete documentation index at: https://docs.openfiskal.com/llms.txt
Use this file to discover all available pages before exploring further.
Every error response uses the same envelope. Switch on code (machine-readable, stable), not on message (human-readable, may change).
Response shape
{
"code": "precondition_failed",
"message": "Resource version mismatch. Expected 1, current is 2.",
"retryable": true
}
| Field | Type | Notes |
|---|
code | string | Stable machine-readable identifier. See the catalog below. |
message | string | Short human-readable label. Safe to log, not locale-stable, not a stable contract. |
retryable | boolean | Whether retrying the same request can succeed. 5xx, 429, and 412 are retryable. |
details | object | Reserved for structured context. Not currently populated by any handler — variable state (e.g. expected vs. current resource versions) is interpolated into message today. Treat the field as optional and absent in current responses. |
The trace identifier is returned only in the X-Request-Id response header (e.g. X-Request-Id: req_a1b2c3d4e5f60718293a). It is not embedded in the error body. Persist it in your client logs and attach it to support cases.
Retry guidance
| Class | Retry? | How |
|---|
5xx, rate_limit_exceeded, precondition_failed | Yes | Resend the request. For precondition_failed, re-read the resource to refresh ETag first. For rate_limit_exceeded, honor Retry-After. |
All other 4xx | No | Fix the request before resending. |
The retryable field on each response reflects this rule: it is true for any 5xx, 429, or 412, and false otherwise.
Standard error codes
These codes map directly to HTTP status codes and apply to every endpoint across the Fiscalization, Reporting, and Hosted Receipts APIs unless noted.
| HTTP | code | Retryable | When |
|---|
| 400 | invalid_request | No | Request body, query, or path is malformed or fails schema validation. Includes missing required fields, wrong types, decimal vs. integer mismatches, invalid ISO 3166-1 alpha-3 country codes (DEU not DE), unknown discriminator values (e.g. unknown operation type), and a malformed If-Match header value (must be a quoted positive integer, e.g. "1"). |
| 401 | unauthorized | No | Missing or malformed Authorization header, invalid API key, or API key environment mismatch (sandbox key against production or vice versa). |
| 403 | forbidden | No | Authenticated caller is not allowed to access the requested tenant, country, or merchant. Most commonly: API key country does not match the merchant or location country. |
| 404 | not_found | No | Resource does not exist or is not visible to the caller. |
| 409 | conflict | No | Generic resource conflict. Specific 409 subtypes are listed below — code distinguishes the cause. |
| 412 | precondition_failed | Yes | Stale If-Match on a mutation. The expected and current resource versions appear in message (e.g. "Resource version mismatch. Expected 1, current is 2."). Re-read the resource and retry with the current ETag. |
| 422 | validation_error | No | Request shape is valid but a field value is semantically wrong (amount sign mismatch, missing register reference, invalid timestamp range, etc.). See Validation errors for the specific conditions. |
| 428 | precondition_required | No | If-Match header is missing on a mutation that requires it (PATCH /operations/{id}/complete, PATCH /operations/{id}/void). |
| 429 | rate_limit_exceeded | Yes | Per-API-key rate budget exceeded. Honor Retry-After and reduce concurrency. |
| 500 | internal_error | Yes | Unhandled server error. Retry the request; escalate with X-Request-Id if sustained. Status-501 paths (see Not-yet-implemented endpoints) currently surface with this code as well. |
Conflict (409) variants
All 409 responses share the envelope. The code field distinguishes the cause.
code | Where | When |
|---|
location_has_registers | DELETE /locations/{id} | Location still has registers attached. Delete or reassign the registers first. |
register_has_dependencies | DELETE /registers/{id} | Register has operations recorded against it, or the register is fiscalized. Decommission first if applicable; otherwise the register cannot be removed. |
register_already_fiscalized | POST /registers/{id}/fiscalize | Register is already fiscalized, or a fiscalization is in progress. |
register_invalid_fiscal_state | POST /registers/{id}/decommission, completion paths | Register is decommissioned, not yet fiscalized, or otherwise not in a state that supports the requested action. |
register_no_open_session | POS operation endpoints | Register has no open session. Open a session first via POST /operations with type: SESSION_OPEN. |
session_invalid_state | Session lifecycle endpoints | Session-level state violation: a session is already open on the register, session counts or amounts are invalid, or the session cannot be closed in its current state. |
operation_invalid_state | PATCH /operations/{id}/complete, PATCH /operations/{id}/void | Operation is not in OPEN state. You cannot complete an already-completed or voided operation, and you cannot void what is not open. |
Validation errors (422)
422 responses currently emit code: "validation_error" for every case. The message field describes the specific violation.
Operation shape
| Condition | Operation types |
|---|
ONLINE operations must not specify a register_id. | SALE, RETURN, EXCHANGE |
POS operations must specify a register_id. | SALE, RETURN, EXCHANGE |
SALE operations must not specify external_related_operation or related_operation_id. | SALE |
RETURN and EXCHANGE operations must specify exactly one of related_operation_id or external_related_operation. | RETURN, EXCHANGE |
Amount and payment validation
| Condition | Operation types |
|---|
SALE total_amount must be ≥ 0; RETURN total_amount must be ≤ 0. | SALE, RETURN |
Each line_items[].total_amount must follow the same sign rule as the parent operation. | SALE, RETURN |
Each payments[].amount must follow the same sign rule as the parent operation. | All goods-movement types |
Sum of payments[].amount must match the operation total_amount. | All goods-movement types on complete |
Fiscalization preconditions
| Condition | Endpoint |
|---|
| Register is not fiscalized. Complete fiscalization before completing operations. | PATCH /operations/{id}/complete |
| Register is missing active TSS or Client. Re-fiscalize the register. | PATCH /operations/{id}/complete |
| Operation has no goods-movement details; cannot complete. | PATCH /operations/{id}/complete |
POS operation is missing a register reference. | PATCH /operations/{id}/complete |
Merchant is missing a German fiscal identity (DEU). | POST /registers/{id}/fiscalize |
Fiscalization is only supported for locations in Germany (DEU). | POST /registers/{id}/fiscalize |
| Decommission is only supported for KassenSichV registers. | POST /registers/{id}/decommission |
| Register has no active TSS or client to decommission. | POST /registers/{id}/decommission |
| Active TSS is missing an admin PIN; cannot authenticate to decommission. | POST /registers/{id}/decommission |
| TSE signing failed (phase 1) — TSS unreachable or rejected the request before the operation reached the device. | PATCH /operations/{id}/complete |
| TSE signing failed (phase 2) — TSS rejected the operation after start. The operation is left in a recoverable state. | PATCH /operations/{id}/complete |
Reporting API
| Condition | Endpoint |
|---|
from and to must be valid ISO-8601 timestamps. | Reporting export endpoints (returns 400 invalid_request). |
from must be strictly before to. | Reporting export endpoints. |
Not-yet-implemented endpoints
Two paths return HTTP 501 Not Implemented today. The exception filter has no 501 mapping, so the body surfaces with code: "internal_error" and retryable: true even though the status itself signals the cause:
| HTTP | Where | Why |
|---|
| 501 | POST /operations with type: SESSION_CASH_COUNT | The session cash-count operation type is reserved in the schema but not yet handled. |
| 501 | Reporting export endpoints with an unsupported export type | No exporter is registered for the requested type (e.g. DSFinV-K archive compilation). |
Treat the 501 status — not the body code — as the signal here. This will tighten in a future filter update.
Reserved codes (not yet emitted)
The OpenAPI contract advertises these codes on relevant endpoints, but the runtime does not emit them yet. They are kept here so client switch statements can include them ahead of enforcement landing.
code | HTTP | Status |
|---|
idempotency_key_conflict | 409 | Documented on every mutating operation endpoint. Idempotency-Key replay is not enforced server-side today; reusing a key currently re-runs the request. Treat the documented behavior as forward-looking. |
regime_validation_failed | 422 | Documented for regime-specific rule failures (KassenSichV / RKSV / Fattura). Current handlers throw UnprocessableEntityException without a custom code, so these responses surface as validation_error. |
Idempotency and concurrency
Idempotency keys, retention windows, and concurrency rules (If-Match / ETag) are documented on the Limits page. The errors those rules emit (precondition_required, precondition_failed) are in the standard table above; idempotency_key_conflict is forward-looking — see Reserved codes.
Request tracing
Every response includes an X-Request-Id header. Persist it in your client logs and attach it to every support case — it is the fastest path to a root-cause answer.
Next steps