Skip to content
StraventaDocs
Now in Sandbox.
API Reference

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:

  • disbursementListRailInvoices reads the projection only. It does not dial the rail, so it is safe on every page load. total is the count across all pages, not the length of the page returned. Filter by status (INITIATED, UNPAID, PAID); limit is bounded to [1,500].
  • disbursementSyncRailInvoices dials 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.
  • disbursementPayRailInvoice settles 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:

CodeMeaning
invoice_already_paidthe invoice is settled and owes nothing
invoice_pay_in_flighta 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_enabledthe rail is off for this tenant
rail_mode_mismatchthe configured mode disagrees with the counterparty
rail_capability_unsupportedthe 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 — by transfer_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?

On this page