XAuth client API
Everything an app asks the XAuth console after sign-in: its people, branches, roles, subscription and
account actions. Each call is one method in vsd/xauth-client (PHP) and one in
@vsd/xauth-client-next (TypeScript), over the same endpoint.
You call methods, not URLs. Each package keeps a table of endpoints from a short
key to a path: branches_lookup → /api/xauth/branches/lookup. The key is the
path after /api/xauth/ with / and - turned into _,
so it is a valid PHP array key and a plain TypeScript property. You only see a key when you override a path or
use the raw call(); you only see a path in network traffic or when calling from another stack.
How every call works
| Part | What it is |
|---|---|
| Request | POST {XAUTH_BASE_URL}/api/xauth/… with a JSON body |
| The app's credential | Headers on every call: X-Project-Id + X-Project-Secret (+ X-App-Id), or X-App-Id + X-App-Secret. The package adds them from .env |
| The answer | HTTP 200 with {status, status_code, error_message, data}. Success is status_code: 200 in the body — HTTP 200 alone means nothing |
| Engine unreachable | TypeScript: a failure with status_code: 503. PHP: a transport error result. Lists answer [], single rows null |
Two kinds of authority
| Authorised by | What the engine decides | Examples |
|---|---|---|
| App credential | Reads are limited to the subscriptions your project is entitled to. Another tenant's row answers
exactly like a missing one — an empty list or null — so the answer never confirms it exists |
branchLookup, roleByName, usersByClass, userIdentity |
| Actor token (the signed-in user's bearer) | The engine judges the person: their subscription, their rank, the action's permission. Subscription comes from the token, never from the body | roster, userBranches, userSetStatus, userAddRole, userSetPassword |
In PHP, AuthGate takes the actor token from the session for you, so those methods have no token argument.
In TypeScript you pass it: await xauth.token() in Next.js, @XauthToken() in NestJS.
Getting a client
use Vsd\XauthClient\Auth\AuthGate;
$ss = AuthGate::user(); // the signed-in snapshot
$subsId = $ss->subs_id; // hex string
import { createXauth } from '@vsd/xauth-client-next/core'
import { xauth } from '@/lib/xauth' // createNextXauth()
const engine = createXauth().engine
const user = await xauth.user()
const token = await xauth.token() // the actor token
const subsId = user?.subs_id ?? ''
import { Controller, Get, Inject, Query } from '@nestjs/common'
import { XAUTH_GATEWAY, XauthToken, XauthUser } from '@vsd/xauth-client-next/nest'
import type { XauthGateway } from '@vsd/xauth-client-next/core'
@Controller('staff')
export class StaffController {
constructor(@Inject(XAUTH_GATEWAY) private readonly xauth: XauthGateway) {}
@Get()
list(@XauthToken() token: string, @Query('search') search?: string) {
return this.xauth.engine.roster(token, { search, page: 1, per_page: 50 })
}
}
Never call the console from the browser. The credential is a server secret. A browser screen asks your own back end, and your back end makes these calls.
People: the roster
users/roster is the list behind a staff or member screen: one page of the people the signed-in user
administers, each with their primary role, their branches, and whether the user may act on them. The engine applies
three rules nobody can switch off: only the actor's own subscription; never platform identities; the subscription root
and commercial owner only for someone with commercial authority.
$page = AuthGate::userRoster([
'search' => 'dara', // login name, full name or phone; case-insensitive
'user_class' => ['member', 'admin'], // one class, a comma list, or an array
'branch_ids' => [3, 5], // optional; can only NARROW
'page' => 1,
'per_page' => 50, // 1..200, default 20
]);
foreach ($page['data'] ?? [] as $row) {
echo $row['local_login_name'], ' ', $row['role_name'], $row['can_manage'] ? ' (can manage)' : '', PHP_EOL;
}
const page = await engine.roster(token, {
search: 'dara',
user_class: ['member', 'admin'],
branch_ids: [3, 5],
page: 1,
per_page: 50,
})
if (page.status_code !== 200) {
// unreachable or refused - show an error, NOT "no users"
throw new Error(page.error_message)
}
page.data.forEach((row) => console.log(row.local_login_name, row.role_name, row.can_manage))
{
"data": [
{
"id": 42, "login_name": "wis-dara", "local_login_name": "dara",
"full_name": "Dara Sok", "phone_number": "012345678", "email": null,
"user_class": "admin", "status": 1, "is_locked": 0, "identity_type": null,
"official_id": null, "official_code": null,
"role_id": 7, "role_name": "Branch Manager", "authority_rank": 40,
"branches": [{ "id": 3, "name": "Toul Kork" }],
"can_manage": true
}
],
"total": 1, "per_page": 50, "current_page": 1, "last_page": 1
}
- Show
local_login_name, notlogin_name— the stored name may carry the subscription code. can_manageis for the screen only. Offer edit actions when it is true; the engine judges every change again anyway.- Narrow with
branch_ids, never by filtering the page afterwards. The engine narrows before paging, so pages stay full and the total stays right. - TypeScript returns a failure, not an empty page, when it cannot answer. Check
status_codefirst.
Branches
In the tables below, PHP calls are on AuthGate:: and TS calls on engine.
| You want | Call | Returns |
|---|---|---|
| A picker, or labels for ids | PHP branchLookup($subsId, $ids, true)TS branchLookup(subsId, ids, true) | [{id, name, shortcut}] |
| Just the ids | PHP branchIdsBySubs($subsId)TS branchIdsBySubs(subsId) | [1, 2, 3] |
| A letterhead or report header | PHP branchDetails($id)TS branchDetails(id) | name, name_kh, address, phones, email, website, contact person, logo |
| Which branches a person holds (actor token) | PHP userBranches($userId)TS — (use call('users_branches', …)) | [{id, name}] |
| Grant or withdraw one branch (actor token) | PHP userSetBranch($userId, $branchId, true)TS — (use call('users_set_branch', …)) | result |
// every branch of the subscription, by name
$branches = AuthGate::branchLookup($subsId, null, true);
// only some branches - e.g. to label rows that carry branch_id
$labels = collect(AuthGate::branchLookup($subsId, [3, 5]))
->mapWithKeys(fn ($b) => [(int) data_get($b, 'id') => data_get($b, 'name')]);
$ids = AuthGate::branchIdsBySubs($subsId); // [3, 5, 9]
$header = AuthGate::branchDetails(3); // object or null
const branches = await engine.branchLookup(subsId, null, true) // [] on failure
const labels = new Map(branches.map((b) => [Number(b.id), String(b.name)]))
const ids = await engine.branchIdsBySubs(subsId)
const header = await engine.branchDetails(3) // null when absent or out of scope
Branch numbers. On a project that uses branch_key = 'no', a branch's id in every
answer is its number within its subscription, and each row also carries subs_id. Numbers repeat across
subscriptions, so store the pair. Store whatever you are given as your branch key; never translate it.
Roles
| You want | Call | Returns |
|---|---|---|
| A role picker: only roles this admin may assign | PHP roleOptions($subsId, null, null, 500, true)TS roleOptions(subsId, { token }) | Role rows, in console order |
| Every role of the subscription | PHP roleOptions($subsId)TS roleOptions(subsId) | Role rows |
| One role by name | PHP roleByName('Teacher', $subsId, true)TS roleByName('Teacher', subsId, true) | {id, name} or null |
| Who has a role (rows) | PHP roleMembers($roleId, true)TS roleMembers(roleId, true) | [{id, official_id, full_name, user_class}] |
| Who has a role (ids, by name) | PHP roleUserIdsByName('Teacher', $subsId)TS roleUserIdsByName('Teacher', subsId) | [12, 15] |
A role row is {id, name, user_class, code, authority_rank}.
// the options of an "assign role" select: grantable only (the last argument)
$roles = AuthGate::roleOptions($subsId, 'admin', null, 500, true);
$teacher = AuthGate::roleByName('Teacher', $subsId, true); // case-insensitive
$members = $teacher ? AuthGate::roleMembers((int) $teacher->id) : [];
const roles = await engine.roleOptions(subsId, { token, user_class: 'admin', search: 'man', limit: 100 })
const teacher = await engine.roleByName('Teacher', subsId, true)
const members = teacher ? await engine.roleMembers(teacher.id) : []
Pass the actor token (TypeScript) or true as the last argument (PHP) whenever the list feeds an assign-role
picker. Without it you get every role, and the engine will refuse the ones the admin cannot grant — after the user picked one.
Other user reads
// every user of a class - for backend jobs; no paging, no role, no branches
$drivers = AuthGate::usersByClass('driver', $subsId, 'id,full_name,phone_number');
// one user by a few allowed columns
$u = AuthGate::userIdentity(['login_name' => 'dara'], 'id,full_name,user_class');
// one value
$phone = AuthGate::userValueBy(['id' => 42], 'phone_number');
// does a login name exist (sign-up forms)
$taken = AuthGate::loginNameExists('dara');
const drivers = await engine.usersByClass('driver', subsId, 'id,full_name,phone_number')
const u = await engine.userIdentity({ login_name: 'dara' }, 'id,full_name,user_class')
const phone = await engine.userValueBy<string>({ id: 42 }, 'phone_number')
const taken = await engine.loginNameExists('dara')
- A screen uses the roster; a job uses
usersByClass. The roster pages and judges;usersByClassreturns bare rows. - Columns are limited.
usersByClassreturns only theum_userscolumns the console lets your project read, anduserIdentity/userValueByaccept a small allow-list. A password hash or OTP never comes back. - For joins against your own tables, use the mirror, not a call per row.
Endpoint reference
Every endpoint both packages know. Credential = the app credential alone; Actor = the
signed-in user's token as well; Pre-auth = nobody is signed in (the credential still applies). Token = the call is about the token it carries: checking, refreshing or revoking it.
In Method, PHP means AuthGate:: and TS means engine.; one name marked PHP & TS is the same in both. A dash means that package has no method yet — use the raw call shown under Raw calls.
Session
| Endpoint | Auth | Method | Purpose |
|---|---|---|---|
login/api/xauth/login | Pre-auth | PHP verifyUserTS login | Web sign-in; returns tokens and the snapshot |
mobile_login/api/xauth/mobile/login | Pre-auth | mobileLogin PHP & TS | Mobile sign-in; the user carries firebase_topics |
introspect/api/xauth/introspect | Token | PHP authenticateTokenTS introspect | Turn a bearer into a user. 401 = refused, 503 = not judged |
refresh/api/xauth/refresh | Token | PHP automatic TS refreshToken | Swap a refresh token for a new pair; both rotate |
logout/api/xauth/logout | Token | revokeToken PHP & TS | Revoke; an unknown token is not an error |
sso_exchange/api/xauth/sso/exchange | Credential | PHP redeemSsoCodeTS ssoExchange | Redeem the one-time code from a cross-domain sign-in |
social_providers/api/xauth/social/providers | Pre-auth | PHP XAuthClient::socialProvidersTS socialProviders | Which external sign-in buttons to draw |
reports, reports_by_category/api/xauth/reports[/by-category] | Actor | reportCatalog PHP & TS | The reports the user may open, flat or grouped |
Users
| Endpoint | Auth | Method | Purpose |
|---|---|---|---|
users_roster/api/xauth/users/roster | Actor | PHP userRosterTS roster | One page of the people the actor administers, with role, branches, can_manage |
users_by_class/api/xauth/users/by-class | Credential | usersByClass PHP & TS | All users of a class; for jobs |
users_identity/api/xauth/users/identity | Credential | userIdentity PHP & TS | One user by allowed columns |
users_value_by/api/xauth/users/value-by | Credential | userValueBy PHP & TS | One column of one user |
users_special_details/api/xauth/users/special-details | Credential | userSpecialDetails PHP & TS | Account details by login name |
users_enrich/api/xauth/users/enrich | Credential | userEnrich PHP & TS | Profile fields for one user and app |
users_mobile_data/api/xauth/users/mobile-data | Credential | PHP userMobileDataTS — | What a mobile app needs about a user |
users_profile_photo/api/xauth/users/profile-photo | Credential | userProfilePhoto PHP & TS | The photo URL |
users_login_name_exists/api/xauth/users/login-name-exists | Pre-auth | loginNameExists PHP & TS | Is a login name taken |
users_register/api/xauth/users/register | Pre-auth | userRegister PHP & TS | Self-registration, where the class allows it |
users_create_identity/api/xauth/users/create-identity | Actor | PHP userCreateIdentityTS — | An admin creates someone; rank and permission are checked |
users_update/api/xauth/users/update | Credential | userUpdate PHP & TS | Update profile fields |
users_set_status/api/xauth/users/set-status | Actor | userSetStatus PHP & TS | Activate, suspend |
users_add_role/api/xauth/users/add-role | Actor | userAddRole PHP & TS | Give a user a role |
users_branches/api/xauth/users/branches | Actor | PHP userBranchesTS — | Branches a user holds |
users_set_branch/api/xauth/users/set-branch | Actor | PHP userSetBranchTS — | Grant or withdraw one branch |
users_set_password/api/xauth/users/set-password | Actor | userSetPassword PHP & TS | An admin sets someone's password |
users_change_password/api/xauth/users/change-password | Actor | userChangePassword PHP & TS | Change your own password |
users_deactivate_self/api/xauth/users/deactivate-self | Actor | userDeactivateSelf PHP & TS | Close your own account |
users_delete_identity/api/xauth/users/delete-identity | Actor | userDeleteIdentity PHP & TS | Delete an identity |
users_generate_otp/api/xauth/users/generate-otp | Pre-auth | userGenerateOtp PHP & TS | Create and send an OTP — do not send your own SMS too |
users_match_otp/api/xauth/users/match-otp | Pre-auth | userMatchOtp PHP & TS | Check an OTP |
users_activate_by_otp/api/xauth/users/activate-by-otp | Pre-auth | userActivateByOtp PHP & TS | Activate a new account with its OTP |
users_reset_password_by_otp/api/xauth/users/reset-password-by-otp | Pre-auth | userResetPasswordByOtp PHP & TS | Forgot-password; the OTP authorises it |
users_enter_linked_identity/api/xauth/users/enter-linked-identity | Actor | PHP enterLinkedIdentityTS — | Switch to a linked identity in another app |
Branches, roles, subscription, apps
| Endpoint | Auth | Method | Purpose |
|---|---|---|---|
branches_lookup/api/xauth/branches/lookup | Credential | branchLookup PHP & TS | Branches of a subscription, optionally by ids, by name |
branches_ids_by_subs/api/xauth/branches/ids-by-subs | Credential | branchIdsBySubs PHP & TS | The branch ids of a subscription |
branches_details/api/xauth/branches/details | Credential | branchDetails PHP & TS | One branch with its contact block |
roles_options/api/xauth/roles/options | Credential or Actor | roleOptions PHP & TS | Roles for a picker; grantable only with the actor |
roles_by_name/api/xauth/roles/by-name | Credential | roleByName PHP & TS | One role by name |
roles_members/api/xauth/roles/members | Credential | roleMembers PHP & TS | The people holding a role |
roles_user_ids_by_name/api/xauth/roles/user-ids-by-name | Credential | roleUserIdsByName PHP & TS | User ids holding a role |
subscriptions_default/api/xauth/subscriptions/default | Credential | subscriptionDefault PHP & TS | This app's default subscription |
subscriptions_by_app/api/xauth/subscriptions/by-app | Credential | PHP subscriptionsByAppTS — | Subscriptions entitled to an app — for a pre-login screen |
subscriptions_customer_id/api/xauth/subscriptions/customer-id | Credential | subscriptionCustomerId PHP & TS | The subscriber behind a subscription |
subscriptions_signup_plans, subscriptions_signup/api/xauth/subscriptions/signup[-plans] | Pre-auth | PHP — TS signupPlans, signup | Self-serve signup of a whole new tenant |
subscribers_details/api/xauth/subscribers/details | Credential | customerDetails PHP & TS | The subscriber's details |
subscribers_upsert/api/xauth/subscribers/upsert | Actor | customerUpsert PHP & TS | Edit the subscriber's details |
subscribers_logo_*/api/xauth/subscribers/logo/{save,url,delete} | Actor | PHP customerLogoSave, …Url, …DeleteTS customerLogoSave, …Url, …Delete | The subscriber's logo |
entitlements_offers, entitlements_activate/api/xauth/entitlements/{offers,activate} | Actor | PHP activateFreeTrialTS trialOffers, activateTrial | Free-trial offers and activation, by feature code or module ref_code |
apps_list/api/xauth/apps/list | Credential | appList PHP & TS | Applications |
apps_by_route/api/xauth/apps/by-route | Credential | PHP — (getApplicationByRoute answers from appList)TS appByRoute | The app that owns a route |
The capability endpoints (jwks, client/user-snapshot, client/authority-events,
subscriber/token) are used by /verify, and the mirror/* endpoints by the
mirror. You do not call them yourself.
Raw calls
For an endpoint a package has no method for yet, call it by key. The credential, retries and the response unwrapping still apply:
const res = await engine.call<Array<{ id: number; name: string }>>('users_branches', { user_id: 42, token })
// null = engine unreachable; otherwise { status_code, error_message, data }
const branches = res?.status_code === 200 ? res.data ?? [] : []
From any other stack — Python, Go, a quick test — it is a plain JSON POST:
curl -s -X POST "$XAUTH_BASE_URL/api/xauth/branches/lookup" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-H "X-App-Id: $XAUTH_APP_ID" -H "X-App-Secret: $XAUTH_APP_SECRET" \
-d '{"subs_id": "'"$SUBS_ID"'", "order_by_name": true}'
curl -s -X POST "$XAUTH_BASE_URL/api/xauth/users/roster" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-H "X-App-Id: $XAUTH_APP_ID" -H "X-App-Secret: $XAUTH_APP_SECRET" \
-d '{"token": "'"$USER_TOKEN"'", "search": "dara", "page": 1, "per_page": 20}'
With a project credential, send X-Project-Id and X-Project-Secret (and X-App-Id
for the entry app) instead of the app pair.
Overriding a path
Only needed if a console serves an endpoint somewhere else. Override by key; every method keeps working.
'endpoints' => [
// ...the published defaults, then:
'branches_lookup' => '/api/xauth/v2/branches/lookup',
],
Laravel merges config shallowly, so a published endpoints array replaces the default one
wholesale. Keep every key you did not change, or those calls fail with "no path configured for endpoint".
import { createXauth } from '@vsd/xauth-client-next/core'
const xauth = createXauth({ endpoints: { branches_lookup: '/api/xauth/v2/branches/lookup' } })