How it works
Two flows, opposite directions. Read both before writing any code: most integration mistakes come from assuming the money-in flow is synchronous, or that a payout is finished when the API answers.
Taking a payment
Section titled “Taking a payment”Your customer owes you money. You ask Moria for something they can pay with — a virtual account number, a QR code, or an e-wallet checkout link — and Moria tells you when it is paid.
sequenceDiagram
autonumber
participant C as Customer
participant Y as You
participant M as Moria
participant P as Provider
Y->>M: POST /payments
M->>P: create instrument
P-->>M: VA / QR / link
M-->>Y: 200 · pending
Y-->>C: show payment code
Note over C,M: minutes or days
C->>P: pays
P-->>M: settlement
M->>M: credit balance
M-->>Y: payment.succeeded
The API answers immediately, and it answers pending. That is the instrument being ready, not the money having arrived. The money arrives later — sometimes days later — and you learn about it from the webhook.
If the customer never pays, the instrument expires and you get payment.expired. If the provider rejects the payment, you get payment.failed. Every payment ends in exactly one terminal state.
Paying money out
Section titled “Paying money out”Your balance with Moria is money you have collected, less fees. A payout moves some of it to a bank account.
sequenceDiagram
autonumber
participant Y as You
participant M as Moria
participant P as Provider
participant B as Bank
Y->>M: POST /payouts
M->>M: check limits, reserve
M-->>Y: 200 · pending
M->>P: transfer
P->>B: transfer
P-->>M: settled / failed
M-->>Y: payout.succeeded / failed
The reservation happens before the transfer is sent, so the same money cannot be promised twice. If the payout fails, the reservation is returned and your balance goes back up.
Where the money sits
Section titled “Where the money sits”flowchart TD
A["Customer pays<br/>(gross)"] --> B["− Combined fee"]
B --> C["= Your balance<br/>(net)"]
C --> D["Payout to your bank"]
The Partner Gateway defaults to DEDUCTED: amount is what the payer pays, and the combined fee comes out of it. A negotiated partner- or organisation-scoped rule may use ON_TOP, where the payer covers the fee above the amount that should land. The mode is resolved by Moria’s pricing configuration, not sent as a request field.
Every payment exposes three partner-facing figures:
| Field | What it is |
|---|---|
amount | Gross — what the customer paid |
fee | The combined provider fee and Moria fee |
net_amount | What lands in your balance |
The provider/margin split remains inside Moria for accounting and reconciliation; it is not exposed on the Partner Gateway surface. For a supported money-in instrument, the provider-facing gross must also be a whole IDR amount. See the changelog for the exact validation and migration notes.
What the states mean
Section titled “What the states mean”| Status | Meaning | Final? |
|---|---|---|
pending | Waiting. For a payment: nobody has paid. For a payout: the outcome is not yet known. | No |
succeeded | The money moved. | Yes |
failed | It did not, and will not under this request. | Yes |
expired | The instrument was never paid in time. | Yes |
cancelled | Called off before it completed. | Yes |
refunded | Paid, then returned. | Yes |
Every response carries is_final so you do not have to keep your own list of which states are terminal. When we add a state, is_final keeps telling the truth and your code does not change.
Two rules that will save you money
Section titled “Two rules that will save you money”Poll, do not re-send. If a call times out or the connection drops, you do not know whether it worked. Ask for the status — never issue the same instruction again with a fresh idempotency key. See Idempotency.
Deduplicate on the webhook’s id. Delivery is at-least-once by design. The same event will sometimes arrive twice, and treating the second one as a second payment is a real way to double-credit a customer. See Webhooks.