This guide walks you through a complete integration — from receiving your setup token to completing a fiscalized sale and printing a receipt. The examples use a German merchant (TSE jurisdiction) with sandbox URLs.
All examples use https://sandbox.api.openfiskal.com/v1. Replace with https://api.openfiskal.com/v1 when you go live.
Prerequisites
- A partnership agreement signed with OpenFiskal
- The setup token (
ofk_setup_...) received via email
curl or an HTTP client
Step-by-step
Bootstrap your API key
After signing a partnership agreement, OpenFiskal emails a one-time setup token to your technical contact. Exchange it for your platform API key.curl -X POST https://sandbox.api.openfiskal.com/v1/auth/bootstrap \
-H "Content-Type: application/json" \
-d '{
"setup_token": "ofk_setup_abc123def456",
"label": "Production"
}'
{
"object": "platform_api_key",
"id": "key_01HXYZ",
"api_key": "ofk_platform_live_abc123...",
"label": "Production",
"created_at": "2026-02-26T10:00:00Z"
}
All subsequent requests use Authorization: Bearer <api_key>.Store api_key immediately in a secrets manager. It is returned once and cannot be retrieved again. The setup token is now consumed — it cannot be reused.
Create an organization
An organization represents the merchant — the legal entity operating the POS.curl -X POST https://sandbox.api.openfiskal.com/v1/organizations \
-H "Authorization: Bearer ofk_platform_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"legal_name": "Mustermann GmbH",
"country": "DE",
"tax_id": "DE123456789",
"address": {
"line1": "Friedrichstraße 42",
"city": "Berlin",
"postal_code": "10117",
"country": "DE"
}
}'
{
"id": "org_01HXYZ",
"object": "organization",
"legal_name": "Mustermann GmbH",
"country": "DE",
"tax_id": "DE123456789",
"address": {
"line1": "Friedrichstraße 42",
"city": "Berlin",
"postal_code": "10117",
"country": "DE"
},
"created_at": "2026-02-26T10:01:00Z"
}
Note the returned id — use it as the OpenFiskal-Organization header on all subsequent requests scoped to this merchant. Create a location
A location is a physical point of sale — a store, restaurant, or kiosk.curl -X POST https://sandbox.api.openfiskal.com/v1/organizations/org_01HXYZ/locations \
-H "Authorization: Bearer ofk_platform_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"name": "Berlin Mitte",
"address": {
"line1": "Friedrichstraße 42",
"city": "Berlin",
"postal_code": "10117",
"country": "DE"
},
"timezone": "Europe/Berlin"
}'
{
"id": "loc_01HXYZ",
"object": "location",
"name": "Berlin Mitte",
"organization_id": "org_01HXYZ",
"timezone": "Europe/Berlin",
"created_at": "2026-02-26T10:02:00Z"
}
Create a register
A register is a POS device. Creating one is lightweight — it’s just metadata.curl -X POST https://sandbox.api.openfiskal.com/v1/locations/loc_01HXYZ/registers \
-H "Authorization: Bearer ofk_platform_live_abc123..." \
-H "OpenFiskal-Organization: org_01HXYZ" \
-H "Content-Type: application/json" \
-d '{
"label": "Kasse 1",
"jurisdiction": "DE"
}'
{
"id": "reg_01HXYZ",
"object": "register",
"label": "Kasse 1",
"jurisdiction": "DE",
"state": "active",
"active_fiscal_unit": null,
"location_id": "loc_01HXYZ",
"created_at": "2026-02-26T10:03:00Z"
}
The register is active but has no fiscal unit yet — it cannot process operations until one is provisioned. Create a fiscal unit
A fiscal unit initializes the fiscal security device. For Germany, this sets up the TSE and establishes the signature chain.curl -X POST https://sandbox.api.openfiskal.com/v1/registers/reg_01HXYZ/fiscal-units \
-H "Authorization: Bearer ofk_platform_live_abc123..." \
-H "OpenFiskal-Organization: org_01HXYZ"
{
"object": "fiscal_unit_response",
"fiscal_unit": {
"id": "fu_01HXYZ",
"object": "fiscal_unit",
"register_id": "reg_01HXYZ",
"state": "active",
"jurisdiction": "DE",
"fiscal_registration": {
"regime": "tse",
"tse_serial": "abc123def456",
"client_id": "Kasse 1",
"signature_algorithm": "ecdsa-plain-SHA384",
"time_format": "utcTime",
"registered_at": "2026-02-26T10:03:30Z"
},
"provisioned_at": "2026-02-26T10:03:30Z",
"created_at": "2026-02-26T10:03:30Z"
},
"register": {
"id": "reg_01HXYZ",
"state": "active",
"active_fiscal_unit": { "id": "fu_01HXYZ", "state": "active" }
},
"register_api_key": null,
"credential_issued": false
}
The register now has an active fiscal unit and is ready to accept operations.For server-side integrations, the platform API key already authorizes all register operations. Only pass issue_register_credential: true for device-direct integrations (EV chargers, unattended kiosks).
Start a sale
Opening a sale is a fiscal event. The TSE signs the start immediately. Store the returned signature and transaction counter — they must appear on the final receipt.curl -X POST https://sandbox.api.openfiskal.com/v1/registers/reg_01HXYZ/sales \
-H "Authorization: Bearer ofk_platform_live_abc123..." \
-H "OpenFiskal-Organization: org_01HXYZ" \
-H "Idempotency-Key: sale-table3-order42-start" \
-H "Content-Type: application/json" \
-d '{
"external_id": "table-3-order-42",
"cashier": {
"id": "cashier_01",
"display_name": "Maria R."
}
}'
{
"id": "op_01HXYZ",
"object": "operation",
"type": "sale",
"state": "open",
"fiscal_state": "succeeded",
"register_id": "reg_01HXYZ",
"external_id": "table-3-order-42",
"started_at": "2026-02-26T12:00:00Z",
"fiscal": {
"regime": "tse",
"fiscal_unit_id": "fu_01HXYZ",
"tse_serial": "abc123def456",
"start_event": {
"signed_at": "2026-02-26T12:00:00Z",
"transaction_counter": 1,
"signature": "base64encodedStartSignature=="
}
}
}
The fiscal.start_event data is required on the printed receipt. Do not discard it.
Add line items
While the sale is open, add items via PATCH. This is not a fiscal event — no signatures are produced. The line_items array replaces the current list entirely.All monetary amounts are integers in the minor currency unit (cents for EUR). Tax rates are decimal strings ("0.19" for 19%).curl -X PATCH https://sandbox.api.openfiskal.com/v1/sales/op_01HXYZ \
-H "Authorization: Bearer ofk_platform_live_abc123..." \
-H "OpenFiskal-Organization: org_01HXYZ" \
-H "Content-Type: application/json" \
-d '{
"line_items": [
{
"description": "Wiener Schnitzel",
"quantity": 2,
"unit_price_gross": 1800,
"tax": { "rate": "0.19", "category": "standard" }
},
{
"description": "Apfelschorle",
"quantity": 2,
"unit_price_gross": 350,
"tax": { "rate": "0.19", "category": "standard" }
}
]
}'
The response includes the resolved line items with computed totals:{
"id": "op_01HXYZ",
"state": "open",
"data": {
"line_items": [
{
"description": "Wiener Schnitzel",
"quantity": 2,
"unit_price_gross": 1800,
"total_gross": 3600,
"tax": { "rate": "0.19", "category": "standard", "amount": 575 }
},
{
"description": "Apfelschorle",
"quantity": 2,
"unit_price_gross": 350,
"total_gross": 700,
"tax": { "rate": "0.19", "category": "standard", "amount": 112 }
}
],
"totals": {
"total_gross": 4300,
"total_net": 3613,
"total_tax": 687
}
}
}
To add an item to an existing list, include all current items plus the new one. The array is a full replacement, not an append.
Complete with payment
Completing the sale is a fiscal event. The TSE signs the end, and OpenFiskal generates the fiscal document. At least one payment is required. Card payment
Cash payment
curl -X POST https://sandbox.api.openfiskal.com/v1/sales/op_01HXYZ/complete \
-H "Authorization: Bearer ofk_platform_live_abc123..." \
-H "OpenFiskal-Organization: org_01HXYZ" \
-H "Idempotency-Key: sale-table3-order42-complete" \
-H "Content-Type: application/json" \
-d '{
"payments": [
{
"method": "card",
"direction": "inbound",
"amount": 4300,
"card": { "scheme": "visa", "last4": "4242" }
}
]
}'
Cash tendered and change are modeled as two separate payment lines with opposite directions.curl -X POST https://sandbox.api.openfiskal.com/v1/sales/op_01HXYZ/complete \
-H "Authorization: Bearer ofk_platform_live_abc123..." \
-H "OpenFiskal-Organization: org_01HXYZ" \
-H "Idempotency-Key: sale-table3-order42-complete" \
-H "Content-Type: application/json" \
-d '{
"payments": [
{
"method": "cash",
"direction": "inbound",
"amount": 5000,
"cash": { "amount_tendered": 5000 }
},
{
"method": "cash",
"direction": "outbound",
"amount": 700
}
]
}'
The response includes the completed operation with the full fiscal document:{
"id": "op_01HXYZ",
"state": "completed",
"fiscal_state": "succeeded",
"completed_at": "2026-02-26T12:14:55Z",
"fiscal": {
"regime": "tse",
"fiscal_unit_id": "fu_01HXYZ",
"document_number": "2026-000001",
"document_type": "Kassenbeleg",
"tse_serial": "abc123def456",
"start_event": {
"signed_at": "2026-02-26T12:00:00Z",
"transaction_counter": 1,
"signature": "base64encodedStartSignature=="
},
"end_event": {
"signed_at": "2026-02-26T12:14:55Z",
"transaction_counter": 2,
"signature": "base64encodedEndSignature==",
"process_type": "Kassenbeleg-V1",
"process_data": "Beleg^43.00_0.00_0.00_0.00_0.00^43.00:Unbar"
},
"verification": {
"qr_data": "V0;Kasse 1;ecdsa-plain-SHA384;2026-02-26T12:00:00;2026-02-26T12:14:55;43.00;2;base64encodedEndSignature=="
}
}
}
Retrieve the receipt
The receipt is available as JSON or PDF immediately after completion.# JSON receipt
curl https://sandbox.api.openfiskal.com/v1/operations/op_01HXYZ/receipt \
-H "Authorization: Bearer ofk_platform_live_abc123..." \
-H "OpenFiskal-Organization: org_01HXYZ"
# PDF receipt
curl https://sandbox.api.openfiskal.com/v1/operations/op_01HXYZ/receipt.pdf \
-H "Authorization: Bearer ofk_platform_live_abc123..." \
-H "OpenFiskal-Organization: org_01HXYZ" \
-o receipt.pdf
The JSON receipt is self-contained — it includes issuer details, line items, totals, payments, and the full fiscal document with QR data:{
"object": "receipt",
"issuer": {
"legal_name": "Mustermann GmbH",
"tax_id": "DE123456789",
"location_name": "Berlin Mitte",
"address": {
"line1": "Friedrichstraße 42",
"city": "Berlin",
"postal_code": "10117",
"country": "DE"
}
},
"operation": {
"id": "op_01HXYZ",
"type": "sale",
"external_id": "table-3-order-42",
"cashier": { "id": "cashier_01", "display_name": "Maria R." },
"started_at": "2026-02-26T12:00:00Z",
"completed_at": "2026-02-26T12:14:55Z"
},
"line_items": ["..."],
"totals": {
"total_gross": 4300,
"total_net": 3613,
"total_tax": 687
},
"payments": ["..."],
"fiscal": {
"regime": "tse",
"fiscal_unit_id": "fu_01HXYZ",
"verification": {
"qr_data": "V0;Kasse 1;ecdsa-plain-SHA384;..."
}
},
"links": {
"self": "https://sandbox.api.openfiskal.com/v1/operations/op_01HXYZ/receipt",
"pdf": "https://sandbox.api.openfiskal.com/v1/operations/op_01HXYZ/receipt.pdf"
}
}
Print the fiscal.verification.qr_data value as a QR code on the receipt. In Germany, this is legally required.
What’s next