> ## 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.

# Changelog

> API contract updates to the OpenFiskal Fiscalization API.

<Update label="May 19, 2026" description="Fiscalization API" tags={["Addition"]}>
  **Cart-level discounts on SALE**

  `POST /operations` for `type: "sale"` now accepts an optional `cart_level_discounts[]` array — discounts applied to the order as a whole, not apportioned onto specific line items.

  * Each entry carries an optional `description` (free-text label, e.g. promo code) and a required positive `amount` decimal string.
  * `line_items[].total_amount` values must already reflect their post-discount totals — `cart_level_discounts[]` is documentation of the order-level reduction, not a re-pricing instruction.
  * The same array is returned on `sale` operation responses, with server-issued `id` per entry.
  * For German fiscalization: each cart-level discount emits a separate `Rabatt` business case in the DSFinV-K cashpoint-closing archive, slotted between the line-item `Umsatz` lines and any gift-card `MehrzweckgutscheinEinloesung` lines. Descriptions over 255 characters are trimmed for the DSFinV-K text field; the original is preserved in the operation response.
</Update>

<Update label="May 16, 2026" description="Fiscalization API" tags={["Addition"]}>
  **Gift card redemptions**

  `payments[]` on `POST /operations/{id}/complete` now accepts an optional `gift_card_id` field — the integrator-supplied identifier of the voucher being redeemed.

  * `gift_card_id` is **required** when `method: "gift_card"`. Sending a `gift_card` payment without it (or with empty/whitespace) returns `422 gift_card_id_required`.
  * The same field is returned on payments in the operation response.
  * For German fiscalization: each `gift_card` payment on a `sale` now emits a `MehrzweckgutscheinEinloesung` line in the DSFinV-K cashpoint-closing archive, carrying the redeemed amount (negative) and `voucher_id`. Per §3 Abs. 15 Satz 2 UStG, the redemption is anti-payment rather than revenue, so the gift-card leg is no longer reported under `payment_types`; the SALE's `full_amount_incl_vat` is reduced by the redeemed total and a balancing entry is added to `amounts_per_vat_id` at the non-taxable bucket. No integrator action needed beyond setting `gift_card_id`.
</Update>

<Update label="May 12, 2026" description="Fiscalization API" tags={["Addition"]}>
  **Gift card line items**

  Line items on `POST /operations` (for `sale`, `return`, `exchange`) now accept an optional `type` field with values `item` (default) or `gift_card`.

  * `gift_card` lines represent multi-purpose voucher sales (Mehrzweckgutschein). All taxes on a `gift_card` line must carry `rate: "0"` and `tax_amount: "0"` — non-zero rates are rejected with `422 gift_card_tax_rate_must_be_zero`. VAT on multi-purpose vouchers is deferred to redemption.
  * An optional `gift_card_id` field carries the integrator-supplied gift card identifier.
  * The same `type` and `gift_card_id` fields are returned on line items in operation responses.
  * For German fiscalization: `gift_card` lines emit `MehrzweckgutscheinKauf` (instead of `Umsatz`) in the DSFinV-K cashpoint-closing archive. No integrator action needed beyond setting `type`.
</Update>

<Update label="May 7, 2026" description="Fiscalization API" tags={["Addition"]}>
  **`tax_amount` consistency check**

  Each `line_items[].taxes[].tax_amount` is now checked against `total_amount × rate_i / (1 + Σ rate_j)` at 8 dp; mismatches return `422 tax_amount_precision_invalid`. Submit `tax_amount` rounded to 8 dp. See [Decimal precision](/decimal-precision).
</Update>

<Update label="May 3, 2026" description="Fiscalization API" tags={["Breaking", "Addition"]}>
  **Line items now require `sku_identifier`**

  Each entry in `line_items[]` on `POST /operations` (for `sale`, `return`, `exchange`) now requires a `sku_identifier` field — the merchant's stable variant identifier (e.g. `"TSHIRT-BLUE-M"`).

  Identifies a unique number used to maintain and manage the item, product, or merchandise category in the company's systems. Example: A Tshirt has one sku for each size + color combination.

  **Breaking — `sku_identifier` is mandatory, as it's required for regulatory exports**

  The same field is returned on every line item in the operation response.
