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 ever 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, never 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, on purpose.
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,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,access_settings,view_audit_log - Hardware:
open_cash_drawer,print_receipts,print_barcode_labels,configure_hardware - Held transactions:
hold_cart,recall_any_cart
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.