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.
Overview
A register session captures one shift on a fiscalized register. You open a session at the start of the shift, every Sale, Return, Exchange that follows binds to it, and you close the session at end of shift. Sessions exist only for POS. ONLINE operations have no register and therefore no session.
Sessions are opened, adjusted, and closed by sending session-event operations through the same POST /operations endpoint that ingests sales, returns, and exchanges. This was intentional since under many regimes a session open, cash adjustment, or close is itself a fiscal event that must be tracked, securely persisted, and signed.
Session states
open — the session accepts goods-movement operations and cash adjustments
closed — the session is final; new operations on the register need a new session_open
A register has at most one open session at a time. Opening a second session while one is still open is rejected.
Session-event operation types
| Type | Purpose |
|---|
session_open | Start a new shift on a register with a counted opening float |
session_cash_adjustment | Record a mid-shift cash movement (drop, pay-in, till correction) |
session_close | End the shift with a merchant-counted closing balance |
All three flow through POST /operations, return an operation resource, and carry the same resource_version / ETag mechanics as goods-movement operations.
Opening a session
Send a session_open operation with the register, currency, and counted opening float.
curl -X POST https://api.openfiskal.com/v1/operations \
-H "Authorization: Bearer $API_KEY" \
-H "X-OpenFiskal-Merchant: merchant_01HXYZ" \
-H "Idempotency-Key: ses-2026-04-30-reg_abc123-open" \
-H "Content-Type: application/json" \
-d '{
"type": "session_open",
"register_id": "reg_abc123",
"currency": "EUR",
"opening_balance_amount": "50.00",
"opening_note": "Float supplied by office manager."
}'
The response carries the new session_id. It is recommended to persist this.
{
"id": "op_ses_open_001",
"type": "session_open",
"register_id": "reg_abc123",
"session_id": "ses_abc123",
"status": "completed",
"currency": "EUR",
"resource_version": 1
}
The operation status is completed because session-event operations are final the moment OpenFiskal accepts them, i.e. there is no PATCH .../complete step. Don’t confuse it with the session’s lifecycle state (the open / closed flag tracked separately).
Goods movements bind to the session
Every sale, return, and exchange you send on a POS register carries the session_id of the open session in the response. You do not set session_id on the request - OpenFiskal resolves it from the register’s currently open session.
If you call POST /operations with source: POS on a register that has no open session, the request is rejected.
Mid-shift cash adjustments
Use session_cash_adjustment for any cash movement that is not a sale or return: cash drops to the safe, pay-ins, till corrections. The cash_amount is signed: Negative for cash out, positive for cash in. Multiple adjustments per session are allowed.
curl -X POST https://api.openfiskal.com/v1/operations \
-H "Authorization: Bearer $API_KEY" \
-H "X-OpenFiskal-Merchant: merchant_01HXYZ" \
-H "Idempotency-Key: ses_abc123-adj-001" \
-H "Content-Type: application/json" \
-d '{
"type": "session_cash_adjustment",
"register_id": "reg_abc123",
"currency": "EUR",
"cash_amount": "-20.00",
"note": "Cash drop to safe."
}'
Closing a session
Send session_close with the merchant-counted closing balance. OpenFiskal compares it against the expected balance (opening float + cash sales − cash returns + adjustments) and records any discrepancy.
curl -X POST https://api.openfiskal.com/v1/operations \
-H "Authorization: Bearer $API_KEY" \
-H "X-OpenFiskal-Merchant: merchant_01HXYZ" \
-H "Idempotency-Key: ses_abc123-close" \
-H "Content-Type: application/json" \
-d '{
"type": "session_close",
"register_id": "reg_abc123",
"currency": "EUR",
"counted_closing_amount": "127.50",
"discrepancy_note": "Two coins from the day before were unaccounted for."
}'
Once the close is completed, the register has no open session. The next shift starts with a new session_open and a fresh session_id.
Walkthrough: a single shift
The scenario: a German bakery opens at 08:00 with a 50 EUR float, rings up two cash sales, drops 20 EUR to the safe at midday, and closes the till at 17:00 with 127.50 EUR counted.
Open the session with a 50 EUR float
curl -X POST https://api.openfiskal.com/v1/operations \
-H "Authorization: Bearer $API_KEY" \
-H "X-OpenFiskal-Merchant: merchant_01HXYZ" \
-H "Idempotency-Key: ses-2026-04-30-reg_abc123-open" \
-H "Content-Type: application/json" \
-d '{
"type": "session_open",
"register_id": "reg_abc123",
"currency": "EUR",
"opening_balance_amount": "50.00"
}'
Response (abbreviated):{
"id": "op_01_open",
"type": "session_open",
"session_id": "ses_abc123",
"register_id": "reg_abc123",
"status": "completed"
}
Capture ses_abc123 — every operation that follows on this register binds to it automatically.Ring up the first sale (4.50 EUR)
Open the sale:curl -X POST https://api.openfiskal.com/v1/operations \
-H "Authorization: Bearer $API_KEY" \
-H "X-OpenFiskal-Merchant: merchant_01HXYZ" \
-H "Idempotency-Key: order-1001-start" \
-H "Content-Type: application/json" \
-d '{
"type": "sale",
"source": "POS",
"register_id": "reg_abc123",
"currency": "EUR",
"external_id": "order-1001",
"pretax_amount": "4.21",
"tax_amount": "0.29",
"tip_amount": "0.00",
"total_amount": "4.50",
"line_items": [
{
"title": "Brötchen",
"quantity": 3,
"unit_price": "1.50",
"total_amount": "4.50",
"taxes": [
{ "name": "MwSt 7%", "rate": "0.07", "tax_amount": "0.29" }
]
}
]
}'
The response carries "session_id": "ses_abc123" even though you did not send it, and the operation comes back with "status": "open". Capture the returned id and resource_version for the complete step.Complete the sale with a cash payment so it counts toward the session’s expected closing balance:curl -X PATCH https://api.openfiskal.com/v1/operations/op_01_sale_4.50/complete \
-H "Authorization: Bearer $API_KEY" \
-H "X-OpenFiskal-Merchant: merchant_01HXYZ" \
-H "If-Match: \"1\"" \
-H "Idempotency-Key: order-1001-complete" \
-H "Content-Type: application/json" \
-d '{
"payments": [
{
"payment_id": "pay_1001",
"method": "cash",
"amount": "4.50",
"currency": "EUR",
"status": "captured"
}
]
}'
The response is now "status": "completed" and (on a fiscalized German register) carries a TSE signature in fiscal_information. Ring up the second sale (98.00 EUR)
curl -X POST https://api.openfiskal.com/v1/operations \
-H "Authorization: Bearer $API_KEY" \
-H "X-OpenFiskal-Merchant: merchant_01HXYZ" \
-H "Idempotency-Key: order-1002-start" \
-H "Content-Type: application/json" \
-d '{
"type": "sale",
"source": "POS",
"register_id": "reg_abc123",
"currency": "EUR",
"external_id": "order-1002",
"pretax_amount": "91.59",
"tax_amount": "6.41",
"tip_amount": "0.00",
"total_amount": "98.00",
"line_items": [
{
"title": "Catering tray",
"quantity": 1,
"unit_price": "98.00",
"total_amount": "98.00",
"taxes": [
{ "name": "MwSt 7%", "rate": "0.07", "tax_amount": "6.41" }
]
}
]
}'
Complete the sale the same way the first one was completed (PATCH /v1/operations/{id}/complete with If-Match: "1" and a payments array). Both sales must be completed for their cash to count toward the expected closing balance below.Drop 20 EUR to the safe at midday
curl -X POST https://api.openfiskal.com/v1/operations \
-H "Authorization: Bearer $API_KEY" \
-H "X-OpenFiskal-Merchant: merchant_01HXYZ" \
-H "Idempotency-Key: ses_abc123-drop-001" \
-H "Content-Type: application/json" \
-d '{
"type": "session_cash_adjustment",
"register_id": "reg_abc123",
"currency": "EUR",
"cash_amount": "-20.00",
"note": "Cash drop to safe."
}'
Expected balance now: 50.00 + 4.50 + 98.00 − 20.00 = 132.50 EUR.Close the session with a counted 127.50 EUR
curl -X POST https://api.openfiskal.com/v1/operations \
-H "Authorization: Bearer $API_KEY" \
-H "X-OpenFiskal-Merchant: merchant_01HXYZ" \
-H "Idempotency-Key: ses_abc123-close" \
-H "Content-Type: application/json" \
-d '{
"type": "session_close",
"register_id": "reg_abc123",
"currency": "EUR",
"counted_closing_amount": "127.50",
"discrepancy_note": "Five euro short — investigating."
}'
OpenFiskal records expected 132.50 EUR, counted 127.50 EUR, and a −5.00 EUR discrepancy. The session is now closed. The next shift on reg_abc123 starts with a new session_open and a fresh session_id.
Next steps