Skip to content

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
  }
}
  • passcode is 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_code is a server-generated 5-character identifier used for receipt-side staff identification (when the location's staff-name format is staff_id); it is stable for the lifetime of the row.
  • all_locations: true bypasses 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):

{ "email": "anna@example.com", "passcode": "482913", "terminalId": 3 }

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.