Documentation Index
Fetch the complete documentation index at: https://dev.openfiskal.com/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Every fiscal event that occurs at a register is reported to OpenFiskal as an operation. Your backend sends the event, OpenFiskal applies the fiscal rules for the target regime, and the response carries the fiscal payload you need for receipts, exports, and auditability.Lifecycle
Each operation follows the same public lifecycle:open— the operation has been created but not finalizedcompleted— the fiscal event is final and immutablevoided— the operation was voided before completion
status in request payloads. They complete operations through PATCH /operations/{operationId}/complete and void them through PATCH /operations/{operationId}/void.
Operation types
| Type | When to use it |
|---|---|
sale | Standard sale flow |
return | Return against an earlier sale (set related_operation_id or external_related_operation) |
exchange | Return plus replacement in a single operation (set related_operation_id or external_related_operation) |
return and exchange must reference the original sale. Set exactly one of:
related_operation_id— when the original sale exists in OpenFiskal as an operation.external_related_operation— when the original sale lives outside OpenFiskal (typical during platform migration). The object carriesdescription(free-text identifying the upstream sale) andexternal_operation_id(the integrator-side identifier, e.g. the legacy POS or Shopify order ID).
422 unprocessable_entity. sale operations must not set either.
Sign convention
Amounts carry their sign end-to-end — the API does not re-derive sale-vs-return from the operation type. Send amounts with the sign that reflects what actually moves at the register.| Type | total_amount | line_items[].total_amount |
|---|---|---|
sale | ≥ 0 | ≥ 0 |
return | ≤ 0 | ≤ 0 |
exchange | any sign | any sign (mixed within one operation is allowed) |
422 unprocessable_entity. The operation-level arithmetic rule still holds: pretax_amount + tax_amount + tip_amount = total_amount. When a return is signed, the pretax_amount and tax_amount are negative too.
This sign is the only on-wire signal Fiskaly’s TSE has for sale-vs-return, so making it explicit in the request payload keeps intent, fiscal signature, and downstream exports aligned.
Start an operation
Create operations withPOST /operations. The request is flat — source (POS or ONLINE), type (sale / return / exchange), currency, all four amount fields (pretax_amount, tax_amount, tip_amount, total_amount) as decimal strings, and line_items. register_id is required for POS, must be omitted for ONLINE. The response returns the operation resource, a resource_version, and an ETag header.
Complete an operation
UsePATCH /operations/{operationId}/complete when you want OpenFiskal to produce the final fiscal document and receipt state. Completion accepts a typed payments array and returns fiscal_information for regime-specific fiscal payloads. Send the latest ETag in If-Match.
Completion is a fiscal event. Use an idempotency key on every completion request.
Canonical examples
Standard sale
Split tender (on completion)
Return against prior receipt
All amounts are negative: the customer is getting €12 back, goods are flowing back into stock, VAT is being reversed. The completion payload mirrors this —payment.amount is negative too (the merchant is paying the customer), and status stays captured so the payment counts toward the operation total.
Return against an external sale
Useexternal_related_operation when the original sale was rung up before the merchant moved onto OpenFiskal — typically a platform migration. The body is otherwise identical to a normal return; swap related_operation_id for the nested object.
Exchanges
An exchange is a goods swap — the customer returns one or more items and receives replacements in the same transaction. OpenFiskal signs an exchange as a single fiscal event against the register.The mental model
Send the net delta — the amounts that actually move at the register after the returned items are offset against the replacements. The sign of each amount drives the fiscal signature:total_amountpositive — customer pays the differencetotal_amountnegative — merchant refunds the differencetotal_amountzero — even swap, no money moves
line_items array. Use negative unit_price and total_amount for returned items, positive for replacements, and set each item’s taxes to match. Keep this sign convention on the line items regardless of whether the net delta is positive or negative — the net delta only changes the operation-level pretax_amount, tax_amount, and total_amount. OpenFiskal aggregates per VAT rate when it builds the signing payload, so the signed amounts reflect the net movement per rate.
The operation amounts must remain arithmetically consistent: pretax_amount + tax_amount + tip_amount = total_amount. When the net delta is negative, all three of pretax_amount, tax_amount, and total_amount are negative.
An exchange must reference the original sale. Set related_operation_id when the prior sale lives in OpenFiskal, or external_related_operation when it lives in a legacy system (see Return against an external sale).
Customer pays the difference
Customer returns a €100 item and picks up a €150 item. Net delta: customer pays €50.Merchant refunds the difference
Customer returns a €150 item and picks up a €100 item. Net delta: merchant refunds €10 — sototal_amount, pretax_amount, and tax_amount are negative, but the line items keep the standard sign convention (returned item negative, replacement positive).
status: "captured" — the payment still counts toward the operation total under the split-tender sum rule. The negative sign encodes the refund direction.
Cross-VAT exchanges
If the returned and replacement items sit at different VAT rates — for example, a 19% item returned and a 0% item taken in its place — send each line item with its own rate. The signed fiscal payload will carry a negative amount in one VAT bucket and a positive amount in the other. This is the correct representation and is preserved in the KassenSichV QR code and DSFinV-K export.Even swaps
For a zero-net exchange, sendtotal_amount: "0.00" and a single payment with amount: "0.00". The API requires at least one payment entry on completion.
Void an open operation
UsePATCH /operations/{operationId}/void only while the operation is still open. The endpoint is named /void, not /cancel.
reason values: void_before_completion, customer_abandoned_checkout, operator_cancelled, payment_failed.
Completed operations remain completed. Voiding is not a replacement for return or reversal — use type: return for that.
Concurrency model
Use conditional writes with server-issued versions:- create or read the operation
- persist the returned
ETag - treat
resource_versionin the body andETagin the header as the same server-issued version - send that value in
If-Matchon the next mutation (/completeor/void) - replace your stored version after every successful mutation
- re-read on
412 precondition_failed - a missing
If-Matchreturns428 precondition_required