Register Reports
Hyvä POS is in closed beta
Hyvä POS is currently in a closed beta (pilot phase) with a small group of merchants. It is not yet generally available: the App Store release follows the pilot, and features and configuration may still change, possibly in backwards-incompatible ways, before the general release. Want to take part? Sign up at hyva.io/pos.
Every register closing (the Z report of one register session) that a PRO register issues is uploaded to Magento, where it is stored once in hyva_pos_register_report and read by the admin and by connectors. Module 1.4.0; see the feature page for what a closing holds and how the admin shows it.
| Method | Route | Purpose |
|---|---|---|
POST |
/V1/pos/register-reports |
Store the closing of a register session, schema version 1 |
GET |
/V1/pos/register-reports |
List closings via searchCriteria, or the closings changed after a revision |
GET |
/V1/pos/register-reports/:reportId |
One closing with its document and annotations |
| ACL resource | Routes |
|---|---|
Hyva_Pos::register_reports_submit |
POST. Granted on upgrade to every role and integration that has Hyva_Pos::receipts |
Hyva_Pos::register_reports |
Both GET routes, the Register Reports listing and Register Totals in the admin |
Hyva_Pos::register_reports_annotate |
Adding an annotation in the admin; there is no annotation route |
Submit a Closing
POST /V1/pos/register-reports
Called by the register right after the close, and again on later sync passes while Magento has not taken the closing. The body wraps the closing as report. Magento stores the document byte for byte as received and copies the indexed fields into columns.
{
"report": {
"schema_version": 1,
"report_id": "2F8D1C8E-5B1A-4F0E-9C3D-7A6B5C4D3E2F",
"session_id": "0B3C9A7E-1D2F-4E5A-8B6C-9D0E1F2A3B4C",
"terminal_id": 3,
"location_id": 1,
"terminal_name": "Front Desk 01",
"location_name": "Amsterdam Store",
"device_identifier": "A1B2C3D4-…",
"series_id": "C4A1F0E9-…",
"series_code": "C4A1",
"closing_number": 41,
"currency": "EUR",
"timezone": "Europe/Amsterdam",
"business_date": "2026-10-12",
"opened_at": "2026-10-12T08:02:11+02:00",
"closed_at": "2026-10-12T18:31:40+02:00",
"opened_by": { "local_user_id": "…", "name": "Anna", "staff_id": 7 },
"closed_by": { "local_user_id": "…", "name": "Anna", "staff_id": 7 },
"cash_drawer": true,
"opening_float": "150.00",
"cash_expected": "612.35",
"cash_counted": "612.00",
"cash_difference": "-0.35",
"cash_left_in_drawer": "150.00",
"order_count": 23,
"sales": { "excl_tax": "1810.45", "tax": "380.19", "incl_tax": "2190.64" },
"refunds": { "excl_tax": "40.50", "tax": "8.51", "incl_tax": "49.01" },
"tenders": { "cash": "462.35", "card": "1600.00", "split_cash": "0.00", "split_card": "0.00", "invoice": "128.29", "...": "…" },
"tax_rates": [ { "rate": "21.00", "treatment": "standard", "sales_net": "1810.45", "sales_tax": "380.19", "refund_net": "40.50", "refund_tax": "8.51" } ],
"tax_treatments": { "reverse_charge": { "sales_net": "0.00", "refund_net": "0.00" }, "exempt": { "sales_net": "0.00", "refund_net": "0.00" } },
"providers": [ { "provider": "mollie", "name": "Mollie", "collected": "1600.00", "refunded": "0.00", "count": 19 } ],
"movements": [ { "type": "cash_out", "amount": "50.00", "note": "Bank run", "at": "2026-10-12T13:10:02+02:00", "by": { "local_user_id": "…", "name": "Anna", "staff_id": 7 } } ],
"documents": [ { "sale_id": "9A9E4AF3-…", "type": "sale", "amount": "95.00", "receipt_number": "R-000412" } ],
"first_receipt_number": "R-000390",
"last_receipt_number": "R-000412",
"closing_note": ""
}
}
| Field | Type | Notes |
|---|---|---|
schema_version |
int | 1 |
report_id |
UUID | the closing's id, the key of the stored row |
session_id |
UUID | the register session; one closing per session |
terminal_id, location_id |
int | Magento ids at the close |
terminal_name, location_name |
string | the names at the close |
device_identifier |
string | the device, as on the heartbeat; must be a device the terminal is or was bound to |
series_id, series_code, closing_number |
UUID, string, int | the numbering series of the install, its first four characters in upper case, and the number from 1 within it |
currency |
string | ISO 4217; every amount is in it |
timezone, business_date |
string, date | IANA zone of the location at open; the opening date in that zone |
opened_at, closed_at |
datetime | with offset |
opened_by, closed_by |
object | local_user_id, name, staff_id (null without a staff account) |
cash_drawer |
bool | false: the five drawer amounts are absent |
opening_float, cash_expected, cash_counted, cash_difference, cash_left_in_drawer |
amount | only with a drawer |
order_count |
int | |
sales, refunds |
object | excl_tax, tax, incl_tax |
tenders |
object | cash, card, split_cash, split_card, invoice, credit_redeemed, card_refunds, manual_card_refunds, credits_issued, credits_returned, invoice_credit_notes, layaway_receivable, layaway_payments, credit_due_collections, cash_rounding |
tax_rates |
array | rate, treatment (standard, unknown), sales_net, sales_tax, refund_net, refund_tax |
tax_treatments |
object | reverse_charge and exempt, each sales_net and refund_net |
providers |
array | provider, name, collected, refunded, count |
movements |
array | type (cash_in, cash_out, cash_refund), amount, note, at, by |
documents |
array | sale_id, type, amount, receipt_number; the receipts Magento matches for Documents Received |
first_receipt_number, last_receipt_number |
string | display only |
closing_note |
string |
Amounts are decimal strings. A closing is stored once: the answer carries the stored row's report_id, its revision and the series_ordinal Magento gave the series on the terminal (1 for the first series of a terminal, 2 for the next).
Sending the same document again answers the stored row with 200. A document that is not a valid schema 1 closing is refused with 400; a terminal that does not exist with 404. A document that collides with a stored one is refused with 409 and parameters.outcome naming the collision:
outcome |
Meaning |
|---|---|
report_payload_differs |
a different document under a stored report_id |
session_already_closed |
Magento already holds a closing for this session_id |
number_taken |
this closing_number is already stored in this series |
device_mismatch |
the device is not and never was bound to the terminal |
The register lists a refused closing under Reports → Sync Issues and retries it from there. A closing is never updated or deleted through the API.
List Closings
GET /V1/pos/register-reports?searchCriteria[...]
Standard searchCriteria over the indexed columns: terminal_id, location_id, business_date, closed_at, currency, series_id, closing_number, the four states, and the amounts. Each item carries the indexed fields below; the document itself (report_json) and the annotations come with the single read.
| Field | Notes |
|---|---|
report_id, session_id, terminal_id, location_id |
ids |
series_id, series_code, series_ordinal, closing_number |
the numbering |
terminal_name, location_name |
the names at the close |
business_date, opened_at, closed_at, timezone, currency |
|
opened_by_staff_id, opened_by_name, closed_by_staff_id, closed_by_name |
|
order_count, has_cash_drawer |
|
sales_excl_tax, sales_tax, sales_incl_tax, refunds_incl_tax |
decimal strings |
cash_expected, cash_counted, cash_difference |
null without a drawer |
tender_cash, tender_card, tender_split_cash, tender_split_card, tender_invoice |
|
state_closing_received, state_documents_received, state_orders_processed, state_figures_reconciled |
not_evaluated, received, complete, incomplete, matched, mismatched |
corrects_report_id |
reserved; null in this release |
revision |
from one module-wide sequence; see below |
synced_at, updated_at |
when the closing arrived and when it last changed |
Reading changes
Every closing carries a revision drawn from one module-wide sequence: once when the closing arrives and again whenever something about it changes later (a state, an annotation). A connector keeps the highest revision it has seen and asks for everything above it:
GET /V1/pos/register-reports?changedAfter=1187&searchCriteria[pageSize]=100
The answer lists the closings whose revision is above the cursor, oldest change first, and highest_revision is the cursor to ask from next. A closing that changed comes back again with its new revision; start from 0 for everything.
{ "items": [ { "report_id": "…", "revision": 1188, "...": "…" } ], "total_count": 1, "highest_revision": 1188 }
Read One Closing
GET /V1/pos/register-reports/:reportId
The indexed fields above, plus report_json (the document exactly as the register sent it) and annotations, oldest first: each with annotation_id, report_id, author, author_id (the admin user, null when the account is gone), created_at and text.
Event
hyva_pos_register_report_saved is dispatched after a closing is stored or changed, once the database transaction has committed, with report_id in the event data. Read the closing through the repository (Hyva\Pos\Api\RegisterReportRepositoryInterface::get) in the observer; an accounting or ERP connector can also poll with changedAfter instead.
Receipts
Each receipt a register from 0.3.0 uploads to a module from 1.4.0 carries register_session_id, the session it was booked into. Documents Received on a closing is complete when Magento holds a receipt for every sale_id the closing lists, and a receipt that arrives after its closing sets the state again. See Receipts.