The accounts module manages “pocket” / ledger accounts belonging to a user or organization. Every account has an account_number (encrypted + hash) used for internal transfers. The main endpoints: list/filter, transfer between accounts, detail by ID, label update, and soft-delete. All endpoints require Bearer and specific permissions (read-account, top-up-account, update-account, delete-account).
Property Value Base URL {HOST}/v1Auth Bearer JWT (header Authorization) or cookie access_token Content-Type application/jsonError envelope { "message": string | string[], "statusCode": number, "error": string }Validation Global ValidationPipe · whitelist: true, forbidNonWhitelisted: true · unknown field → 400 Related modules users, organizations, transactions, payments Document version v1 · 2026-05-20 Audience Internal FE devs (mobile + web)
GET /accounts is polymorphic: with no filter → list of user/organization accounts (paginated); with user_id → accounts for a specific user; with account_number → a single account by number. The pair user_id + account_number cannot be sent together. Internal transfers use source & destination account_number (not the account UUID).
Method Path Summary GET /v1/accountsList accounts or fetch detail by user_id/account_number POST /v1/accounts/transferTransfer funds between accounts (by account number) GET /v1/accounts/:idDetail of one account by UUID PATCH /v1/accounts/:idUpdate account (name, currency, account_type) DELETE /v1/accounts/:idSoft delete an account
Key notes
account_number is stored encrypted (@EncryptedColumn) plus a separate hash for lookup. FE only receives/displays the plaintext value already decrypted by the server.
Transfers use account_number, not id. The source account must belong to the caller’s user/organization.
DELETE is a soft-delete (sets deleted_at); the data remains in the DB.
List accounts owned by the user (or the organization if the caller is UserType.ORGANIZATION). When user_id or account_number is sent, the response is a single account (not paginated). The two filters cannot be sent together.
bearer
read-account
Param Type Default Notes pagenumber 1Page number (list mode) limitnumber 10Records per page order'asc' | 'desc'descOrder by created_at user_idUUID optional Filter to a specific user’s accounts (detail mode) account_numberstring optional Filter to a specific account number (detail mode)
"message" : " User Accounts retrrieved successfully " ,
"id" : " 770e8400-e29b-41d4-a716-446655440222 " ,
"account_number" : " 1234567890 " ,
"name" : " Savings Pocket " ,
"slug_name" : " savings_pocket " ,
"balance" : " 150000.0000 " ,
"owner_type" : " individual " ,
"account_type" : " pocket " ,
"owner_id" : " 660e8400-e29b-41d4-a716-446655440111 " ,
"opened_at" : " 2026-05-20T08:30:00.000Z " ,
"created_at" : " 2026-05-20T08:30:00.000Z " ,
"updated_at" : " 2026-05-20T08:30:00.000Z "
Detail mode (user_id / account_number filter): data contains { account: AccountDto }.
Status When it occurs 400 Bad RequestFilters user_id + account_number sent together 401 UnauthorizedAttempted to read an account that is not theirs 403 ForbiddenMissing read-account permission 404 Not FoundAccount not found
Transfer funds between two internal accounts using account_number. The source account must belong to the caller (or their organization). Self-transfers (source = destination) are rejected.
bearer
top-up-account
Field Type Required Notes source_account_numberstring yes Source account number (must belong to the caller) destination_account_numberstring yes Destination account number (can be another account) amountstring yes Transfer amount (decimal string, e.g. "50000.0000")
"source_account_number" : " 123456789012 " ,
"destination_account_number" : " 210987654321 " ,
"message" : " Transfer completed successfully " ,
"transaction_id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"source_account_id" : " 550e8400-e29b-41d4-a716-446655440001 " ,
"destination_account_id" : " 550e8400-e29b-41d4-a716-446655440002 " ,
Status When it occurs 400 Bad RequestInvalid amount, inactive account, or self-transfer 401 UnauthorizedInvalid Bearer/cookie 403 ForbiddenSource account does not belong to the caller 404 Not FoundOne of the accounts not found
Creates a ledger transaction (transactions module).
Updates both account balances atomically.
Detail of a single account by UUID. The server validates that the account belongs to the user or their organization.
bearer
read-account
Param Type Notes idUUID Account ID (validated by ParseUUIDPipe)
"message" : " Account retrieved successfully " ,
"id" : " 770e8400-e29b-41d4-a716-446655440222 " ,
"account_number" : " 1234567890 " ,
"name" : " Savings Pocket " ,
"balance" : " 150000.0000 " ,
"owner_type" : " individual " ,
"account_type" : " pocket " ,
"owner_id" : " 660e8400-e29b-41d4-a716-446655440111 " ,
"opened_at" : " 2026-05-20T08:30:00.000Z " ,
Status When it occurs 400 Bad Requestid is not a UUID401 UnauthorizedAccount does not belong to the caller 403 ForbiddenMissing read-account permission 404 Not FoundAccount not found
Update account fields (e.g. name / label, currency, account_type). The DTO extends CreateAccountDto via PartialType — all fields are optional.
bearer
update-account
ACCOUNT_UPDATED
Param Type Notes idUUID Account ID
Field Type Notes account_typeenum AccountType pocket, saving_goal, saving_circle, commodity_financing, charitable_cause, investment, takafulnamestring Account label currencystring ISO currency (e.g. IDR)
"name" : " Tabungan Pernikahan "
"message" : " Account updated successfully " ,
"id" : " 770e8400-e29b-41d4-a716-446655440222 " ,
"name" : " Tabungan Pernikahan " ,
"account_type" : " pocket " ,
"updated_at" : " 2026-05-20T10:00:00.000Z "
Status When it occurs 400 Bad RequestValidation failed (invalid enum) 401 UnauthorizedAccount does not belong to the caller 403 ForbiddenMissing update-account permission 404 Not FoundAccount not found
Soft delete an account. The record is preserved in the DB (deleted_at + deleted_by columns are populated) but the account no longer appears in list/detail endpoints.
bearer
delete-account
RESOURCE_DELETED
Param Type Notes idUUID ID of the account to delete
"message" : " Account deleted successfully "
Status When it occurs 400 Bad Requestid is not a UUID401 UnauthorizedAccount does not belong to the caller 403 ForbiddenMissing delete-account permission 404 Not FoundAccount not found
Soft-delete: deleted_at set to the current timestamp, deleted_by to the caller’s user ID.
Emit BusinessEvent RESOURCE_DELETED (impact HIGH).
pocket — general pocket
saving_goal
saving_circle
commodity_financing
charitable_cause
investment
takaful
internal — Moria ledger
external — linked to an external bank account
active, suspended, closed, inactive
individual, organization, moria
"message" : " you can't filter by user_id and account_id at the same time " ,
message can be a string or an array of strings.
400 body/query/param validation or illegal filter combination
401 account does not belong to the caller
403 permission mismatch
404 account not found
500 internal — show a generic toast
All monetary values are sent as strings with 4 decimals (e.g. "50000.0000") to preserve precision — FE must parse via BigDecimal / a high-precision library.