Skip to main content

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.

This guide walks the v1 integration contract for Germany. You create a deu API key, onboard a merchant with a German fiscal_identity, register a location and terminal, fiscalize the register under KassenSichV, start an operation, and complete it with the typed payment contract.
Examples use https://sandbox.api.openfiskal.com/v1. Replace with https://api.openfiskal.com/v1 when you go live.

Prerequisites

  • An OpenFiskal tenant
  • A tenant-scoped, country-scoped API key (of_test_deu_โ€ฆ or of_live_deu_โ€ฆ)
  • curl or an HTTP client
  • A backend service / database to store your API key, entity IDs and ETags

Authentication model

Use the standard bearer header on every request. The key encodes the environment and country โ€” of_test_deu_โ€ฆ for the German sandbox, of_live_deu_โ€ฆ for German production. A deu key rejects payloads for any other country.
-H "Authorization: Bearer of_live_deu_abc123..."
Every merchant-scoped request must also include:
-H "X-OpenFiskal-Merchant: merchant_01HXYZ"
This header value is the merchant ID returned by POST /merchants, not your API key.

Create resources

Create these resources in order. The register cannot fiscalize until the merchant has a DEU fiscal_identity.
1

Create your API key

Start in the OpenFiskal tenant dashboard. Create a deu key. Store it in your secrets manager. Format: of_{env}_deu_{random}, e.g. of_test_deu_abcdefgh12345678.
2

Create a merchant

country_code and every address.country_code must be DEU (ISO 3166-1 alpha-3). The German fiscal_identity requires both tax_number (Steuernummer) and vat_id (USt-IdNr).
curl -X POST https://sandbox.api.openfiskal.com/v1/merchants \
  -H "Authorization: Bearer of_test_deu_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "legal_name": "Mustermann GmbH",
    "country_code": "DEU",
    "address": {
      "line1": "FriedrichstraรŸe 42",
      "city": "Berlin",
      "postal_code": "10117",
      "country_code": "DEU"
    },
    "fiscal_identities": [
      {
        "country_code": "DEU",
        "tax_number": "21/815/08150",
        "vat_id": "DE123456789"
      }
    ]
  }'
The returned id becomes the X-OpenFiskal-Merchant header on every merchant-scoped request that follows.
3

Create a location

A location is a physical point of sale. timezone is a required IANA zone string โ€” KassenSichV embeds local time into signed records.
curl -X POST https://sandbox.api.openfiskal.com/v1/locations \
  -H "Authorization: Bearer of_test_deu_abc123..." \
  -H "X-OpenFiskal-Merchant: merchant_01HXYZ" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Berlin Mitte",
    "address": {
      "line1": "FriedrichstraรŸe 42",
      "city": "Berlin",
      "postal_code": "10117",
      "country_code": "DEU"
    },
    "timezone": "Europe/Berlin"
  }'
4

Create a register

A register is the logical checkout or terminal that creates operations.
curl -X POST https://sandbox.api.openfiskal.com/v1/registers \
  -H "Authorization: Bearer of_test_deu_abc123..." \
  -H "X-OpenFiskal-Merchant: merchant_01HXYZ" \
  -H "Content-Type: application/json" \
  -d '{
    "location_id": "loc_01HXYZ",
    "name": "Register 1",
    "external_id": "pos-register-1"
  }'
5

Fiscalize the register

Before the register can accept operations, call POST /registers/{id}/fiscalize. This provisions the KassenSichV components โ€” the TSS and POS client โ€” using the merchantโ€™s German fiscal_identity and the locationโ€™s country. The response is 202 Accepted; poll GET /registers/{id} until fiscalizedAt is populated.
curl -X POST https://sandbox.api.openfiskal.com/v1/registers/reg_01HXYZ/fiscalize \
  -H "Authorization: Bearer of_test_deu_abc123..." \
  -H "X-OpenFiskal-Merchant: merchant_01HXYZ"
6

Verify your saved IDs

Persist the returned merchant, location, and register IDs. You will use them for every later operation request.

Perform a fiscalized sale

Once your register is fiscalized, you can create and complete sale operations.
1

Start an operation

POST /operations is a single-shot create. All monetary fields are decimal strings, not integers. pretax_amount + tax_amount + tip_amount must equal total_amount. line_items is required with at least one entry.
curl -X POST https://sandbox.api.openfiskal.com/v1/operations \
  -H "Authorization: Bearer of_test_deu_abc123..." \
  -H "X-OpenFiskal-Merchant: merchant_01HXYZ" \
  -H "Idempotency-Key: op-order-1001-start" \
  -H "Content-Type: application/json" \
  -d '{
    "register_id": "reg_01HXYZ",
    "source": "POS",
    "type": "sale",
    "currency": "EUR",
    "external_id": "order-1001",
    "pretax_amount": "39.72",
    "tax_amount": "2.78",
    "tip_amount": "0.00",
    "total_amount": "42.50",
    "line_items": [
      {
        "title": "Lunch menu",
        "quantity": 1,
        "unit_price": "42.50",
        "total_amount": "42.50",
        "taxes": [
          { "name": "MwSt. 7%", "rate": "0.07", "tax_amount": "2.78" }
        ]
      }
    ]
  }'
