Saving Circles is the implementation of Arisan rotational savings — a group of users contributes each round, and one member receives the payout per round. The module provides circle CRUD, member management, contributions (gateway or balance rail), and admin summaries. POST /saving-circles accepts two DTO shapes: CreateSavingCircleDto (regular user) or AdminCreateSavingCircleDto (admin creating on behalf of employees).
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 accounts, payment-gateway (bisabiller), users, organizations, withdrawal Document version v1 · 2026-05-20 Audience Internal FE devs (mobile + web)
Typical flow: create circle → add members (member accounts) → each round members contribute (gateway VA / QR or balance debit) → the round winner receives the payout. Organization admins can view statistics summaries for circles in their org. The visibility field controls whether the circle is discoverable in other orgs’ marketplaces or kept private.
Method Path Summary POST /v1/saving-circlesCreate a saving circle (regular or admin oneOf DTO) POST /v1/saving-circles/membersAdd members to a circle POST /v1/saving-circles/contributeContribute to a circle (gateway / balance) GET /v1/saving-circlesList circles with status / org / user filters GET /v1/saving-circles/:idDetail of a single circle PATCH /v1/saving-circles/:idUpdate circle parameters DELETE /v1/saving-circles/:idDelete a circle (not allowed when there are active contributions) GET /v1/summary/saving-circlesStatistics summary (admin MORIA/ORGANIZATION)
Auth + DTO notes
The POST /saving-circles endpoint accepts oneOf CreateSavingCircleDto (regular user) or AdminCreateSavingCircleDto (MORIA/ORGANIZATION). The server picks the path based on the caller’s user_type.
Circle owner types are only individual or organization — there is no MORIA-owned circle (the DB enum SavingCircleOwnerType rejects it).
Balance-rail contributions are immediately COMPLETED; gateway-rail contributions remain PENDING until the Bisabiller callback (see the payment-gateway module).
Authorization filtering follows the same layered model as saving-goals (INDIVIDUAL → self, ORGANIZATION → org, MORIA → unrestricted).
Create a new saving circle. The server picks the DTO based on user_type: INDIVIDUAL uses CreateSavingCircleDto, MORIA/ORGANIZATION uses AdminCreateSavingCircleDto (creating a circle for employees).
bearer
create-saving-circle
RESOURCE_CREATED
Field Type Required Notes account_idstring (UUID) ✓ Source account of the circle owner namestring ✓ Circle name (e.g. Hajj) capacitynumber optional Maximum number of members, min 1 start_datedate (ISO 8601) ✓ Start date of the first round amountstring ✓ Contribution nominal per round (rupiah)
Field Type Required Notes account_idstring (UUID) ✓ Sponsoring organization account namestring ✓ Circle name capacitynumber optional Maximum number of members start_datedate (ISO 8601) ✓ Start date amountstring ✓ Contribution nominal per round
"account_id" : " be2cb7da-e217-401c-8d07-b01e64adfb34 " ,
"start_date" : " 2028-02-06 " ,
"message" : " Saving circle created successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"amount" : " 240000000.0000 " ,
"start_date" : " 2028-02-06 " ,
"owner_type" : " individual " ,
"owner_id" : " 770e8400-e29b-41d4-a716-446655440222 " ,
"account_id" : " be2cb7da-e217-401c-8d07-b01e64adfb34 " ,
"created_at" : " 2026-05-20T08:30:00.000Z "
Status When it occurs 400 Bad RequestValidation failed (capacity < 1, invalid date) 401 UnauthorizedBearer/cookie token invalid 403 ForbiddenMissing create-saving-circle permission
Add new members to an existing saving circle. Only the creator or an admin may add. Send a list of account_ids for prospective members.
bearer
create-saving-circle
RESOURCE_UPDATED
Field Type Required Notes saving_circle_idstring (UUID) ✓ Target circle account_idsstring[] (UUID) optional List of prospective member accounts
"saving_circle_id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
" 03be5259-f281-478e-a8d0-e7e825e525f2 " ,
" 03be5259-f281-478e-a8d0-e7e825e525f3 "
"message" : " Members added successfully " ,
"data" : { "members" : [ { "id" : " ... " , "saving_circle_id" : " ... " , "account_id" : " ... " , "turn_number" : null } ] }
Status When it occurs 400 Bad RequestValidation failed · circle is full · account is already a member 401 UnauthorizedBearer/cookie token invalid 403 ForbiddenNot the circle creator/admin 404 Not FoundCircle not found
A member contributes to the circle for the current round. Can use the balance rail (debit Moria balance directly) or the gateway rail (Bisabiller). Dedup rule: a row with status PENDING or COMPLETED for the same (circle_id, user_id, turn_number) rejects a repeat attempt; FAILED/CANCELLED rows allow retry.
bearer
update-saving-circle
CONTRIBUTION_MADE
Field Type Required Notes saving_circle_idstring (UUID) ✓ Target circle contribution_amountnumeric string ✓ Must equal the circle’s per-round amount · IsNotZero + IsValidDecimal
"saving_circle_id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"contribution_amount" : " 1000.00 "
"message" : " Contribution made successfully " ,
"id" : " 880e8400-e29b-41d4-a716-446655440333 " ,
"saving_circle_id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"user_id" : " 770e8400-e29b-41d4-a716-446655440222 " ,
Status When it occurs 400 Bad RequestAmount does not match circle.amount · user already has a PENDING/COMPLETED row for this turn · insufficient balance 401 UnauthorizedBearer/cookie token invalid 403 ForbiddenNot a circle member 404 Not FoundCircle not found
Inserts a row into saving_circle_contributions (status=PENDING for gateway, COMPLETED for balance).
For balance rail: ledger transaction debits the user account and credits the holding circle account.
Emits BusinessEvent CONTRIBUTION_MADE (impact MEDIUM).
List saving circles. Scope depends on role: INDIVIDUAL sees their own circles, ORGANIZATION admin sees the org’s circles, MORIA sees all. Supports organization_id for marketplace lookup (searching for public circles in another org).
bearer
read-saving-circle
Param Type Default Notes pagenumber 1Page number limitnumber 10Records per page statusenum SavingCircleStatus optional new, active, cancelled, completedorder'asc' | 'desc'descSort by created_at organization_idstring (UUID) optional Marketplace lookup; INDIVIDUAL may only use their own organization user_idstring (UUID) optional User filter; same authorization as saving-goals account_idstring (UUID) optional Account filter; same authorization as saving-goals
"message" : " Saving circles retrieved successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"amount" : " 240000000.0000 " ,
"owner_type" : " individual " ,
"owner_id" : " 770e8400-e29b-41d4-a716-446655440222 "
Status When it occurs 400 Bad RequestInvalid query param 401 UnauthorizedBearer/cookie token invalid 403 ForbiddenINDIVIDUAL queries another user’s data · ORGANIZATION queries another org
Detail of a single saving circle, including members and a contributions summary. Response format differs between WEB and MOBILE.
bearer
read-saving-circle
Param Type Notes idUUID Saving circle ID
"message" : " saving circle fetched successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"amount" : " 240000000.0000 " ,
"owner_type" : " individual " ,
{ "id" : " ... " , "account_id" : " ... " , "user_id" : " ... " , "turn_number" : 1 }
Status When it occurs 400 Bad Requestid is not a UUID401 UnauthorizedBearer/cookie token invalid 403 ForbiddenNo access to the circle 404 Not FoundCircle not found
Update circle parameters. Only the creator or an admin may perform this. All fields are optional.
bearer
update-saving-circle
RESOURCE_UPDATED
Param Type Notes idUUID Saving circle ID
Field Type Notes namestring New name capacitynumber New capacity start_datestring New start date (free-form string; ISO format recommended) amountstring Per-round contribution nominal current_turnnumber Current turn visibilityenum Visibility private, public, organization
{ "capacity" : 12 , "visibility" : " organization " }
"message" : " Saving circle updated successfully " ,
"data" : { "..." : " SavingCircleDto shape " }
Status When it occurs 400 Bad RequestValidation failed 401 UnauthorizedBearer/cookie token invalid 403 ForbiddenNot the creator/admin 404 Not FoundCircle not found
Delete a saving circle. Not allowed if the circle is already ACTIVE with recorded contributions — use status cancellation elsewhere.
bearer
delete-saving-circle
RESOURCE_DELETED
Param Type Notes idUUID Saving circle ID
"message" : " Saving circle deleted successfully "
Status When it occurs 400 Bad RequestCircle is ACTIVE with recorded contributions 401 UnauthorizedBearer/cookie token invalid 403 ForbiddenNot the creator/admin 404 Not FoundCircle not found
Organization-level saving circles statistics summary. Admin endpoint — only MORIA and ORGANIZATION. Includes totals, status breakdown, capacity utilization, etc.
bearer
MORIA, ORGANIZATION
read-saving-circle
RESOURCE_FETCHED
"message" : " saving circles summary fetched successfully " ,
"organization_id" : " 660e8400-e29b-41d4-a716-446655440111 " ,
"organization_name" : " Moria Fund " ,
"completed_circles" : " 2 " ,
"cancelled_circles" : " 1 " ,
"total_contribution_amount" : " 120000000.00 " ,
"average_capacity" : " 8.50 " ,
"average_current_turn" : " 2.40 "
Status When it occurs 401 UnauthorizedToken invalid · querying another org (mismatch) 403 ForbiddenRole is not MORIA/ORGANIZATION 404 Not FoundOrganization not found
new — circle created, not yet running
active — rounds in progress
cancelled — cancelled
completed — all rounds finished
private — only owner + members can see it
public — discoverable in the cross-org marketplace
organization — limited to organization members
pending · completed · failed · cancelled · refunded
gateway — VA/QR via Bisabiller
balance — direct debit of Moria balance
"message" : " capacity must be at least 1 " ,
400 validation · circle full · insufficient balance · duplicate contribution
401 token expired / missing
403 not owner/member · wrong scope
404 circle not found
500 internal — show a generic toast