Skip to content

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.

PropertyValue
AudiencePartner integrators · backend engineers
Base URL/v1 · JSON over HTTPS
AuthBearer token (JWT) · per-endpoint permission
Lifecyclenew · active · paused · completed · cancelled · converted
CurrencyIDR · amounts as integer-string (smallest unit)
Integration~1–2 sprints for a typical mobile or web client

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.


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;

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

MethodPathPurposePermission
POST/v1/saving-goalsCreate a new goal for an accountcreate-saving-goal
GET/v1/saving-goalsList goals (paginated, filterable)read-saving-goal
GET/v1/saving-goals/:idFetch a single goal by idread-saving-goal
PATCH/v1/saving-goals/:idUpdate editable metadata (name, end date, etc.)update-saving-goal
PATCH/v1/saving-goals/:id/pausePause or resume auto-debitpause-saving-goal
PATCH/v1/saving-goals/:id/manual-deductionOne-time contribution (org admin only)update-saving-goal
DELETE/v1/saving-goals/:idSoft-delete a goal (terminal)delete-saving-goal
GET/v1/summary/saving-goalsAggregate 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.


Request

POST /v1/saving-goals
Authorization: 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"
}
}
}

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-debit · monthly

  • Runs on the deduction_date day each month, starting from start_date
  • Debits deduction_amount from the owner’s main balance, credits the goal-specific holding
  • Updates saved_amount on the goal — which your client polls or displays
  • Silently skipped when the main balance is insufficient — the goal stays active and tries again next cycle
  • Does not run when the goal is paused, completed, cancelled, or converted

Manual deduction · org admin only

PATCH /v1/saving-goals/:id/manual-deduction
Authorization: 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.


Request

POST /v1/withdrawals
Authorization: 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

PATCH /v1/saving-goals/:id/pause
Authorization: Bearer <token>
{ "pause": true }

Sets status to paused. Auto-debit stops on the next cycle.

Resume

PATCH /v1/saving-goals/:id/pause
Authorization: Bearer <token>
{ "pause": false }

Returns the goal to active. Auto-debit resumes from the next scheduled deduction_date.


Request

GET /v1/saving-goals?status=active
&liquidity_type=locked
&page=1&limit=20
Authorization: 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

ParamValuesNotes
statusactive · paused · completed · cancelled · convertedFilter by state
liquidity_typelocked · liquidFilter by liquidity
account_iduuidScope to a single account
owner_iduuidOrg admin only · scope to a single member
pageinteger, default 11-indexed
limitinteger, default 20Page 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.


StatusTypical triggerMeaning · how to respond
400ValidationBody fails schema validation — missing field, wrong type, deduction_date out of range, end_date before start_date. Show an inline message.
401Bearer token missing or invalidToken expired, malformed, or absent. Re-authenticate and retry.
403Missing permission or ownershipThe 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.
404Unknown goal idGoal does not exist or has been soft-deleted. Refresh the list and remove the stale entry.
409Illegal state transitionAttempting to pause a completed goal, manual-deduct a cancelled goal, etc. Re-fetch the goal to learn its current state.
422Insufficient main balanceManual deduction (or any flow touching the main balance) cannot proceed because the owner’s funds are insufficient. Prompt the owner to top up.
500InternalRetry 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

  • 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

PermissionUsed by
create-saving-goalPOST create
read-saving-goalGET list · GET one · GET summary
update-saving-goalPATCH update · PATCH manual-deduction
pause-saving-goalPATCH pause
delete-saving-goalDELETE

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_id for scoping
  • Manual deduction and summary are restricted to organization admins, regardless of the granted permission
  • Goals not visible to the caller return 404 — not 403 — 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”


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.