Skip to content

Accounts

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).

PropertyValue
Base URL{HOST}/v1
AuthBearer JWT (header Authorization) or cookie access_token
Content-Typeapplication/json
Error envelope{ "message": string | string[], "statusCode": number, "error": string }
ValidationGlobal ValidationPipe · whitelist: true, forbidNonWhitelisted: true · unknown field → 400
Related modulesusers, organizations, transactions, payments
Document versionv1 · 2026-05-20
AudienceInternal 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).

MethodPathSummary
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

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
ParamTypeDefaultNotes
pagenumber1Page number (list mode)
limitnumber10Records per page
order'asc' | 'desc'descOrder by created_at
user_idUUIDoptionalFilter to a specific user’s accounts (detail mode)
account_numberstringoptionalFilter to a specific account number (detail mode)
{
"status": "success",
"statusCode": 200,
"message": "User Accounts retrrieved successfully",
"data": {
"limit": 10,
"count": 4,
"currentPage": 1,
"totalPages": 1,
"accounts": [
{
"id": "770e8400-e29b-41d4-a716-446655440222",
"account_number": "1234567890",
"name": "Savings Pocket",
"slug_name": "savings_pocket",
"balance": "150000.0000",
"currency": "IDR",
"owner_type": "individual",
"account_type": "pocket",
"scope": "internal",
"status": "active",
"baas_provider": "cimb",
"owner_id": "660e8400-e29b-41d4-a716-446655440111",
"opened_at": "2026-05-20T08:30:00.000Z",
"closed_at": null,
"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 }.

StatusWhen 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

POST /v1/accounts/transfer bearer

Section titled “POST /v1/accounts/transfer ”

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
FieldTypeRequiredNotes
source_account_numberstringyesSource account number (must belong to the caller)
destination_account_numberstringyesDestination account number (can be another account)
amountstringyesTransfer amount (decimal string, e.g. "50000.0000")
{
"source_account_number": "123456789012",
"destination_account_number": "210987654321",
"amount": "50000.0000"
}
{
"status": "success",
"statusCode": 200,
"message": "Transfer completed successfully",
"data": {
"transaction_id": "550e8400-e29b-41d4-a716-446655440000",
"source_account_id": "550e8400-e29b-41d4-a716-446655440001",
"destination_account_id": "550e8400-e29b-41d4-a716-446655440002",
"amount": "50000.0000"
}
}
StatusWhen 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
ParamTypeNotes
idUUIDAccount ID (validated by ParseUUIDPipe)
{
"status": "success",
"statusCode": 200,
"message": "Account retrieved successfully",
"data": {
"id": "770e8400-e29b-41d4-a716-446655440222",
"account_number": "1234567890",
"name": "Savings Pocket",
"balance": "150000.0000",
"currency": "IDR",
"owner_type": "individual",
"account_type": "pocket",
"scope": "internal",
"status": "active",
"baas_provider": "cimb",
"owner_id": "660e8400-e29b-41d4-a716-446655440111",
"opened_at": "2026-05-20T08:30:00.000Z",
"closed_at": null
}
}
StatusWhen it occurs
400 Bad Requestid is not a UUID
401 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
ParamTypeNotes
idUUIDAccount ID

Request body — UpdateAccountDto (all fields optional)

Section titled “Request body — UpdateAccountDto (all fields optional)”
FieldTypeNotes
account_typeenum AccountTypepocket, saving_goal, saving_circle, commodity_financing, charitable_cause, investment, takaful
namestringAccount label
currencystringISO currency (e.g. IDR)
{
"name": "Tabungan Pernikahan"
}
{
"status": "success",
"statusCode": 200,
"message": "Account updated successfully",
"data": {
"id": "770e8400-e29b-41d4-a716-446655440222",
"name": "Tabungan Pernikahan",
"currency": "IDR",
"account_type": "pocket",
"updated_at": "2026-05-20T10:00:00.000Z"
}
}
StatusWhen 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

DELETE /v1/accounts/:id bearer

Section titled “DELETE /v1/accounts/:id ”

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
ParamTypeNotes
idUUIDID of the account to delete
{
"status": "success",
"statusCode": 200,
"message": "Account deleted successfully"
}
StatusWhen it occurs
400 Bad Requestid is not a UUID
401 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
  • cimb, berrypay, amarbank
{
"message": "you can't filter by user_id and account_id at the same time",
"statusCode": 400,
"error": "Bad Request"
}

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.