The users module provides a light CRUD for user profiles (read + update), endpoints for reading role and permissions, and avatar upload/delete. GET /users/:user_id is polymorphic: if user_id is omitted the server returns the currently logged-in user’s data; the response shape also differs between web and mobile clients.
Property Value Base URL {HOST}/v1Auth Bearer JWT (header Authorization) or cookie access_token Content-Type application/json · avatar upload: multipart/form-dataError envelope { "message": string | string[], "statusCode": number, "error": string }Validation Global ValidationPipe · whitelist: true, forbidNonWhitelisted: true · unknown field → 400 Related modules authentication, organizations, acl, file-manager Document version v1 · 2026-05-20 Audience Internal FE devs (mobile + web)
FE typically calls GET /users/:user_id without user_id after login to fetch the active user’s profile. GET /users is used by admins to list organization members (or, for the MORIA role, across organizations via org_id). Avatar updates have their own path (POST/DELETE /users/me/profile-image) that writes directly to S3 — profile_image cannot be modified via PATCH /users/:user_id.
Method Path Summary GET /v1/usersList users (org members / cross-org for MORIA) GET /v1/users/monthly-income-rangeReference data for monthly income ranges GET /v1/users/:user_idUser detail (or self if param omitted) PATCH /v1/users/:user_idUpdate user profile GET /v1/users/:user_id/roleFetch the user’s role (without permissions) GET /v1/users/:user_id/role/permissionsFetch the permissions the user holds POST /v1/users/me/profile-imageUpload/replace profile picture (multipart) DELETE /v1/users/me/profile-imageRemove profile picture
Auth & client notes
Some endpoints use @RequireClientType() — FE must send X-Client-Type: web or X-Client-Type: mobile. Response shapes differ between the two.
MORIA users must include org_id in GET /users. ORGANIZATION/INDIVIDUAL users can only scope to their own organization.
The profile_image field cannot be changed via PATCH — use the dedicated /users/me/profile-image route.
List users (organization members, or cross-organization for the MORIA role). Standard paginated response. The result shape depends on X-Client-Type (web/mobile).
bearer
MORIA, ORGANIZATION, INDIVIDUAL
read-user
RESOURCE_FETCHED
Param Type Default Notes org_idUUID — Required if caller is MORIA. For other roles, defaults to the user’s organization. pagenumber 1Page number limitnumber 10Records per page order'asc' | 'desc'descOrder by created_at user_statusenum UserStatus optional active, inactive, suspendeduser_typeenum UserType optional moria, organization, individual
"message" : " Users retrieved successfully " ,
"id" : " 770e8400-e29b-41d4-a716-446655440222 " ,
"email" : " amal@example.com " ,
"user_type" : " individual " ,
"organization_id" : " 660e8400-e29b-41d4-a716-446655440111 " ,
"created_at" : " 2026-05-20T09:15:00.000Z "
Status When it occurs 400 Bad RequestMORIA calls without org_id401 UnauthorizedAttempted to view another organization (non-MORIA) 403 ForbiddenPermission mismatch 404 Not FoundOrganization not found
Reference data for monthly income ranges for profile/KYC dropdowns. Standard pagination.
bearer
MORIA, ORGANIZATION, INDIVIDUAL
read-user
Param Type Default Notes pagenumber 1Page number limitnumber 10Records per page order'asc' | 'desc'descOrder by created_at
"message" : " Monthly income ranges retrieved successfully " ,
{ "id" : " ... " , "label" : " 0 – 5.000.000 " , "min" : 0 , "max" : 5000000 }
Status When it occurs 401 UnauthorizedInvalid Bearer/cookie 403 ForbiddenPermission mismatch
User detail. If user_id is omitted the server returns the logged-in user’s data. The result shape depends on the caller’s UserType and the X-Client-Type header.
bearer
read-user
RESOURCE_FETCHED
Param Type Notes user_idUUID Optional. If omitted → active user (self).
"message" : " Login user data fetched successfully " ,
"id" : " 770e8400-e29b-41d4-a716-446655440222 " ,
"email" : " amal@example.com " ,
"user_type" : " individual " ,
"phone_number" : " +628156489101 " ,
"organization" : { "id" : " 660e8400-e29b-41d4-a716-446655440111 " , "name" : " Moria Fund " },
"address" : { "country" : " Indonesia " , "city" : " Jakarta " }
On mobile clients the role, password, and security.two_factor_authentication_secret fields are stripped from the payload. On web clients the data shape is formatted differently by getUserByIdFormatted.
Status When it occurs 400 Bad Requestuser_id is not a UUID (if sent)401 UnauthorizedAttempted to access a user from another organization 403 ForbiddenPermission mismatch 404 Not FoundUser not found
Update a user profile. Only for users in the same organization. All fields are optional — send only what changes. The profile_image field is intentionally absent from the DTO.
bearer
update-user
RESOURCE_UPDATED
Param Type Notes user_idUUID ID of the user being updated
Field Type Notes first_name, last_name, middle_namestring Basic identity phone_numberstring International format (+62…) id_card_numberstring KTP number (16 digits) educationenum Education primary_school, junior_high, senior_high, diploma, bachelor, postgraduate, othermother_name, relativesstring Additional KYC data purpose, source_of_income, montly_incomestring Financial profile (typo montly_income retained) genderenum Gender male, femaledate_of_birthdate ISO 8601 (YYYY-MM-DD) place_of_birthstring City of birth religionenum Religion islam, christianity, hinduism, buddhism, confucianism, othermarital_statusenum MaritalStatus single, married, divorced, widowedprovince, city, country, district, subdistrict, villagestring Address rt, rw, postal_codestring Address detail address_typeenum AddressType Default INDIVIDUAL
"phone_number" : " +628156489102 " ,
"marital_status" : " married " ,
"message" : " User updated successfully " ,
"id" : " 770e8400-e29b-41d4-a716-446655440222 " ,
"phone_number" : " +628156489102 " ,
"marital_status" : " married " ,
"updated_at" : " 2026-05-20T10:00:00.000Z "
Status When it occurs 400 Bad RequestValidation failed (invalid enum, bad date format) 401 UnauthorizedUpdating a user from another organization 403 ForbiddenMissing update-user permission 404 Not FoundUser not found
Fetch the role attached to a user (without permissions and role_type). If the user has no role yet, the response is successful with no data field.
bearer
read-role
RESOURCE_FETCHED
Param Type Notes user_idUUID Target user ID
"message" : " user role fetched successfully " ,
"id" : " 03be5259-f281-478e-a8d0-e7e825e525f2 " ,
"name" : " organization_admin " ,
"description" : " Admin role for the organization "
If the user has no role: 200 response with message: "No role assigned to this user" and no data field.
Status When it occurs 403 ForbiddenPermission mismatch 404 Not FoundUser not found
Fetch all permissions the user holds via their role. If they have no role, the response is successful with no data field.
bearer
read-permission
RESOURCE_FETCHED
Param Type Notes user_idstring User ID (no UUID pipe at the controller — still send a valid UUID)
"message" : " user role permissions fetched successfully " ,
{ "id" : " ... " , "name" : " read-user " },
{ "id" : " ... " , "name" : " update-user " }
Status When it occurs 403 ForbiddenPermission mismatch 404 Not FoundUser not found
Upload or replace the active user’s profile picture. The backend accepts a multipart file (max 5 MB, JPEG/PNG/WebP), uploads to S3 (public-read), removes the old object, and stores the key in the users.profile_image column.
bearer
update-user
RESOURCE_UPDATED
Field Type Required Notes filebinary yes JPEG / PNG / WebP, maximum size 5 MB
"message" : " profile image updated successfully " ,
"profile_image" : " users/770e8400.../avatar-1716200000.jpg "
Status When it occurs 400 Bad RequestInvalid file (empty, unsupported MIME, size > 5 MB) 401 UnauthorizedInvalid Bearer/cookie 403 ForbiddenMissing update-user permission
Upload to S3 (public-read bucket) and remove the old object (if any).
Update the users.profile_image column with the new key.
Emit BusinessEvent RESOURCE_UPDATED (impact LOW).
Delete the active user’s profile picture. The backend removes the S3 object referenced by users.profile_image and clears that column.
bearer
update-user
RESOURCE_DELETED
"message" : " profile image removed successfully " ,
Status When it occurs 401 UnauthorizedInvalid Bearer/cookie 403 ForbiddenMissing update-user permission
Remove S3 object (if any).
Set users.profile_image = NULL.
active, inactive, suspended
moria, organization, individual
primary_school, junior_high, senior_high
diploma, bachelor, postgraduate, other
islam, christianity, hinduism
buddhism, confucianism, other
single, married, divorced, widowed
"message" : " you can't view another organization " ,
message can be a string or an array of strings (multi-field validation errors).
X-Client-Type: web — “formatted” response shape
X-Client-Type: mobile — lean shape (no role, password, security)
400 body/query/param validation
401 cross-organization access
403 permission mismatch
404 user not found