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.

Overview

A register session is the cash-drawer shift on a fiscalized register. You open a session at the start of the shift, every POS goods-movement that follows binds to it, and you close the session at end of shift with a counted cash balance. Sessions exist only on POS — ONLINE operations have no register and therefore no session. Sessions are not created with their own endpoint. They are opened, adjusted, and closed by sending session-event operations through the same POST /operations endpoint that ingests sales, returns, and exchanges. Under many regimes a session open, cash adjustment, or close is itself a fiscal event that must be tracked, persisted, and signed alongside goods movements — routing them through /operations is what gives them the same lifecycle, idempotency, and signature surface as a sale.

Session states

[open] ──► [closed]
  • 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

TypePurpose
session_openStart a new shift on a register with a counted opening float
session_cash_adjustmentRecord a mid-shift cash movement (drop, pay-in, till correction)
session_closeEnd 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. Store it — every operation you send on this register until close binds to it automatically.
{
  "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 — there is no later 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.
1

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

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

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

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

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