Skip to main content

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, the merchant counts the drawer at the end of the shift, then you close the session. Sessions exist only for POS. ONLINE operations have no register and therefore no session. Sessions are opened, adjusted, counted, 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, count, or close is itself a fiscal event that must be tracked, securely persisted, and signed.

Session lifecycle

  • open — the session accepts goods-movement operations and cash adjustments
  • counted — the merchant has counted the drawer and the system has signed the counted-vs-expected variance. The session is frozen: only session_close is accepted from here. No further sales / returns / exchanges / adjustments / repeat counts.
  • closed — the session is final; new operations on the register need a new session_open
A register has at most one in-progress (open or counted) session at a time. Opening a second session while one is still in progress is rejected.

Session-event operation types

All four 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.
The response carries the new session_id. It is recommended to persist this.
The operation status is completed because session-event operations are final the moment OpenFiskal accepts them, i.e. there is no POST .../complete step. Don’t confuse it with the session’s lifecycle state (the open / counted / 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.

Counting the drawer

End-of-shift is two POSTs: first the merchant counts the drawer (session_cash_count), then they seal the session (session_close). The two events are split because they attest to different fiscal facts:
  • The count attests what was actually in the drawer vs what the system expected. This is the variance attestation — the regulator’s primary interest. Fiscally signed as a standalone event.
  • The close records the optional cash movement at end of shift (cash pulled to the safe / float top-up for the next shift). When non-zero, fiscally signed as a cash-movement event.
Send session_cash_count with the merchant-counted closing balance. OpenFiskal computes the expected balance (opening float + cash sales − cash returns + adjustments), returns the diff, and signs the variance.
The response carries the system-calculated expected_closing_amount and the signed closing_cashcount_diff. The merchant UI typically displays “you’re 5€ over” / “you’re 5€ short” / “on the dot” using the diff value.
One count per session. A second session_cash_count POST against the same session returns 409 session_already_counted. The integrator’s UX should only fire the request once the merchant has confirmed their count. The count freezes the session. Once the count is accepted, the session is frozen against further sales / returns / exchanges / adjustments / repeat counts — anything other than session_close returns 409 session_already_counted. The freeze is what makes the signed variance legally meaningful: expected cannot drift between count and close.

Closing a session

Send session_close to seal the session. The body requires business_date (the posting date in YYYY-MM-DD format) and optionally carries closing_adjustment_amount (signed: negative = pull cash out, positive = add cash in) and closing_adjustment_note. Counting must have happened first — session_close on an un-counted session returns 409 session_not_counted.
business_date is the date of posting for financial-accounting purposes. If the session opens and closes on the same calendar day, use that date. If the session crosses midnight (for example, a bar that closes after 02:00), pick the business day the merchant treats as the operating day; consult your tax advisor when in doubt. When closing_adjustment_amount is non-zero, the close is fiscally signed (a cash movement at close is a fiscal event). Omit the field (or pass 0) for a clean close that doesn’t move cash — those closes don’t produce a separate signature, only a state transition. The drawer state at end of shift is therefore: counted_closing_amount + closing_adjustment_amount = amount left for the next shift. Once the close is completed, the register has no in-progress 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, counts the drawer at 17:00 finding 127.50 EUR, then pulls 100 EUR to the safe deposit before sealing the session.
1

Open the session with a 50 EUR float

Response (abbreviated):
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:
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:
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)

Complete the sale the same way the first one was completed (POST /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

Expected balance now: 50.00 + 4.50 + 98.00 − 20.00 = 132.50 EUR.
5

Count the drawer at 17:00 (counted 127.50 EUR)

Response:
The signed variance (closing_cashcount_diff: -5.00) is the regulator’s primary attestation. The session is now counted and frozen — only session_close is accepted from here.
6

Pull 100 EUR to the safe and seal the session

The drawer is left with 127.50 − 100.00 = 27.50 EUR for the next shift’s float. The closing_adjustment_amount is fiscally signed (a non-zero pull is a fiscal cash event). The session is now closed. The next shift on reg_abc123 starts with a new session_open and a fresh session_id.Audit trail summary: two TSE signatures came out of this end-of-shift sequence — one for the count’s −5.00 EUR variance, one for the close’s −100.00 EUR pull.

Next steps