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

# Sessions

> How register sessions group POS operations into a cash-drawer shift, from open to count to close.

## 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] ──► [counted] ──► [closed]
```

* `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

| 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) |

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.

```bash theme={null}
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.

```json theme={null}
{
  "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 `POST .../complete` step. Don't confuse it with the session's lifecycle state (the [`open` / `counted` / `closed`](#session-states) 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.

```bash theme={null}
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."
  }'
```

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

```bash theme={null}
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-count" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "session_cash_count",
    "register_id": "reg_abc123",
    "currency": "EUR",
    "counted_closing_amount": "127.50",
    "discrepancy_note": "Two coins from the day before were unaccounted for."
  }'
```

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.

```json theme={null}
{
  "id": "op_ses_count_001",
  "type": "session_cash_count",
  "session_id": "ses_abc123",
  "register_id": "reg_abc123",
  "status": "completed",
  "currency": "EUR",
  "counted_closing_amount": "127.50",
  "expected_closing_amount": "132.50",
  "closing_cashcount_diff": "-5.00",
  "discrepancy_note": "Two coins from the day before were unaccounted for."
}
```

**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`.

```bash theme={null}
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",
    "business_date": "2026-04-30",
    "closing_adjustment_amount": "-100.00",
    "closing_adjustment_note": "Cash pulled to safe deposit."
  }'
```

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

<Steps>
  <Step title="Open the session with a 50 EUR float">
    ```bash theme={null}
    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):

    ```json theme={null}
    {
      "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.
  </Step>

  <Step title="Ring up the first sale (4.50 EUR)">
    Open the sale:

    ```bash theme={null}
    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",
            "sku_identifier": "BROETCHEN",
            "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:

    ```bash theme={null}
    curl -X POST 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`.
  </Step>

  <Step title="Ring up the second sale (98.00 EUR)">
    ```bash theme={null}
    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",
            "sku_identifier": "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 (`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.
  </Step>

  <Step title="Drop 20 EUR to the safe at midday">
    ```bash theme={null}
    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`.
  </Step>

  <Step title="Count the drawer at 17:00 (counted 127.50 EUR)">
    ```bash theme={null}
    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-count" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "session_cash_count",
        "register_id": "reg_abc123",
        "currency": "EUR",
        "counted_closing_amount": "127.50",
        "discrepancy_note": "Five euro short — investigating."
      }'
    ```

    Response:

    ```json theme={null}
    {
      "id": "op_05_count",
      "type": "session_cash_count",
      "session_id": "ses_abc123",
      "status": "completed",
      "counted_closing_amount": "127.50",
      "expected_closing_amount": "132.50",
      "closing_cashcount_diff": "-5.00",
      "discrepancy_note": "Five euro short — investigating."
    }
    ```

    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.
  </Step>

  <Step title="Pull 100 EUR to the safe and seal the session">
    ```bash theme={null}
    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",
        "business_date": "2026-04-30",
        "closing_adjustment_amount": "-100.00",
        "closing_adjustment_note": "Cash pulled to safe deposit."
      }'
    ```

    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.
  </Step>
</Steps>

## Next steps

* [Register lifecycle](/register-lifecycle)
* [Transaction lifecycle](/pos-operation-ingestion)
