Skip to content

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

  1. An admin creates the terminal in POS > Terminals; the grid shows the generated activation code.
  2. 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" }
  1. The server matches the code against terminals with an unused code, stamps activation_used_at (codes are single-use), binds the device_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.
  2. 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
}
  1. The device follows up with GET /V1/pos/terminals/:terminalId/config for 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_hash changes whenever the server-side config changes; the device re-pulls the terminal config payload when it no longer matches.
  • inactive_reason explains is_active: false: code_regenerated, license_revoked, or deactivated (null while active), so the lock screen shows the right message.
  • last_upstream_check_at / last_upstream_check_succeeded describe 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.