Locations and Terminals
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.
A location is a physical store: address, receipt branding, payment methods, tax zone, MSI source. A terminal is one register device bound to a location through a device_identifier. Merchants manage both in the Magento admin; the routes below serve the device and external integrations.
| Method | Route | ACL | Purpose |
|---|---|---|---|
GET |
/V1/pos/locations |
Hyva_Pos::locations |
List locations (searchCriteria) |
GET |
/V1/pos/locations/:locationId · /code/:code |
Hyva_Pos::locations |
Fetch by id or code |
POST / PUT |
/V1/pos/locations · /:locationId |
Hyva_Pos::locations_manage |
Create / update |
DELETE |
/V1/pos/locations/:locationId |
Hyva_Pos::locations_manage |
Delete |
PUT |
/V1/pos/locations/:locationId/config |
Hyva_Pos::locations_manage |
Push unlocked settings edits from a device |
GET |
/V1/pos/terminals · /:terminalId |
Hyva_Pos::locations |
List (searchCriteria) and fetch terminals |
POST / PUT |
/V1/pos/terminals · /:terminalId |
Hyva_Pos::locations_manage |
Create / update |
DELETE |
/V1/pos/terminals/:terminalId |
Hyva_Pos::locations_manage |
Delete |
POST |
/V1/pos/terminals/activate |
Hyva_Pos::locations_manage |
Bind a device with a one-time activation code |
POST |
/V1/pos/terminals/deactivate |
Hyva_Pos::locations |
Release the device binding |
POST |
/V1/pos/terminals/:terminalId/heartbeat |
Hyva_Pos::locations |
Periodic device check-in |
POST |
/V1/pos/terminals/:terminalId/license/refresh |
Hyva_Pos::locations |
Force a license refresh from hyva.io |
Locations
The iPad lists locations during setup (GET /V1/pos/locations?searchCriteria[pageSize]=100) and receives its own location inside the terminal config payload. Create and update take the entity wrapped in a location key; the response is the saved row. Field names are the hyva_pos_location columns - the most commonly set ones:
{
"location": {
"name": "Amsterdam Flagship",
"code": "ams-01",
"street": "Main Street 1",
"city": "Amsterdam",
"postcode": "1234AB",
"country_id": "NL",
"phone": "+31 20 123 4567",
"vat_number": "NL123456789B01",
"website_id": 1,
"source_code": "ams_store",
"is_active": true,
"payment_methods": "cash,card",
"low_stock_threshold": 5,
"tax_use_address": true,
"receipt_header": "Thank you!",
"receipt_footer": "",
"card_payment_provider": "mollie",
"pickup_completed_status_code": "complete",
"layaway_deposit_min_percent": 25,
"layaway_expiry_days": 30,
"layaway_cancellation_fee_percent": 0
}
}
Nullable override fields (vat_number, receipt_logo_size, card_payment_provider, receipt_store_info_placement, and others) inherit the global default when null. Errors: 404 for unknown id or code, 400 (CouldNotSaveException / CouldNotDeleteException) on failed writes.
Device Config Push - PUT /V1/pos/locations/:locationId/config
The reverse channel: a terminal pushes settings a cashier edited on the device back to the location row. The body is a config object holding only the changed fields. Accepted keys: receipt_header, receipt_footer, payment_methods, low_stock_threshold, store_address, store_phone, vat_number, mollie_enabled, mollie_organisation_id, mollie_api_key, mollie_test_api_key, mollie_test_mode. Unknown keys are ignored. Fields whose enforcement flag locks them are skipped - the interface contract is that only non-enforced fields are updated.
The optional meta keys _staff_id, _staff_name, _terminal_id, _terminal_name attribute the change; every accepted field change is written to the POS audit log with old and new values.
{
"config": {
"receipt_footer": "Returns within 30 days with receipt",
"low_stock_threshold": 3,
"_staff_id": 7,
"_staff_name": "Anna de Vries",
"_terminal_id": 3,
"_terminal_name": "Front Desk"
}
}
Returns true. Errors: 404 unknown location, 400 when config is not an object.
Terminals
Standard CRUD. The iPad also uses POST /V1/pos/terminals for code-less self-registration when the store does not require activation codes:
{
"terminal": {
"name": "Front Desk",
"device_identifier": "8F3A61E2-5B2D-4C55-9A1E-0F47C2D9B3A1",
"location_id": 1,
"is_active": true
}
}
A terminal row created in the admin instead carries a generated one-time activation_code and a null activation_used_at.
Activation Flow
- An admin creates the terminal in POS > Terminals; the grid shows the generated activation code.
- The cashier enters the code on the device during setup. The app calls:
POST /V1/pos/terminals/activate
{ "activationCode": "K7Q2M9", "deviceIdentifier": "8F3A61E2-5B2D-4C55-9A1E-0F47C2D9B3A1" }
- The server matches the code against terminals with an unused code, stamps
activation_used_at(codes are single-use), binds thedevice_identifier, and - when a license is linked and hyva.io credentials are configured - activates the license upstream. A license already bound to another device is auto-released and retried once. - The response carries everything the register needs to boot: the terminal, its location, the staff who may sign in, their roles, and fiskaly credentials for fiscalized (German) stores:
{
"terminal": { "terminal_id": 3, "name": "Front Desk", "device_identifier": "8F3A61E2-...", "location_id": 1, "is_active": true, "activation_used_at": "2026-08-12 09:14:02" },
"location": { "location_id": 1, "name": "Amsterdam Flagship", "code": "ams-01" },
"staff_list": [ { "staff_id": 7, "first_name": "Anna", "last_name": "de Vries", "email": "anna@example.com", "role_id": 2, "is_active": true } ],
"role_list": [ { "role_id": 2, "name": "Cashier", "permissions": "{\"place_orders\":true}", "is_active": true } ],
"fiskaly_credentials": null
}
- The device follows up with
GET /V1/pos/terminals/:terminalId/configfor the resolved config and license detail, and starts heartbeating.
Errors: 400 Invalid activation code format. (code must match ^[A-Za-z0-9]{4,12}$) or Invalid device identifier format. (^[a-zA-Z0-9-]{1,100}$); 404 Activation code not valid. for unknown or already-used codes (attempts are rate limited per device identifier); 400 This terminal is disabled. Enable it in the Magento admin before activating. when an admin has switched is_active off; 400 License validation failed: ... when hyva.io rejects the license.
Deactivation - POST /V1/pos/terminals/deactivate
Called by the device when the user unregisters the terminal in Settings. Body: { "terminalId": 3, "deviceIdentifier": "8F3A61E2-..." }. The server verifies the device identifier matches the registered one, then releases the binding. Returns true. Errors: 404 unknown terminal, 401 (AuthenticationException) on device mismatch.
Heartbeat - POST /V1/pos/terminals/:terminalId/heartbeat
The device's periodic check-in: it updates last_seen_at, verifies device identity, and returns terminal, config, and license state in one lightweight call. Body: { "deviceIdentifier": "8F3A61E2-..." }.
{
"is_active": true,
"device_identifier": "8F3A61E2-...",
"require_terminal_registration": true,
"config_hash": "5c9a1f0e",
"hyvaio_connection_ok": true,
"has_license": true,
"license_status": "active",
"license_expires_at": "2027-08-12 09:14:02",
"license_token": "eyJhbGciOi...",
"inactive_reason": null,
"last_upstream_check_at": "2026-08-12 06:00:11",
"last_upstream_check_succeeded": true
}
config_hashchanges whenever the server-side config changes; the device re-pulls the terminal config payload when it no longer matches.inactive_reasonexplainsis_active: false:code_regenerated,license_revoked, ordeactivated(nullwhile active), so the lock screen shows the right message.last_upstream_check_at/last_upstream_check_succeededdescribe the most recent hyva.io refresh so the device never pings hyva.io itself.
Errors: 404 unknown terminal, 401 on device-identifier mismatch.
License Refresh - POST /V1/pos/terminals/:terminalId/license/refresh
Triggered by the "Check License" button on the device; forces an immediate refresh against hyva.io instead of waiting for the scheduled check. Body: { "deviceIdentifier": "8F3A61E2-..." }.
{
"success": true,
"license_token": "eyJhbGciOi...",
"license_status": "active",
"license_expires_at": "2027-08-12 09:14:02",
"fiskaly_credentials": null,
"error": null
}
On upstream failure, success is false and error carries the hyva.io message. Errors: 404 unknown terminal, 401 on device-identifier mismatch.