Disbursement Service API
Internal money-movement service — rail configuration, funding balance, billable-lookup invoices, and idempotent transfers
The Disbursement Service executes money-movement transfers through pluggable rails behind DB-backed idempotency.
Internal-only — this is not a merchant-facing API
Only recon-service may call this service. Every route requires recon-service's client identity, and each transfer carries an approved-batch binding that preserves recon's segregation-of-duties ladder.
Its only server is the in-cluster address http://disbursement-service:8091, which is not publicly routable. Merchant-facing disbursement — the two-eyes create/approve/reject workflow, recipients and batch uploads — lives on the Payments Service; the operator-facing approval ladder lives on the Recon Service. This page documents the rail-execution layer beneath both.
All routes authenticate with Authorization: Bearer <jwt>.
Rails
A rail is one money-movement counterparty. The configured set is fake, flip, mandiri, bca, uob and danarapay.
disbursementListRails returns the caller-tenant's rails, each with its enabled flag, opaque non-secret settings, and a has_credential boolean. Credentials are encrypted at rest (AES-256-GCM) and never returned in plaintext. disbursementUpsertRail writes one rail's configuration; pass clear_credential to remove a stored secret.
Balance is an optional capability
disbursementRailBalance reads the rail's own funding position. A rail that does not implement it answers 409 rail_capability_unsupported — a console should treat that as "omit the panel", not as an error.
Amounts are decimal strings
Every amount on this service is a decimal string and must not be parsed into a float. The mode travels with the figure because a mock balance and a live one are otherwise indistinguishable.
Billable-lookup invoices
Some rails bill per account-inquiry lookup. Those invoices are held in a local projection so that reading them costs nothing:
disbursementListRailInvoicesreads the projection only. It does not dial the rail, so it is safe on every page load.totalis the count across all pages, not the length of the page returned. Filter bystatus(INITIATED,UNPAID,PAID);limitis bounded to[1,500].disbursementSyncRailInvoicesdials the rail and upserts what it returns. Because it reaches a counterparty it belongs behind an explicit operator action, not a page load. It moves no money.disbursementPayRailInvoicesettles one invoice by explicit operator action. There is no scheduler on this path.
200 on a pay means ACCEPTED, not PAID
The gateway acknowledges with "Request is In progress" while the invoice is still UNPAID. Read the returned status — it is the only authority on settlement — and never infer settlement from the call succeeding.
The invoice must already be present in the projection; an unsynced reference is a 404, so a caller cannot name an arbitrary id and have it paid.
A 409 on pay is refused before any money call, and the codes are deliberately distinct because the operator acts differently on each:
| Code | Meaning |
|---|---|
invoice_already_paid | the invoice is settled and owes nothing |
invoice_pay_in_flight | a pay was already claimed and has not resolved — usually a second click, a second tab, or a colleague. The claim ages out, so a crashed attempt does not make the bill permanently unpayable. |
rail_not_enabled | the rail is off for this tenant |
rail_mode_mismatch | the configured mode disagrees with the counterparty |
rail_capability_unsupported | the rail does not implement invoices |
A 502 means the rail answered with a status outside the known vocabulary.
Transfers
disbursementCreateTransfer submits a transfer through the named rail and records it in the idempotency ledger.
(tenant, transfer_ref) is the idempotency key
A replay with the same transfer_ref and the same payload returns the stored outcome with 200. A reuse of the same transfer_ref with a different payload returns 409 — as does a batch item that is already bound to another transfer. A newly created and submitted transfer returns 201.
Read a transfer back two ways:
disbursementGetTransfer— by the ledger UUID.disbursementGetTransferByRef— bytransfer_ref, which is the recon batch-item id /partner_reference_no. recon-service polls status this way because it does not persist the ledger id;(tenant, transfer_ref)is unique, so this returns at most one row.
Both return 404 when there is no such transfer in the caller's tenant.
Endpoint reference
Generated from the service's embedded OpenAPI spec
(services/disbursement-service/internal/http/openapi/openapi.yaml) — every card links to
the full request/response reference for that operation.
Rail configuration
Per-tenant configuration for one money-movement counterparty. Credentials are AES-256-GCM encrypted at rest and never echoed back.
Rail capabilities
Optional per-rail capabilities. A rail that does not implement one answers 409 rail_capability_unsupported, which a console should treat as "omit the panel".
Billable-lookup invoices
The local projection of a rail's inquiry invoices. Listing never dials the rail; syncing and paying are explicit operator actions. A 200 on pay means accepted, not settled.
Transfers
Idempotent money movement. (tenant, transfer_ref) is the idempotency key: same payload replays, different payload conflicts.
Ungrouped operations
1 operation in the spec is not listed in the groups file. Add its operationId to content/docs/api/disbursement.groups.json.
No operationId overlap with the Payments Service
The merchant-facing disbursement operations grouped on the Payments Service page (listDisbursements, createDisbursement, approveDisbursement, the recipient and batch operations, and the Flip/UOB inbound webhooks) are different operations on a different service. Every operation on this page carries the disbursement* prefix, so no operationId appears in two groups files.
Was this page helpful?
Recon Service API
Reconciliation runs, settlements, disbursements, exceptions, deductions, risk holds, rules, and merchant management
DanaraPay Integration Guide
Registering the DanaraPay tenant credential, routing a payment intent to the danarapay rail, the QRIS flow, and the poll-only acceptance model