Register Reports
Hyvä POS is on the App Store
The free app is out for iPhone, iPad and Mac: install it from the App Store, connect your Magento store and sell. PRO, the Magento module with a license per terminal, follows shortly. News and pricing are 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, checked against the orders and credit memos Magento holds (module 1.5.0), 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 |
The routes exist since module 1.4.0. The checks against Magento's records and the no_sales and no_comparison states need module 1.5.0.
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 returns 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 identifying 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, and the terminal holds no closings of that numbering series |
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 |
see States |
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 deleted), created_at and text.
States
| State | Values |
|---|---|
state_closing_received |
not_evaluated, received |
state_documents_received |
not_evaluated, complete, incomplete, no_sales |
state_orders_processed |
not_evaluated, complete, incomplete, no_sales |
state_figures_reconciled |
not_evaluated, matched, mismatched, no_sales, no_comparison |
not_evaluated reads "Waiting for check" in the admin. The cron job hyva_pos_reconcile_register_reports (hourly, cron group hyva_pos_remote) sets the orders and figures states once Magento holds the closing and every document it lists. A closing that lists no documents (a session without sales) takes no_sales for its documents and orders states on arrival; its figures state is set by the next pass, no_sales when every tender lane (cash, card, split cash, split card, invoice) is zero and mismatched otherwise. A closing with only drawer movements (pay-ins, pay-outs, a cash refund from the RMA screen) lists no documents and takes no_sales too. The figures state is no_comparison when the pass ran over a closing whose orders are processed and found no figure to compare, as for a closing of exchanges only: exchanges are not compared, and the tender lanes are compared only when every document is a sale. no_comparison is final; a later pass keeps it, also when a late outcome makes the orders state incomplete. not_evaluated on the figures state means the pass has not run over the closing yet or waits for its orders to be processed. The cash count and cash difference are not compared for any closing. reconciliation_json holds what the pass found and is null until it ran. A closing is evaluated again on every pass until it is complete and matched, and for 35 days after it closed.
The first pass after the update to module 1.5.0 moves every stored closing without documents from not_evaluated to no_sales and draws a new revision for each, so a reader polling with changedAfter receives that burst once. A reader that exports only complete or matched closings does not export no-sales closings. The same first pass moves every stored closing of exchanges only whose orders are processed from not_evaluated to no_comparison, with a new revision each; a reader that maps the figures state needs the new value.
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. A pass over the same data dispatches nothing.
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.