Staff and Roles
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.
POS staff are their own entity (hyva_pos_staff), separate from Magento admin users: a cashier signs in on the register with email + passcode and gets a role whose permission keys gate what the app lets them do. Merchants manage staff and roles in the admin; the device consumes them through the terminal config payload and the authentication endpoints below.
| Method | Route | ACL | Purpose |
|---|---|---|---|
GET |
/V1/pos/staff · /:staffId |
Hyva_Pos::staff_manage |
List (searchCriteria) and fetch staff |
POST / PUT |
/V1/pos/staff · /:staffId |
Hyva_Pos::staff_manage_edit |
Create / update |
DELETE |
/V1/pos/staff/:staffId |
Hyva_Pos::staff_manage_edit |
Delete |
GET |
/V1/pos/staff/:staffId/locations |
Hyva_Pos::staff_manage |
Locations the staff member may work at |
POST / DELETE |
/V1/pos/staff/:staffId/locations · /:locationId |
Hyva_Pos::staff_manage_edit |
Assign / unassign a location |
POST |
/V1/pos/staff/authenticate |
Hyva_Pos::staff_manage |
Email + passcode login |
POST |
/V1/pos/staff/session/validate |
Hyva_Pos::staff_manage |
Validate a session token |
POST |
/V1/pos/staff/:staffId/unlock |
Hyva_Pos::staff_manage_edit |
Clear a login lockout early |
GET |
/V1/pos/roles · /:roleId |
Hyva_Pos::staff_roles |
List (searchCriteria) and fetch roles |
POST / PUT / DELETE |
/V1/pos/roles · /:roleId |
Hyva_Pos::staff_roles_manage |
Create / update / delete |
Staff
Create and update take the entity wrapped in a staff key:
{
"staff": {
"first_name": "Anna",
"last_name": "de Vries",
"email": "anna@example.com",
"passcode": "482913",
"role_id": 2,
"all_locations": false,
"is_active": true
}
}
passcodeis write-only: it is bcrypt-hashed on save, and the data interface has no getter, so no read endpoint returns it. The staff rows in the terminal config payload strip the hash too.staff_codeis a server-generated 5-character identifier used for receipt-side staff identification (when the location's staff-name format isstaff_id); it is stable for the lifetime of the row.all_locations: truebypasses the per-location assignments below.
Errors: 404 unknown staff id, 400 on failed saves.
Passcode Watermark
Every passcode change stamps passcode_updated_at (UTC). Devices cache credentials so cashiers can sign in offline; they store this watermark alongside the cached passcode and compare it against the value in each synced staff row. A newer server-side watermark invalidates the cache and forces one online re-authentication. If you change passcodes through the API, do not set passcode_updated_at yourself; the module maintains it.
Location Assignments
GET /V1/pos/staff/:staffId/locations returns the assigned location rows. Assign with POST /V1/pos/staff/:staffId/locations and body { "locationId": 1 }; unassign with DELETE /V1/pos/staff/:staffId/locations/:locationId. Both return true. A staff member appears in a terminal's config payload when they are assigned to its location or have all_locations.
Authentication (POST /V1/pos/staff/authenticate)
Called by the register when a cashier signs in with email + passcode (the terminalId records which register the session belongs to):
On success the server creates a session row and returns it with the staff record and role embedded:
{
"session_id": 918,
"staff_id": 7,
"staff": {
"staff_id": 7,
"staff_code": "K4T2N",
"first_name": "Anna",
"last_name": "de Vries",
"email": "anna@example.com",
"passcode_updated_at": "2026-08-01 10:22:35",
"role_id": 2,
"all_locations": false,
"is_active": true
},
"role": { "role_id": 2, "name": "Cashier", "permissions": "{\"place_orders\":true,\"accept_cash\":true}", "is_active": true },
"token": "wJd8...64-character-random-string...Qz",
"expires_at": "2026-09-11 09:14:02"
}
Tokens are 64 random characters and expire after 30 days. Failures return 401 with Invalid credentials., the same message for unknown email and wrong passcode.
Login Lockout
Failed attempts are counted per email (the counter resets after 24 hours) with a graduated cooldown:
| Cumulative failures | Cooldown |
|---|---|
| 3 | 15 seconds |
| 5 | 1 minute |
| 8 | 5 minutes |
| 12 | 30 minutes |
During a cooldown, authenticate returns 401 with Too many failed attempts. Please wait %1 or ask a manager to unlock your account. A manager clears the counter early via POST /V1/pos/staff/:staffId/unlock (empty body; the id in the path is enough), which returns true, or false when the staff id is unknown. A successful login also resets the counter.
Session Validation (POST /V1/pos/staff/session/validate)
Called by the register when it comes back online or unlocks, to confirm a cached session still stands. Body: { "token": "wJd8...Qz" }. Returns the same session shape as authenticate, with the staff and role freshly loaded, so a role change takes effect on the next validation. Errors: 401 with Invalid session token., Session has expired., or Staff member not found..
Roles
A role is a name plus a permissions JSON string of boolean keys. Create and update take the entity wrapped in a role key:
{
"role": {
"name": "Cashier",
"permissions": "{\"place_orders\":true,\"apply_discounts\":false,\"accept_cash\":true,\"accept_card\":true,\"issue_refunds\":false}",
"is_active": true
}
}
Note that permissions is a JSON string, not a nested object. Keys the app understands, grouped as in the admin role form:
- Sales:
place_orders,apply_discounts,apply_coupons,custom_line_items,issue_refunds,approve_rma,void_orders,price_override,tax_exempt_sales,reverse_charge - Payments:
accept_cash,accept_card,accept_split_payment,accept_store_credit,pay_by_invoice - Register:
open_register,close_register,cash_in_out,view_register_history - Customers:
view_customers,create_customers,edit_customers,assign_customer_group - Catalog:
view_products,view_stock_levels,manage_stock,assign_source_to_product,toggle_ship_together,edit_dashboard - Reporting:
view_reports,view_daily_summary,export_reports - Staff administration:
view_users,manage_users,assign_roles,view_audit_log - Settings:
access_settings,settings_view_all,settings_license,settings_scanner,settings_printers,settings_payment_terminal,settings_customer_display,settings_self_checkout,settings_appearance - Hardware:
open_cash_drawer,print_receipts,print_barcode_labels - Held transactions:
hold_cart,recall_any_cart
access_settings makes every section of Settings editable and settings_view_all makes every section visible, read-only where the role has no edit right. The seven settings_* section keys each open one part of Settings: a role with settings_printers alone sees the Settings menu with the printer sections in it and nothing else. settings_license and settings_payment_terminal are partial: the first shows the License & Subscription and terminal information and allows Reload but not Unregister or Reset Licence Clock, the second shows the payment settings with only the terminal selection and pairing editable.
configure_hardware is no longer offered on the role form. App versions released before the settings_* keys still read it from role blobs that carry it, where it stands for the printer, barcode scanner and terminal selection screens; a role saved through the current form no longer carries it, and an older app then applies its own role default for hardware configuration (managers and admins on, cashiers off), and a role that had it was migrated to settings_scanner, settings_printers and settings_payment_terminal when the module was upgraded.
Missing keys are treated as denied. Permissions are enforced by the app per signed-in cashier; they do not restrict this REST API, which is governed by the Magento ACL resources above. Errors: 404 unknown role id, 400 on failed saves.