Skip to content

Top-up

Top-up is the only way external money enters Moria. The owner creates a payment order with reference_type: account_top_up; the order moves through pendingwaiting_paymentprocessing → terminal (completed · failed · cancelled). The main balance is credited only when the order reaches completed on a signed async callback from the gateway.

A top-up payment order is a short-lived state machine. The pending state is transient — by the time the API responds, the gateway has already accepted the order and returned the payment instructions (VA number, QR code, or e-wallet link), and the order is sitting in waiting_payment. From there, the order either advances to processing once the user initiates payment, or sweeps to cancelled if it expires.

The main balance is not credited synchronously when the order is created. Credit happens later, on the gateway’s signed callback — and only then does the order land in completed.

stateDiagram-v2
    direction LR

    [*] --> pending: POST /v1/payment-gateway/orders<br/>(transient)

    pending --> waiting_payment: gateway accepts order<br/>VA / QR / e-wallet<br/>returned to client

    waiting_payment --> processing: user initiates<br/>payment

    processing --> completed: signature callback ok<br/>+ main balance credited

    processing --> failed: gateway reports<br/>payment failed

    waiting_payment --> cancelled: user cancels<br/>or sweeper expires

    completed --> [*]
    failed --> [*]
    cancelled --> [*]

    note right of waiting_payment
        Client polls or listens
        for the credit. Main balance
        is NOT credited until
        the callback runs.
    end note

    note right of completed
        Callback is signed and
        idempotent on trx_id.
        Duplicate callbacks are
        safe to receive.
    end note

The main balance is credited only on the callback — not synchronously when the order is created. Build your client to treat the order response as an “instruction to pay” and poll or listen for the credit that will follow. Callbacks are idempotent on trx_id, so duplicate webhooks are safe.

---
config:
  sequence:
    actorMargin: 380
    width: 240
    messageMargin: 38
    boxMargin: 14
    noteMargin: 12
---
sequenceDiagram
    autonumber
    actor U as Account owner
    participant API as Moria API
    participant GW as Payment Gateway

    Note over U,GW: Top-up · happy path<br/>• owner triggers from the client app<br/>• gateway returns payment instructions<br/>• balance is credited only on the callback (async)

    U->>API: POST /v1/payment-gateway/orders<br/>{ amount, payment_method,<br/>reference_type: "account_top_up",<br/>reference_id: account_id }
    API->>GW: create transaction<br/>(server-to-server)
    GW-->>API: { trx_id, va_number / qr_code,<br/>status: waiting_payment }
    API-->>U: 201 Created<br/>{ order_id, payment_instructions,<br/>status: "waiting_payment" }

    U->>GW: pay VA / scan QR<br/>(out of band)

    Note over GW,API: Callback (async, signed)
    GW->>API: POST /v1/payment-gateway/callback<br/>{ trx_id, status: success, signature }
    API->>API: verify signature · idempotent on trx_id<br/>set order status: "completed"<br/>credit main balance · emit event
    API-->>GW: 200 OK

    U->>API: GET /v1/accounts/me/balance
    API-->>U: 200 OK<br/>{ balance: <updated> }