Always send line_items[].taxes[]. Omitting it prevents correct multi-VAT breakdown on the receipt and on the DSFinV-K vat.csv. One tax entry per VAT rate on the line.
Response:
HTTP/1.1 201 Created
ETag: "1"
X-Request-Id: req_01JSTART
{
  "id": "op_01HXYZ",
  "resource_version": 1,
  "merchant_id": "merchant_01HXYZ",
  "location_id": "loc_01HXYZ",
  "register_id": "reg_01HXYZ",
  "source": "POS",
  "external_id": "order-1001",
  "status": "open",
  "type": "sale",
  "currency": "EUR",
  "pretax_amount": "39.72",
  "tax_amount": "2.78",
  "tip_amount": "0.00",
  "total_amount": "42.50",
  "line_items": [
    {
      "id": "li_01HXYZ",
      "title": "Lunch menu",
      "quantity": 1,
      "unit_price": "42.50",
      "total_amount": "42.50",
      "taxes": [
        { "id": "lit_01HXYZ", "name": "MwSt. 7%", "rate": "0.07", "tax_amount": "2.78" }
      ]
    }
  ],
  "payments": [],
  "created_at": "2026-02-26T12:00:00Z",
  "updated_at": "2026-02-26T12:00:00Z"
}
2

Complete the operation

Completion includes typed payment legs. Use one entry per tender leg and include processor references where available. Send the latest ETag in If-Match. The completed operation returns fiscal_information with the KassenSichV regime.
The current API has no generic PATCH /operations/{id} endpoint. Build the full line-item and amount set before calling POST /operations; you cannot amend the body after creation. Open operations only accept /complete and /void.
curl -X PATCH https://sandbox.api.openfiskal.com/v1/operations/op_01HXYZ/complete \
  -H "Authorization: Bearer of_test_deu_abc123..." \
  -H "X-OpenFiskal-Merchant: merchant_01HXYZ" \
  -H "Idempotency-Key: op-order-1001-complete" \
  -H 'If-Match: "1"' \
  -H "Content-Type: application/json" \
  -d '{
    "payments": [
      {
        "payment_id": "pay_1001_card",
        "method": "card",
        "amount": "42.50",
        "currency": "EUR",
        "status": "captured",
        "processor": "sumup",
        "card_brand": "visa",
        "processor_reference": "ch_123",
        "processed_at": "2026-02-26T12:05:00Z"
      }
    ]
  }'
Response:
HTTP/1.1 200 OK
ETag: "2"
X-Request-Id: req_01JCOMPLETE
{
  "id": "op_01HXYZ",
  "resource_version": 2,
  "status": "completed",
  "payments": [
    {
      "payment_id": "pay_1001_card",
      "method": "card",
      "amount": "42.50",
      "currency": "EUR",
      "status": "captured",
      "processor": "sumup",
      "processor_reference": "ch_123",
      "card_brand": "visa",
      "processed_at": "2026-02-26T12:05:00Z"
    }
  ],
  "fiscal_information": {
    "regime": "KassenSichV",
    "document_number": "2026-000123",
    "document_type": "Kassenbeleg",
    "tss_serial_number": "TSE0123456789ABCDEF",
    "pos_client_serial_number": "Kasse1",
    "signature_algorithm": "ecdsa-plain-SHA384",
    "time_format": "utcTime",
    "start_event": {
      "signed_at": "2026-02-25T19:02:11Z"
    },
    "end_event": {
      "signed_at": "2026-02-25T20:14:55Z",
      "transaction_counter": 4822,
      "signature": "base64encodedEndSignature==",
      "public_key": "base64encodedPublicKey==",
      "process_type": "Kassenbeleg-V1",
      "process_data": "Beleg^42.50_0.00_0.00_0.00_0.00^42.50:Bar"
    },
    "verification": {
      "qr_data": "V0;Kasse1;ecdsa-plain-SHA384;2026-02-25T19:02:11;2026-02-25T20:14:55;42.50;4822;base64encodedEndSignature=="
    }
  }
}

Void an open operation

If the sale is abandoned before completion, void the open operation:
curl -X PATCH https://sandbox.api.openfiskal.com/v1/operations/op_01HXYZ/void \
  -H "Authorization: Bearer of_test_deu_abc123..." \
  -H "X-OpenFiskal-Merchant: merchant_01HXYZ" \
  -H "Idempotency-Key: op-order-1001-void" \
  -H 'If-Match: "1"' \
  -H "Content-Type: application/json" \
  -d '{ "reason": "void_before_completion" }'
Completed operations cannot be voided โ€” use a return operation instead.

Next steps