</Update>

<Update label="May 2, 2026" description="Fiscalization API" tags={["Breaking", "Addition"]}>
  **Session close split into count + close**

  End-of-shift is now a two-step flow: the merchant counts the drawer first (`session_cash_count`), then seals the session (`session_close`). The two events attest to different fiscal facts and produce up to two TSE signatures from one end-of-shift sequence.

  * `session_cash_count` — new operation type. Carries `counted_closing_amount` (and optional `discrepancy_note`). Server returns `expected_closing_amount` and signed `closing_cashcount_diff` (counted − expected). The variance is fiscally attested.
  * `session_close` — slimmed. Now carries only the optional closing pull/add (`closing_adjustment_amount` signed: negative = cash out, positive = cash in; plus `closing_adjustment_note`). Fiscally signed only when the pull is non-zero; null/zero is a state-transition close with no Fiskaly signature.

  **Breaking — `session_close` no longer accepts `counted_closing_amount` or `discrepancy_note`.** Those fields moved to `session_cash_count`. Bodies that include them on a close return `400`.

  **Breaking — `session_close` requires a preceding `session_cash_count`.** A close on an un-counted session returns `409 session_not_counted`.

  **Breaking — once counted, the session is frozen.** Posting `sale` / `return` / `exchange` / `session_cash_adjustment` / a second `session_cash_count` against a counted session returns `409 session_already_counted`. Only `session_close` is accepted from the counted state.

  **Breaking — `SessionCloseOperation` response shape narrowed.** `counted_closing_amount` and `discrepancy_note` are no longer on the close response (they're on the `SessionCashCountOperation` response).

  **New session lifecycle state.** Sessions transition `open → counted → closed` (was `open → closed`). The `counted` state is short-lived in the happy path — most integrators will POST count then close back-to-back.

  Update integrator end-of-shift flows to count first, then close, before your next release. See [Sessions](/sessions) for the walkthrough.
</Update>

<Update label="April 30, 2026" description="Fiscalization API" tags={["Breaking", "Addition"]}>
  **Register sessions: new operation types + session-bound POS operations**

  Three new operation types model the cash-drawer shift on a fiscalized register:

  * `session_open` — start a shift with a counted opening float
  * `session_cash_adjustment` — record mid-shift cash movement (drop, pay-in, correction)
  * `session_close` — end the shift with a merchant-counted closing balance

  All three flow through `POST /operations` (no separate sessions endpoint) and carry the same `resource_version` / `ETag` mechanics as goods-movement operations. They are born `status: "completed"` — there is no `POST .../complete` step for session events.

  **Breaking — POS goods-movement now requires an open session.** Sending `POST /operations` with `source: "POS"` on a register that has no open session is rejected with `409 no_open_session`. Open the register's session with `session_open` before posting POS sales, returns, or exchanges. At most one open session per register; opening a second while one is open returns `409 session_invalid_state`.

  **New field — `session_id` on operation responses.** Every operation response now carries `session_id` on the envelope. For POS goods-movement, it's the session the operation was rung up during. For session-event variants, it's the session being opened/adjusted/closed. For `ONLINE` goods-movement (no register), it's `null`.

  **Breaking — `GET /operations/{id}` is now a discriminated union on `type`.** Six variants: `SaleOperation`, `ReturnOperation`, `ExchangeOperation`, `SessionOpenOperation`, `SessionCashAdjustmentOperation`, `SessionCloseOperation`. Each variant carries only the fields meaningful to its type — `SESSION_*` responses no longer include zero-padded `pretax_amount` / `total_amount` / `line_items` / `payments` / `fiscal_information`, and goods-movement responses no longer include `null` `session_*_details` placeholders. `SALE` responses also drop the always-null `related_operation_id` and `external_related_operation`.

  See [Sessions](/sessions) for the full walkthrough.

  Update integrators that issue POS operations to bracket each shift with `session_open` / `session_close` before your next release.
</Update>

<Update label="April 30, 2026" description="Fiscalization API" tags={["Breaking"]}>
  **Merchant `tax_id` removed; Italian `fiscal_identity` reshaped**

  The top-level `Merchant.tax_id` field has been removed from create, update, and response shapes. Tax identifiers now live exclusively on `fiscal_identity`, where they can vary by country.

  The Italian `fiscal_identity` object (`country_code: "ITA"`) has been replaced:

  * Removed (both optional): `vat_id`, `codice_fiscale`.
  * Added (all required): `tax_number` (Codice Fiscale — 11 numeric digits or 16 alphanumeric characters), `vat_number` (Partita IVA — 11 numeric digits), `fisconline_user` (16 alphanumeric characters; the Codice Fiscale of the person delegated to access the Agenzia delle Entrate portal), `fisconline_password`, `fisconline_pin`.
  * `legal_entity_type` (`COMPANY | INDIVIDUAL`) is unchanged.

  The German (`DEU`) and Austrian (`AUT`) `fiscal_identity` shapes are unchanged.

  Update integrator code that creates or updates Italian merchants, and stop sending the top-level `tax_id` field for all merchants, before your next release.
</Update>

<Update label="April 29, 2026" description="Fiscalization API" tags={["Addition"]}>
  **New endpoints: `POST /exports`, `GET /exports/{exportId}`**

  Two new endpoints expose asynchronous fiscal export jobs.

  `POST /exports` enqueues a job and returns `202 Accepted` with `{ id, type, status: "pending", register_id?, from, to, created_at }`. Required body fields: `type` (`dsfinvk` is the first available type), `from` and `to` (ISO 8601 date-times). Optional `register_id` scopes the export to a single register; when omitted, the export covers all eligible registers under the merchant for the matching fiscal regime.

  `GET /exports/{exportId}` returns the job. Once `status` is `completed`, `download_url` is a signed URL to fetch the artifact. When `status` is `failed`, `error.message` (and optional `error.details`) describes why.

  Both endpoints require the `X-OpenFiskal-Merchant` header.
</Update>

<Update label="April 29, 2026" description="Fiscalization API" tags={["Addition"]}>
  **`external_related_operation` accepted on returns/exchanges**

  `POST /operations` with `type: "return"` or `type: "exchange"` now accepts a new optional field, `external_related_operation`, for cases where the original sale was never ingested into OpenFiskal (typically during platform migration before backfill).

  ```json theme={null}
  {
    "type": "return",
    "external_related_operation": {
      "description": "Return against order from external platform.",
      "external_operation_id": "shopify-order-4711"
    }
  }
  ```

  * Mutually exclusive with `related_operation_id` — exactly one must be set on a return or exchange.
  * Both fields on the nested object are required strings.
  * The same field is echoed on the operation response.

  Use `related_operation_id` when the original sale exists in OpenFiskal; use `external_related_operation` otherwise.
</Update>

<Update label="April 23, 2026" description="Fiscalization API" tags={["Breaking"]}>
  **Sign convention enforced on `total_amount` and `line_items[].total_amount`**

  `POST /operations` now rejects payloads whose amount signs do not match `Operation.type`:

  * `sale`: `total_amount` and every `line_items[].total_amount` must be `>= 0`.
  * `return`: `total_amount` and every `line_items[].total_amount` must be `<= 0`.
  * `exchange`: no sign constraint (an exchange may net positive, negative, or zero).

  Zero is permitted on both `sale` (freebie) and `return` (zero-value return). Violations return `422 Unprocessable Entity`.

  Sign is the only on-wire signal Fiskaly's TSE has to distinguish a sale from a return signature, so OpenFiskal enforces it before signing.

  Update return flows that previously sent positive amounts on `type: "return"` to send non-positive amounts before your next release.
</Update>

<Update label="April 23, 2026" description="Fiscalization API" tags={["Breaking"]}>
  **`fiscal_identities[].country` renamed to `country_code`; DE fields now required**

  The discriminator field on every `fiscal_identity` entry has been renamed from `country` to `country_code`. This applies to all three country variants (`DEU`, `AUT`, `ITA`) on both the create/update payload and the merchant response. Payloads that send `country` will reject.

  For `DEU` fiscal identities, `tax_number` (Steuernummer) and `vat_id` (USt-IdNr) are now both **required**. Previously both were optional. `AUT` and `ITA` requirements are unchanged.

  Update integrator code that creates or parses merchant `fiscal_identities` before your next release.
</Update>

<Update label="April 23, 2026" description="Fiscalization API" tags={["Addition"]}>
  **OpenAPI JSON now served at `/openapi-json`**

  The current production OpenAPI spec is now served at:

  * Production: `https://api.openfiskal.com/openapi-json`
  * Sandbox: `https://sandbox.api.openfiskal.com/openapi-json`

  The previous Swagger-default path `/docs-json` is preserved as a `301` redirect to the new path. Import the new URL into Postman, Insomnia, or an OpenAPI code generator to scaffold a client.

  The OpenAPI spec always matches the live API implementation.
</Update>

<Update label="April 23, 2026" description="Fiscalization API" tags={["Breaking"]}>
  **`Operation.status` enum corrected; `412` payload includes `expected_resource_version`**

  The `Operation.status` enum has been corrected from `'open' | 'completed' | 'cancelled'` to `'open' | 'completed' | 'voided'`. Operations that you `POST /operations/{id}/void` resolve to `status: "voided"`, not `cancelled`. There were never any operations with `status: "cancelled"` in production — the OpenAPI value was wrong.

  `412 Precondition Failed` error bodies now include `details.expected_resource_version` alongside `details.current_resource_version`. The `message` field is the literal string `"Resource version mismatch."` (previously `"The supplied If-Match value is stale."`).

  Update `Operation.status` parsers to accept `voided`, and update any UI that surfaces the `precondition_failed` error message string.
</Update>

<Update label="April 23, 2026" description="Fiscalization API" tags={["Breaking"]}>
  **Operation type `refund` renamed to `return`**

  The `Operation.type` enum now uses `return` instead of `refund` for operations where goods come back from the customer. The valid values are `sale | return | exchange`.

  OpenFiskal now distinguishes goods movement from money movement: *operations* model goods (sale/return/exchange), *refund* is reserved for money movement (a negative payment transaction on an operation) and is not an operation type. This unlocks flows the old terminology could not express cleanly — return with store credit (goods back, no money), goodwill refund (money back, no goods), or partial refunds on kept items.

  * `POST /operations` with `type: "refund"` will now reject. Use `type: "return"`.
  * Response shapes now return `type: "return"` for what was previously `type: "refund"`.
  * `Payment.status` enum is unchanged — `refunded` is still the correct status for a payment that has been reversed, because it describes money movement.

  Update integrator code that creates or parses return/refund operations before your next release.
</Update>

<Update label="April 23, 2026" description="Fiscalization API" tags={["Breaking"]}>
  **KassenSichV schema changes**

  * `FiscalInformationKassenSichVVerification`: renamed `tse_serial` to `tss_serial_number` and `client_id` to `pos_client_serial_number`. Both remain required.
  * `FiscalInformationKassenSichVEndEvent`: added required `public_key` (string).
  * `FiscalInformationKassenSichVStartEvent`: removed `transaction_counter` and `signature`. `signed_at` is now the only required field.

  Update integrations that submit or parse KassenSichV fiscal information before your next release.
</Update>

<Update label="April 23, 2026" description="Fiscalization API" tags={["Clarification"]}>
  **Country code casing**

  Clarified that the `country` field on `Address` and on the legal entity schema must be an ISO 3166-1 alpha-3 code in uppercase (for example, `DEU`). No behavior change; existing uppercase values continue to validate.
</Update>

<Update label="April 19, 2026" description="Fiscalization API" tags={["Addition"]}>
  **New endpoint: `POST /registers/{registerId}/decommission`**

  Permanently retires a fiscalized register. The endpoint calls the underlying TSE provider to decommission the register's fiscal components and returns the updated register with `decommissionedAt` populated. Currently `DE`/KassenSichV only; AT/IT will follow as those regimes ship publicly.

  Conflicts return `409 decommission_conflict`:

  * Register has not been fiscalized.
  * Register has already been decommissioned.

  A decommissioned register cannot be re-fiscalized — create a new register for that location instead.
</Update>
