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 adjustmentscounted— the merchant has counted the drawer and the system has signed the counted-vs-expected variance. The session is frozen: onlysession_closeis accepted from here. No further sales / returns / exchanges / adjustments / repeat counts.closed— the session is final; new operations on the register need a newsession_open
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_cash_count | Count the drawer at end of shift; produce the signed counted-vs-expected variance |
session_close | Seal the session, optionally moving cash out (closing pull) or in (next-shift float top-up) |
POST /operations, return an operation resource, and carry the same resource_version / ETag mechanics as goods-movement operations.
Opening a session
Send asession_open operation with the register, currency, and counted opening float.
session_id. It is recommended to persist this.
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
Everysale, 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
Usesession_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.
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.
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.
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
Sendsession_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.Open the session with a 50 EUR float
ses_abc123 — every operation that follows on this register binds to it automatically.Ring up the first sale (4.50 EUR)
Open the sale:The response carries The response is now
"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:"status": "completed" and (on a fiscalized German register) carries a TSE signature in fiscal_information.Ring up the second sale (98.00 EUR)
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.Count the drawer at 17:00 (counted 127.50 EUR)
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.Pull 100 EUR to the safe and seal the session
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.