Authentication and Session API
Tenant discovery, sign-in, sign-out, forgot and reset password, subscription status, roles and the user switcher - every request, response and error the POS app handles.
These endpoints take a staff member from a fresh install to a signed-in session on the right store. They feed the API Key, Login, Forgot Password and Select Store screens, the user switcher in the sidebar header, and Logout.
All paths are on {{server}} (your tenant domain) except find-domain, which is on the central cloudposai.com service.
POST https://cloudposai.com/api/v1/find-domain
Purpose: turn the company API key into the tenant domain ({{server}}).
Used by: the API Key screen on first start, and again after Reset API Key on the Login screen.
Headers
Header | Value |
|---|---|
|
|
|
|
Request body: none.
Response (200)
{
"status": 200,
"message": "Instance found",
"data": {
"domain": "https://<your-company>.cloudposai.com"
}
}Notes
- The app accepts the answer only when the HTTP status is 200 and
statusis200. domainmay come with or withouthttps://. The app addshttps://when missing and strips trailing slashes.- The key is saved as
api_keyand the domain asapp_urlon the device. Both survive restarts and are kept by Clear Local Storage. - An empty key is rejected before any call is made.
POST /api/v1/user/signin
Purpose: sign a staff member in and return the access token, role, company and store list.
Used by: the Login screen (email, password, Remember Me), and the user switcher (switching executive re-runs sign-in with the other user's credentials).
Headers
Header | Value |
|---|---|
|
|
|
|
|
|
Request body
{
"email": "<user email>",
"password": "<password>"
}The app trims spaces around the email before sending.
Response (200)
{
"status": "success",
"message": "Hi salesexecutiv2, welcome to home",
"data": {
"access_token": "<access-token>",
"token_type": "Bearer",
"name": "salesexecutiv2",
"user_id": 516,
"company_id": 2,
"company_name": "FUNZCART",
"user_role": "sales_executive",
"stores": [
{ "store_id": 2, "store_name": "متجر النجمة Store", "location": "Al Hamra", "email": null, "phone": "<phone>" },
{ "store_id": 32, "store_name": "funzcart2", "location": "Al mithnab central", "email": null, "phone": "<phone>" },
{ "store_id": 35, "store_name": "Funzcart Al Zahara", "location": "...", "email": null, "phone": "<phone>" }
],
"time_zone": "Asia/Riyadh",
"country": { "id": "<country id>", "name": "<country name>", "iso_code": "<ISO code>" },
"vat_number": "<company VAT number or null>",
"cr_number": "<company CR number or null>",
"zatca_company_name": "<ZATCA legal name or null>"
}
}Example values follow the demo story; ids and store locations are from the demo company. country is null when the company has no country set. vat_number, cr_number and zatca_company_name come from the company's ZATCA settings and are null when ZATCA is not configured.
What the app does with the response
Field | Saved as / used for |
|---|---|
| Preferences |
| Preference |
| Preferences |
| Shown in Company Info; used for realtime sync channel |
| Shown on Select Store and in the Active Store switcher |
| Business dates and receipt dates (demo: Asia/Riyadh) |
| Preference |
| Preferences |
The server Date response header is also read to store the difference between device time and server time (server_time_offset).
Landing screen by role (after store selection)
| Lands on |
|---|---|
| Attender (restaurant waiter screen) |
| Kitchen Master |
| Dashboard |
| Restaurant |
| Billing |
Errors (checked in this order)
Situation | HTTP | Body |
|---|---|---|
| 400 |
|
Unknown tenant key | 404 |
|
Wrong email or password | 401 |
|
User record not found after check | 401 |
|
User is not linked to this company | 403 |
|
User has no POS role in this company | 403 |
|
Roles allowed to sign in to the POS: sales_executive, kitchen_master, delivery_executive, attender, company_admin, purchase_manager, restaurant_sales. Roles such as customer, supplier and super_admin cannot sign in to the POS.
The app treats 2xx-4xx responses as answers and shows message. A 5xx answer shows "Login failed (HTTP 5xx). Please try again." If no API key is saved, the app asks for one before calling sign-in.
POST /api/v1/user/signout
Purpose: revoke the current access token.
Used by: Logout at the bottom of the side menu (always visible).
Headers: X-Tenant, Authorization: Bearer <access-token>, Content-Type: application/json.
Request body: none.
Response (200)
{
"status": "success",
"message": "You have successfully logged out and the token was successfully deleted"
}If no user is attached to the token the body is {"status":"error","message":"No authenticated user found"}.
Notes
- Logout is best effort: the app clears the local session even when the call fails (for example offline).
- After logout the app resets session data (token, role, active store) but keeps the API key and domain, so the next login goes straight to the Login screen.
POST /api/v1/user/forgot
Purpose: send a password reset code to the user's email.
Used by: Forgot Password on the Login screen.
Headers: Content-Type: application/json, api_key: <your-tenant-key>.
Request body
{
"email": "<user email>"
}Response: the app shows the returned message for status 200, 400 and 401. Other errors show a generic "try again" message.
POST /api/v1/user/reset
Purpose: set a new password with the code from the email.
Used by: the reset step of Forgot Password.
Headers: Content-Type: application/json, api_key: <your-tenant-key>.
Request body
{
"code": "<code from email>",
"password": "<new password>",
"password_confirmation": "<new password>"
}Response: the app shows the returned message for status 200, 400 and 401.
GET /api/v1/company/subscription-status
Purpose: confirm that the company subscription allows selling.
Used by: start-up and before order actions. The app first tries the subscription data inside the sign-in payload. If none is present it calls this endpoint. It re-checks when the last check is older than 2 minutes.
Headers: X-Tenant, Authorization, Accept: application/json.
Query parameters: none.
Response (200) - illustrative shape; the app accepts either a flat or a nested block:
{
"status": "success",
"data": {
"company_id": 2,
"subscription": {
"subscription_status": "active",
"message": "",
"valid_until": "2027-01-31",
"manage_subscription_url": "https://..."
}
}
}Accepted field names: subscription_status or status; message or subscription_message; valid_until or end_date; manage_subscription_url or management_url. The block can sit at data.subscription, data.company_subscription, subscription or company_subscription.
Statuses: active, warning, blocked, expired, suspended, inactive. Blocked, expired, suspended and inactive stop order actions (confirm, save) and show the subscription dialog with the message and a manage link.
Fallback: when the endpoint cannot be reached or is not offered by your server, the app uses the website settings COMPANY_SUBSCRIPTION_STATUS, COMPANY_SUBSCRIPTION_MESSAGE, COMPANY_SUBSCRIPTION_VALID_UNTIL and COMPANY_SUBSCRIPTION_MANAGE_URL. If neither source answers, the app shows "Unable to verify the company subscription. Check your connection and try again."
GET /api/v1/roles/list
Purpose: return every role of the company with its permission strings. The sign-in response does not include permissions; the app gets them here.
Used by: start-up sync step "permissions", the side menu, dashboard widgets and in-screen permission checks.
Query parameters
Name | Type | Required | Description |
|---|---|---|---|
| integer | no | Active store id (the app always sends it) |
| integer | no | Page number (default 1) |
Response (200) (trimmed: permission lists cut, 11 more roles)
{
"status": "success",
"message": "Roles retrieved successfully",
"data": {
"roles": [
{
"id": 20,
"name": "Attender",
"original_name": "attender",
"guard_name": "web",
"permissions_count": 51,
"permissions": ["view_order", "view_any_order", "..."],
"company_id": 2,
"company_name": "FUNZCART",
"created_at": "2025-12-09T23:40:24.000000Z",
"updated_at": "2025-12-09T23:40:24.000000Z"
},
{
"id": 12,
"name": "Company Admin",
"original_name": "company_admin",
"guard_name": "web",
"permissions_count": 544,
"permissions": ["view_bank", "view_any_bank", "..."],
"company_id": 2,
"company_name": "FUNZCART"
}
],
"pagination": {
"current_page": 1,
"last_page": 1,
"per_page": 15,
"total": 13,
"from": 1,
"to": 13
}
}
}How the app uses it
- It finds the role whose
original_namematches the signed-inuser_role. - A permission is granted only if that role's
permissions[]contains the exact string, for examplemenu.sales.orders.accessordashboard.widgets.zatca_overview.access. - Nothing permission-gated appears until this list has loaded. The list is cached on the device for offline use (Offline Data > Roles and permissions).
- Roles and their permissions are edited in the admin panel, not in the POS.
Full permission catalogue and the role matrix: Navigation and roles.
User switcher and store switcher
- User switcher (sidebar header, permission
menu.utility.user_switcher.access): lets you sign in as another staff member without logging out, by entering that user's credentials. It callsPOST /api/v1/user/signinagain. The new token and role replace the old ones; the menu reloads for the new role. - Store switcher (profile card Active Store, permission
menu.utility.store_switcher.access): no API call of its own. It changesactive_store_id, re-syncs printer defaults and reloads store-scoped data with the newstore_id.
Related
- API overview - headers, envelopes and error codes
- Settings and master data API
- Reports and day close API - pending day close check after store selection
- Getting started
- Navigation and roles
- Troubleshooting FAQ