Skip to main content

Payment — moving money

Every movement produces balanced double-entry ledger records. There is no endpoint that changes a balance without writing a ledger pair, which is why a balance and its history can never disagree.

Money in — POST /issuances

One wallet on one side, the SYSTEM wallet on the other, so the entry balances.

POST /issuances
Idempotency-Key: <uuid>

{
"type": "NET_EARNINGS",
"walletId": 4021,
"amount": 148.50,
"fundType": "TRANSFERABLE",
"referenceId": "trip-88213",
"description": "Trip payout"
}

type accepts NET_EARNINGS, TIP, BONUS, REIMBURSEMENT and OFFLINE_TOP_UP. Sending a deduction type here is refused with a 422 naming the endpoint it belongs on.

referenceId is your correlation id — your trip, order or invoice number. You will use it to reconcile in the next chapter, so set it on everything.

Money out — POST /deductions

The same shape, in the other direction. type accepts CASH_TRANSACTION_DEDUCTION and CASH_TRANSACTION_REPLACEMENT.

Two rails are deliberately not reachable here: a customer top-up goes through the payment gateway, and a payout through withdrawal. Asking for TOP_UP or WITHDRAWAL on either endpoint is a 422 that points you at the right one.

Which types you may post at all is an allowlist configured per tenant, with per-transaction and daily caps. Posting a type you are not enabled for is 422, and so is breaching a cap. That is deliberate: it bounds how much e-money you can instruct us to issue.

There is no general POST /transactions

There was, and it is gone. A single write taking a free-text transactionType meant the endpoint could not tell you which direction the money went until it had parsed the type. The typed pair above is direction-consistent by construction, and the rails that need their own lifecycle — top-up and withdrawal — are not reachable through either.

Wallet to wallet — POST /transfers

POST /transfers
Idempotency-Key: <uuid>

{ "fromWalletId": 4021, "toWalletId": 4022, "amount": 50.00 }

Posts two linked transactions sharing one idempotency key. Either both land or neither does.

Undoing — POST /transactions/{id}/reverse

A reversal is a new compensating transaction, never an edit. The original keeps its history and gains a REVERSED status; the new one carries reversalOf. Ledger entries are append-only — nothing in this system is ever updated or deleted.

Adjustments

POST /admin/adjustments
Idempotency-Key: <uuid>

{ "walletId": 4021, "amount": 20.00, "direction": "CREDIT", "reason": "Goodwill — ticket 8821" }

reason is mandatory and lands in the audit log.

direction: "DEBIT" is not implemented

It returns 422. An adjustment always credits the wallet in v1.

To take money out, post an ordinary debit transaction with the right type, or reverse the original transaction. Both leave a better audit trail than an adjustment would.

Failures worth handling

WhenDo
422 balance floorThe debit would take the fund below its floorSurface "insufficient balance". Whether a wallet may go negative, and how far, is per-tenant configuration
422 type not enabledThe transaction type is not on your allowlistConfiguration, not code — talk to us
422 cap exceededPer-transaction or daily issuance capRetrying will not help today
422 wallet frozenThe wallet is not ACTIVEUnfreeze, or fail the operation
409Same key still in flightBack off, retry with the same key

What is true afterwards

GET /transactions/{id} shows COMPLETED; the wallet balance has moved by exactly the amount; and GET /ledger/entries contains a balanced debit/credit pair for it. Assert the ledger, not just the balance — that is the check that catches a genuine bug.

Next: Ledger.