User Profile API — Developer Manual
This API lets a third-party application authenticate a user against the ImpAcc user database and read that user's profile. It uses Laravel Sanctum bearer tokens.
- Base URL:
https://usr.impacc.work/api - Auth scheme: Bearer token (issued by
POST /api/login) - Request format:
application/json - Response format:
application/json - Time zone of timestamps: server local time, ISO‑8601 strings
Tokens issued by
/api/logindo not expire automatically. Call/api/logout(ortokens()->delete()on the server) to revoke a token.
1. Authentication flow
┌──────────────┐ 1. POST /api/login ┌────────────────┐
│ │ citizen_id + password │ │
│ Your app │ ─────────────────────► │ Profile API │
│ (3rd party) │ ◄───────────────────── │ (usr.impacc) │
│ │ bearer token │ │
└──────────────┘ └────────────────┘
│ 2. Store token (memory / secure storage)
│
│ 3. Every subsequent call carries the header:
│ Authorization: Bearer <token>
▼
GET /api/profile
GET /api/permissions
POST /api/logout
Two ways to obtain the bearer token:
POST /api/login— withcitizen_id+password(§2.1).POST /api/sso/exchange— with a one-timemTokenissued by the imPACC portal, for apps launched via Single Sign-On (§2.5). No password handled.
Both return the same { token, token_type, user } payload; everything after
step 2 is identical.
Required request headers
| Header | Value | When |
|---|---|---|
Accept |
application/json |
always |
Content-Type |
application/json |
on POST/PUT |
Authorization |
Bearer <token> |
on protected EPs |
If Accept: application/json is missing, Laravel may return HTML
redirects instead of JSON on auth failures. Always send it.
2. Endpoints
2.1 POST /api/login
Exchange credentials for a bearer token.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
citizen_id |
string | yes | The user's 13‑digit citizen ID. |
password |
string | yes | The user's plain‑text password (sent over HTTPS). |
device_name |
string | no | Label saved with the token, e.g. "mobile-app-ios". |
Example
curl -X POST https://usr.impacc.work/api/login \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"citizen_id": "1234567890123",
"password": "user-secret",
"device_name": "partner-app-web"
}'
Success — 200 OK
{
"token": "12|H7c0gQ9b3l...redacted...XyZ",
"token_type": "Bearer",
"user": {
"id": 1234,
"citizen_id": "1234567890123",
"title": "นาย",
"firstname": "สมชาย",
"lastname": "ใจดี",
"title_english": "Mr.",
"firstname_english": "Somchai",
"lastname_english": "Jaidee",
"email": "somchai@example.com",
"mobile": "0812345678",
"born_date": "1990-05-12",
"workgroup": "IT",
"workgroup_id": 39,
"division_id": 2,
"organization": "Impacc",
"role_type1": "officer",
"role_type2": null,
"role_type3": null,
"status": "1",
"roles": ["user"],
"last_login": "2026-05-19T08:42:11.000000Z",
"current_profile": { "...": "see profiles[] entry below" },
"profiles": [
{
"id": 1,
"label": "ต้นสังกัด",
"kind": "home",
"dept1": "สำนักงาน ป.ป.ท.",
"dept2": "สำนักงาน ป.ป.ท.",
"dept3": null,
"officer_type_id": 4,
"officer_type_name": "ทั่วไป",
"position_type_id": 3,
"position_type_name": "วิชาการ",
"position": "นักวิชาการ",
"description": null,
"level": "ปฏิบัติการ",
"management_position": null,
"is_default": true,
"is_active": true,
"is_temporary": false,
"mission_title": null,
"mission_note": null,
"mission_start_date": null,
"mission_end_date": null,
"mission_order_no": null,
"mission_order_file": null,
"status": "approved",
"approved_at": "2024-07-10T08:00:00.000000Z"
},
{
"id": 4,
"label": "คณะทำงานพิเศษ",
"kind": "mission",
"dept1": "รองเลขาธิการ ป.ป.ท. (คนที่ 1)",
"dept2": "งานรองเลขาธิการ ป.ป.ท. (คนที่ 1)",
"dept3": null,
"officer_type_id": 2,
"officer_type_name": "เจ้าหน้าที่ ป.ป.ท.",
"position_type_id": null,
"position_type_name": null,
"position": null,
"description": null,
"level": null,
"management_position": null,
"is_default": false,
"is_active": false,
"is_temporary": true,
"mission_title": "สืบสวน",
"mission_note": "ช่วยราชการคณะทำงาน X",
"mission_start_date": "2026-01-01",
"mission_end_date": "2026-06-30",
"mission_order_no": "123/2569",
"mission_order_file": "orders/2569-123.pdf",
"status": "approved",
"approved_at": "2025-12-20T03:00:00.000000Z"
}
]
}
}
profiles[] is ordered with the currently active profile first, then the
default, then the rest. current_profile is a convenience pointer to
the same row a consumer would otherwise have to find by scanning
profiles[] for is_active=true (with is_default=true as a
fallback). If the user has no rows in user_profiles, both
current_profile is null and profiles[] is empty.
The user object also carries a permissions block — the user's
effective applications, menus and sections. Its structure is documented
under §2.4 GET /api/permissions; it is
omitted from the example above for brevity but is always present.
Store the token securely on the client. Send it on every later call
as Authorization: Bearer <token>.
Failure — 422 Unprocessable Entity (wrong credentials or inactive
account)
{
"message": "The provided credentials are incorrect.",
"errors": {
"citizen_id": ["The provided credentials are incorrect."]
}
}
Failure — 422 Unprocessable Entity (missing fields)
{
"message": "The citizen id field is required. (and 1 more error)",
"errors": {
"citizen_id": ["The citizen id field is required."],
"password": ["The password field is required."]
}
}
2.2 GET /api/profile
Return the authenticated user's profile. The user object is identical
to the one returned by POST /api/login — it includes current_profile,
the full profiles[] array, and the permissions block (see §2.4). The
example below shows only the user-level fields for brevity.
Headers
Accept: application/json
Authorization: Bearer <token>
Example
curl https://usr.impacc.work/api/profile \
-H "Accept: application/json" \
-H "Authorization: Bearer 12|H7c0gQ9b3l...XyZ"
Success — 200 OK
{
"user": {
"id": 1234,
"citizen_id": "1234567890123",
"title": "นาย",
"firstname": "สมชาย",
"lastname": "ใจดี",
"title_english": "Mr.",
"firstname_english": "Somchai",
"lastname_english": "Jaidee",
"email": "somchai@example.com",
"mobile": "0812345678",
"born_date": "1990-05-12",
"workgroup": "IT",
"workgroup_id": 39,
"division_id": 2,
"organization": "Impacc",
"role_type1": "officer",
"role_type2": null,
"role_type3": null,
"status": "1",
"roles": ["user"],
"last_login": "2026-05-19T08:42:11.000000Z"
}
}
Failure — 401 Unauthenticated (missing, invalid, or revoked token)
{ "message": "Unauthenticated." }
2.3 POST /api/logout
Revoke the bearer token used on this request. After this call, the token can no longer be used.
Headers
Accept: application/json
Authorization: Bearer <token>
Example
curl -X POST https://usr.impacc.work/api/logout \
-H "Accept: application/json" \
-H "Authorization: Bearer 12|H7c0gQ9b3l...XyZ"
Success — 200 OK
{ "message": "Logged out" }
Failure — 401 Unauthenticated
{ "message": "Unauthenticated." }
2.4 GET /api/permissions
Return the authenticated user's effective access across the three-tier
permission model: application → menu → section. The same object is
also embedded as user.permissions in the login and profile
responses; this endpoint returns it on its own when that is all you need.
How access is resolved
A permission is effective for a user when it is granted at that tier and not blocked:
effective(tier) = ( grants from the user's groups ) ← groups come from BOTH
∪ ( the user's direct grants ) users.workgroup_id → workgroup_groups
∖ ( the user's blocks ) AND direct user_groups membership
- Groups are the union of workgroup-derived groups (
workgroup_groupsviausers.workgroup_id) and direct memberships (user_groups). - Grants come from
group_*_permissions(per group) anduser_*_permissions(per user). - Blocks (
user_application_blocks,user_menu_blocks) remove an application/menu for that user even if a group would grant it. The section tier has no block table.
Menus are nested under an application only when that application is itself effective; sections are nested under an effective menu likewise.
The section tier exists in the schema but currently holds no data, so
sectionsis presently[]for every menu. It will populate automatically once section permissions are assigned — no client change needed.
Headers
Accept: application/json
Authorization: Bearer <token>
Example
curl https://usr.impacc.work/api/permissions \
-H "Accept: application/json" \
-H "Authorization: Bearer 12|H7c0gQ9b3l...XyZ"
Success — 200 OK
{
"permissions": {
"group_ids": [1, 2, 3, 22, 23],
"applications": [
{
"id": 22,
"app_id": "11-22",
"name": "ระบบDIDC",
"link": "/miniapp/didc",
"menus": [
{
"id": 81,
"name": "จัดการ Dashboard",
"path": "/dashboard-management",
"level": 2,
"parent": 75,
"sections": []
}
]
},
{
"id": 13,
"app_id": "11-13",
"name": "ระบบประชาสัมพันธ์-ภายนอก",
"link": "/miniapp/xcms",
"menus": []
}
]
}
}
An application with no effective menus returns "menus": [] (the user can
reach the app but no specific menu has been granted/resolved under it).
Failure — 401 Unauthenticated
{ "message": "Unauthenticated." }
2.5 POST /api/sso/exchange
Single Sign-On token exchange. This is an alternative to POST /api/login
for callers that are launched from the imPACC portal (impacc.work) instead
of collecting a password.
When a user already logged into the portal opens a linked app, the portal
mints a one-time mToken on that user's record and redirects the browser
to the app with it (e.g. https://your-app?appId=...&mToken=...). The app
posts that mToken here to obtain a bearer token and the user's profile —
without ever handling the user's credentials.
mTokenis single-use. It is cleared on the first successful exchange, so it cannot be replayed. A freshmTokenis minted by the portal on each launch. Exchange it promptly.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
mToken |
string | yes | The one-time token from the portal redirect. |
device_name |
string | no | Label saved with the issued token, e.g. "sso-web". |
Example
curl -X POST https://usr.impacc.work/api/sso/exchange \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{ "mToken": "0f3a…64-char-token…d9", "device_name": "sso-web" }'
Success — 200 OK
Identical shape to POST /api/login — token, token_type, and the full
user object (including current_profile, profiles[], and the
permissions block from §2.4):
{
"token": "9|iU7SMJ2TjJ7feNnG…redacted…",
"token_type": "Bearer",
"user": { "id": 1, "citizen_id": "…", "permissions": { "…": "see §2.4" } }
}
Failure — 422 Unprocessable Entity (unknown, already-used, or expired
mToken, or an inactive account)
{
"message": "Invalid or expired SSO token.",
"errors": { "mToken": ["Invalid or expired SSO token."] }
}
After exchange, treat the returned token exactly like one from /login:
send it as Authorization: Bearer <token> on every subsequent call, and
call POST /api/logout to revoke it.
3. Profile field reference
3.1 User-level fields
| Field | Type | Description |
|---|---|---|
id |
integer | Internal user ID. |
citizen_id |
string | 13‑digit citizen ID. Primary external identifier. |
title |
string | Thai title (นาย / นาง / นางสาว…). |
firstname |
string | Thai first name. |
lastname |
string | Thai last name. |
title_english |
string | English title (Mr./Mrs./Ms.). |
firstname_english |
string | English first name. |
lastname_english |
string | English last name. |
email |
string | Email address. |
mobile |
string | Mobile phone number. |
born_date |
date | YYYY-MM-DD. |
workgroup |
string | Work group / team name on the user row. |
workgroup_id |
integer/null | FK to workgroups. Drives group/permission resolution (see §2.4). |
division_id |
integer/null | FK to divisions — the user's organizational division. |
organization |
string | Organization name on the user row. |
role_type1..3 |
string | Role classifiers (free form, project specific). |
status |
string | "1" = active. Other values are inactive. |
roles |
string[] | Spatie permission role names assigned. |
last_login |
datetime | ISO‑8601 UTC of the most recent login. |
current_profile |
object/null | Convenience pointer — same shape as a profiles[] item. The user's currently active profile, falling back to the default. |
profiles[] |
object[] | Every OU / mission assignment the user has. See §3.2. |
permissions |
object | The user's effective applications/menus/sections. See §2.4 and §3.3. |
password, remember_token, and uploaded user-file paths are
intentionally not returned.
3.2 Profile (OU / mission) fields
Each entry in profiles[] represents one organizational-unit assignment
for the user. A user always has a "home" OU (is_default=true), may
have one or more secondary OUs, and may have one or more special-mission
OUs (is_temporary=true with the mission_* fields populated).
| Field | Type | Description |
|---|---|---|
id |
integer | Internal profile row ID. Stable across requests. |
label |
string | Display label ("ต้นสังกัด", "สังกัดเพิ่มเติม", "ภารกิจพิเศษ", …). |
kind |
enum | Convenience classifier: "home", "secondary", or "mission". |
dept1 |
string | Top-level department (DivisionLevel1). |
dept2 |
string | Mid-level department (DivisionLevel2). |
dept3 |
string | Lowest-level department (DivisionLevel3). |
officer_type_id |
integer | FK to officer_types. |
officer_type_name |
string | Joined name from officer_types. |
position_type_id |
integer | FK to ds_positions_types. |
position_type_name |
string | Joined name from ds_positions_types. |
position |
string | Position title. |
description |
string | Free-text description (e.g., position description level). |
level |
string | Position level (e.g., "ปฏิบัติการ", "ชำนาญการ"). |
management_position |
string | Management-track position name, if applicable. |
is_default |
bool | true for the user's primary/home OU. Exactly one row per user. |
is_active |
bool | true for the OU the user is currently working under in the web app. |
is_temporary |
bool | true for special-mission OUs; the mission_* fields below will be set. |
mission_title |
string | Name of the special mission (e.g., "สืบสวน"). |
mission_note |
string | Free-text note about the mission. |
mission_start_date |
date | YYYY-MM-DD. |
mission_end_date |
date | YYYY-MM-DD. After this date the row is considered ended. |
mission_order_no |
string | Order/decree number authorizing the mission. |
mission_order_file |
string | Path to the scanned order document, relative to the file storage root. |
status |
string | "approved", "pending", "ended", etc. |
approved_at |
datetime | When this assignment was approved. |
How to interpret kind:
kind |
Condition | Meaning |
|---|---|---|
"home" |
is_default=true |
The user's primary/permanent OU. |
"mission" |
is_default=false, is_temporary=true |
Special mission, time‑bounded. |
"secondary" |
is_default=false, is_temporary=false |
Additional non-mission assignment. |
Consumers that only need the user's main OU can use current_profile
directly. Consumers that present a list (e.g., "switch active OU"
menus) should iterate profiles[].
3.3 Permissions fields
The permissions object (returned standalone by §2.4 and embedded in
user.permissions) has this shape:
| Field | Type | Description |
|---|---|---|
group_ids |
int[] | Every group the user belongs to (workgroup-derived ∪ direct). |
applications[] |
object[] | Effective applications, ordered by sequence then id. |
applications[].id |
integer | Internal application ID (applications.id). |
applications[].app_id |
string | External application code (e.g. "11-22"). |
applications[].name |
string | Application display name. |
applications[].link |
string | App entry path/URL, if any. |
applications[].menus[] |
object[] | Effective menus under this application ([] if none). |
menus[].id |
integer | Internal menu ID (menu_applications.id). |
menus[].name |
string | Menu display name. |
menus[].path |
string | Route/path for the menu. |
menus[].level |
int/null | Menu depth (1 = top level). |
menus[].parent |
int/null | Parent menu ID for nesting in a sidebar. |
menus[].sections[] |
object[] | Effective sections under this menu ([] until section data exists). |
sections[].id |
integer | Internal section ID (section_menus.id). |
sections[].name |
string | Section display name. |
Only granted, non-blocked entries appear; absence of an application,
menu, or section means the user has no effective access to it. Build a
client-side "allowed paths" set from menus[].path to drive route guards.
4. HTTP status codes
| Status | Meaning |
|---|---|
| 200 | Success. |
| 401 | Token missing, invalid, or revoked. Re-login. |
| 419 | CSRF/session mismatch. You forgot Accept: application/json. |
| 422 | Validation error (bad credentials, missing fields). |
| 429 | Rate limited (default 60 req/min/IP on the api middleware). |
| 500 | Server error. Capture the response body and contact the API team. |
5. Token handling — best practices
- Never put the bearer token in a URL. Always use the header.
- Use HTTPS for every call — credentials and tokens are sent in clear.
- Store the token in secure storage (Keychain on iOS, Keystore on
Android, an
httpOnlycookie or encrypted session on the server). - Call
POST /api/logoutwhen the user signs out so the token cannot be reused if leaked. - One token per device. Pass a unique
device_nameon each login so tokens can be tracked and revoked individually if needed. - On
401, treat the session as ended — clear the stored token and send the user back to the login screen.
6. Sample integrations
6.1 JavaScript (fetch)
async function login(citizenId, password) {
const r = await fetch('https://usr.impacc.work/api/login', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
citizen_id: citizenId,
password,
device_name: 'web-partner-app',
}),
});
if (!r.ok) throw new Error(`Login failed: ${r.status}`);
return r.json(); // { token, token_type, user }
}
async function getProfile(token) {
const r = await fetch('https://usr.impacc.work/api/profile', {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
},
});
if (r.status === 401) throw new Error('Token expired');
return r.json();
}
6.2 PHP (Guzzle)
use GuzzleHttp\Client;
$http = new Client(['base_uri' => 'https://usr.impacc.work/api/']);
$login = $http->post('login', [
'headers' => ['Accept' => 'application/json'],
'json' => [
'citizen_id' => '1234567890123',
'password' => $userPassword,
'device_name' => 'partner-backend',
],
]);
$token = json_decode((string) $login->getBody(), true)['token'];
$profile = $http->get('profile', [
'headers' => [
'Accept' => 'application/json',
'Authorization' => "Bearer {$token}",
],
]);
6.3 Python (requests)
import requests
BASE = "https://usr.impacc.work/api"
def login(citizen_id, password):
r = requests.post(f"{BASE}/login", json={
"citizen_id": citizen_id,
"password": password,
"device_name": "partner-python",
}, headers={"Accept": "application/json"})
r.raise_for_status()
return r.json()["token"]
def profile(token):
r = requests.get(f"{BASE}/profile", headers={
"Accept": "application/json",
"Authorization": f"Bearer {token}",
})
r.raise_for_status()
return r.json()["user"]
7. Support
For credentials, allow‑listing, or production access, contact the ImpAcc API team. Include:
- the calling application name,
- the public IP or domain that will make the calls,
- the expected request volume.
Generated 2026-06-06 05:32 · ImpAcc User Profile API