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.

Tokens issued by /api/login do not expire automatically. Call /api/logout (or tokens()->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:

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

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 sections is 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.

mToken is single-use. It is cleared on the first successful exchange, so it cannot be replayed. A fresh mToken is 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/logintoken, 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


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:


Generated 2026-06-06 05:32 · ImpAcc User Profile API