Saving Goals
The Saving Goals API lets your end-users set aside money toward named targets — Hajj, education, emergency fund — with a fixed monthly auto-debit from their main balance into a per-goal holding. This guide is aimed at backend engineers integrating the API: every endpoint, every state transition, every field your client will send and receive.
| Property | Value |
|---|---|
| Audience | Partner integrators · backend engineers |
| Base URL | /v1 · JSON over HTTPS |
| Auth | Bearer token (JWT) · per-endpoint permission |
| Lifecycle | new · active · paused · completed · cancelled · converted |
| Currency | IDR · amounts as integer-string (smallest unit) |
| Integration | ~1–2 sprints for a typical mobile or web client |
What Saving Goals does
Section titled “What Saving Goals does”A saving goal represents an intent to set aside funds toward a named target over a defined period. Once created, the system automatically debits a fixed amount each month from the owner’s main balance into a holding dedicated to that goal. The owner can pause auto-debit, withdraw funds back to the main balance, or let the goal mature toward its target.
Hajj Fund — a 5-year goal with liquidity_type: "locked" and a monthly deduction equal to target_amount / months. The end-user watches the progress bar tick up each cycle.
Emergency fund — a short-term goal with liquidity_type: "loose". The owner can withdraw at any time; auto-debit stops as soon as they pause or delete the goal.
Education fund — a medium-term goal that an organization admin can set up on behalf of a member, including a manual deduction endpoint for off-schedule one-time contributions.
What it is not. Saving Goals does not pull funds from external banks, does not earn interest on stored funds, and does not move funds between goals. Funds must already exist in the main balance before auto-debit can run; withdrawals release funds back to the same main balance.
Where the money lives
Section titled “Where the money lives”flowchart LR
U([Account owner])
TOPUP[/"Top-up<br/>(payment gateway)"/]
subgraph WALLET[" Wallet "]
direction TB
MAIN[("Main balance<br/>(primary cash)")]
HOLD[("Goal holding<br/>(per saving goal)")]
MAIN -->|"auto-debit on<br/>deduction_date (monthly)"| HOLD
HOLD -->|"manual withdrawal or<br/>on completion"| MAIN
end
PAYOUT[/"Disbursement to<br/>external bank"/]
WITHDRAW[/"POST /v1/withdrawals<br/>reference_type: saving_goal"/]
TOPUP -->|credit| MAIN
MAIN -->|"withdrawal to bank<br/>(separate flow)"| PAYOUT
U -.->|creates goal| HOLD
U -.->|reads saved_amount| HOLD
U -.->|initiates| WITHDRAW
classDef money fill:#FDF8E8,stroke:#3C2C96,stroke-width:2px,color:#1E1B4B;
classDef edge fill:#FFF3D0,stroke:#F4BE27,stroke-width:1.5px,color:#8B5A00;
classDef user fill:#E8E1FF,stroke:#3C2C96,stroke-width:1.5px,color:#1E1B4B;
class MAIN,HOLD money;
class TOPUP,WITHDRAW,PAYOUT edge;
class U user;
Status transitions
Section titled “Status transitions”stateDiagram-v2
direction LR
[*] --> new: POST /v1/saving-goals<br/>(transient)
new --> active: created with<br/>default state
active --> paused: PATCH /:id/pause<br/>{ pause: true }
paused --> active: PATCH /:id/pause<br/>{ pause: false }
active --> completed: holding reaches 0<br/>(automatic, after<br/>full withdrawal)
active --> cancelled: DELETE /:id<br/>(terminal)
paused --> cancelled: DELETE /:id<br/>(terminal)
active --> converted: terminal<br/>(reserved)
completed --> [*]
cancelled --> [*]
converted --> [*]
note right of active
Auto-deduction runs
on deduction_date
every month
end note
note right of paused
Auto-deduction is
skipped. Manual
deduction can still
be called by org admin.
end note
Endpoint reference
Section titled “Endpoint reference”| Method | Path | Purpose | Permission |
|---|---|---|---|
POST | /v1/saving-goals | Create a new goal for an account | create-saving-goal |
GET | /v1/saving-goals | List goals (paginated, filterable) | read-saving-goal |
GET | /v1/saving-goals/:id | Fetch a single goal by id | read-saving-goal |
PATCH | /v1/saving-goals/:id | Update editable metadata (name, end date, etc.) | update-saving-goal |
PATCH | /v1/saving-goals/:id/pause | Pause or resume auto-debit | pause-saving-goal |
PATCH | /v1/saving-goals/:id/manual-deduction | One-time contribution (org admin only) | update-saving-goal |
DELETE | /v1/saving-goals/:id | Soft-delete a goal (terminal) | delete-saving-goal |
GET | /v1/summary/saving-goals | Aggregate statistics (org admin only) | read-saving-goal |
Withdrawal is not on this surface — withdrawals use the platform-wide withdrawal endpoint with a saving-goal reference. See the withdrawal section below.
Creating a saving goal
Section titled “Creating a saving goal”Request
POST /v1/saving-goalsAuthorization: Bearer <token>Content-Type: application/json{ "account_id": "a8c1-...-c9f0", "name": "Hajj Fund", "target_amount": "50000000", "deduction_amount": "1000000", "deduction_date": 25, "start_date": "2026-06-01", "end_date": "2030-06-01", "liquidity_type": "locked"}Response · 201 Created
{ "data": { "savingGoal": { "id": "9f3b-...-1d22", "account_id": "a8c1-...-c9f0", "name": "Hajj Fund", "status": "active", "target_amount": "50000000", "saved_amount": "0", "deduction_amount": "1000000", "deduction_date": 25, "start_date": "2026-06-01", "end_date": "2030-06-01", "liquidity_type": "locked", "created_at": "2026-05-18T03:12:44Z" } }}How funds enter the goal
Section titled “How funds enter the goal”sequenceDiagram
autonumber
actor U as Account owner
participant API as Moria API
participant SCH as Scheduler
participant MAIN as Main balance
participant HOLD as Goal holding
Note over U,API: One-time setup
U->>API: POST /v1/saving-goals<br/>{ account_id, name, target_amount,<br/>deduction_amount, deduction_date,<br/>start_date, end_date, liquidity_type }
API-->>U: 201 Created<br/>{ savingGoal: { id, status: "active", ... } }
Note over SCH,HOLD: Every month, on deduction_date
SCH->>API: trigger auto-deduction sweep
API->>MAIN: read available balance for account_id
MAIN-->>API: balance
alt sufficient funds
API->>MAIN: debit deduction_amount
API->>HOLD: credit deduction_amount
API->>API: refresh saved_amount on goal
else insufficient funds
API->>API: skip this cycle<br/>(goal stays active)
end
Note over U,API: Owner reads progress
U->>API: GET /v1/saving-goals/:id
API-->>U: { id, status, saved_amount,<br/>target_amount, progress%, ... }
Auto and manual deduction
Section titled “Auto and manual deduction”Auto-debit · monthly
- Runs on the
deduction_dateday each month, starting fromstart_date - Debits
deduction_amountfrom the owner’s main balance, credits the goal-specific holding - Updates
saved_amounton the goal — which your client polls or displays - Silently skipped when the main balance is insufficient — the goal stays
activeand tries again next cycle - Does not run when the goal is
paused,completed,cancelled, orconverted
Manual deduction · org admin only
PATCH /v1/saving-goals/:id/manual-deductionAuthorization: Bearer <token>{ "deduction_amount": "500000" }A one-time contribution off the monthly schedule. Useful for ad-hoc top-ups (year-end bonus, employer match). The same balance check applies — the caller must have funds in the main balance. Available only to organization admins; individuals cannot call this.
Both paths use the same money mechanic: a debit on the main balance is paired with a credit on the goal-specific holding. saved_amount on the goal reflects the holding, so a single read gives you the progress number.
Returning funds to the main balance
Section titled “Returning funds to the main balance”Request
POST /v1/withdrawalsAuthorization: Bearer <token>{ "reference_type": "saving_goal", "reference_id": "9f3b-...-1d22", "amount": "2000000"}Withdrawal uses the platform-wide withdrawal endpoint. reference_type tells the system to debit the goal-specific holding, not some other source.
Pause and resume
Section titled “Pause and resume”Pause
PATCH /v1/saving-goals/:id/pauseAuthorization: Bearer <token>{ "pause": true }Sets status to paused. Auto-debit stops on the next cycle.
Resume
PATCH /v1/saving-goals/:id/pauseAuthorization: Bearer <token>{ "pause": false }Returns the goal to active. Auto-debit resumes from the next scheduled deduction_date.
List and filter
Section titled “List and filter”Request
GET /v1/saving-goals?status=active &liquidity_type=locked &page=1&limit=20Authorization: Bearer <token>Returns a paginated data envelope of goals visible to the caller. Individuals see their own goals; organization admins see goals across all members in their organization.
Query parameters
| Param | Values | Notes |
|---|---|---|
status | active · paused · completed · cancelled · converted | Filter by state |
liquidity_type | locked · liquid | Filter by liquidity |
account_id | uuid | Scope to a single account |
owner_id | uuid | Org admin only · scope to a single member |
page | integer, default 1 | 1-indexed |
limit | integer, default 20 | Page size |
Responses wrap the list inside data with pagination metadata. Plan your client to consume the object envelope, not a bare array — this is forward-compatible against future additions.
Errors you will encounter
Section titled “Errors you will encounter”| Status | Typical trigger | Meaning · how to respond |
|---|---|---|
400 | Validation | Body fails schema validation — missing field, wrong type, deduction_date out of range, end_date before start_date. Show an inline message. |
401 | Bearer token missing or invalid | Token expired, malformed, or absent. Re-authenticate and retry. |
403 | Missing permission or ownership | The caller lacks the required permission (e.g. an individual trying to call manual-deduction) or operates on a goal that is not theirs. Do not retry — surface as an access-denied state. |
404 | Unknown goal id | Goal does not exist or has been soft-deleted. Refresh the list and remove the stale entry. |
409 | Illegal state transition | Attempting to pause a completed goal, manual-deduct a cancelled goal, etc. Re-fetch the goal to learn its current state. |
422 | Insufficient main balance | Manual deduction (or any flow touching the main balance) cannot proceed because the owner’s funds are insufficient. Prompt the owner to top up. |
500 | Internal | Retry once with exponential backoff. If persistent, capture the request id from the error envelope and contact support. |
Standard error envelope
{ "statusCode": 400, "message": "deduction_date must be between 1 and 28", "error": "Bad Request" }Auth, permissions, and visibility
Section titled “Auth, permissions, and visibility”Auth
- Every endpoint expects an
Authorization: Bearer <token>header - The token is a JWT issued by the platform’s auth flow — your client obtains it on behalf of the end-user and forwards it on every call
- Each call is checked against the caller’s role and the permissions bound to that role
Permission matrix
| Permission | Used by |
|---|---|
create-saving-goal | POST create |
read-saving-goal | GET list · GET one · GET summary |
update-saving-goal | PATCH update · PATCH manual-deduction |
pause-saving-goal | PATCH pause |
delete-saving-goal | DELETE |
Visibility rules
- Individuals see and act only on their own goals (where the underlying account belongs to them)
- Organization admins see goals across all members in their organization and can use
owner_idfor scoping - Manual deduction and summary are restricted to organization admins, regardless of the granted permission
- Goals not visible to the caller return
404— not403— to avoid leaking existence
What is not supported, and how to work around it
Section titled “What is not supported, and how to work around it”Integration checklist
Section titled “Integration checklist”Build with confidence
Section titled “Build with confidence”Saving Goals is a deliberately small surface, designed to compose with the rest of the platform — top-ups land in the main balance, withdrawals exit through the same generic withdrawal endpoint, and goals themselves just express a contract: “this much, per month, into a named holding.” Knowing the lifecycle, the money flow, and a handful of notes above is enough to ship a production integration.