Appearance
Payouts
Pay out to the customer's own verified contact identifier
POST
/api/v1/tenant/payoutsScope payouts:write · Write — send Idempotency-Key and X-Request-Id
Sends money from a customer's wallet to one of the customer's own verified contact identifiers through a payment provider. railCode names the provider; one that cannot take the payout is refused with 422 RailUnavailable. The assertion must be bound to this request (see the Customer authentication guide), and the wallet's available balance must cover amount + fee + tax. On acceptance the wallet is debited for the total, with the charges itemized, and the response is the operation in state processing. The outcome follows on your webhook: payout.confirmed, payout.failed (amount and refundable charges returned to the wallet) or payout.uncertain.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
walletUid | string | yes | The wallet to pay out from, as returned by POST /tenant/customers. |
destinationIdentifierUid | string | yes | A verified contact identifier of the wallet's customer (payoutEligible on GET /tenant/customers/{uid}/identifiers); anything else is refused. |
amount | string | yes | A positive amount with at most two decimal places; the amount the destination receives. |
currency | string | yes | ISO-4217; must equal the wallet's currency. |
railCode | string | yes | Code of the payment provider to pay out through; it must support payouts. At most 64 characters. |
clientReference | string | yes | Your own reference; stored and returned verbatim, never interpreted; unique within your account. |
assertion | object | yes | The customer's authentication for this payout. |
assertion.customerUid | string | yes | The customer who authenticated. |
assertion.identifierUid | object | no | The customer identifier the customer authenticated with, if known. |
assertion.assuranceLevel | enum | yes | How strongly your channel authenticated the customer. One of single_factor, two_factor, step_up. |
assertion.factor1Type | string | yes | The first factor used, e.g. pin, password, biometric. At most 32 characters. |
assertion.factor2Type | object | no | Required unless assuranceLevel is single_factor. At most 32 characters. |
assertion.authenticatedAt | string | yes | ISO-8601 — when the customer authenticated. |
assertion.expiresAt | string | yes | ISO-8601; by default at most 10 minutes after authenticatedAt. An expired assertion is refused (422). |
assertion.channelCode | string | yes | Your channel the customer authenticated on. At most 40 characters. |
assertion.deviceHash | object | no | SHA-256 hex of the device fingerprint, if your channel has one. |
assertion.sourceReference | string | yes | Opaque channel evidence reference (a session or login id). Presenting one again while it is live is allowed only for the same customer. At most 160 characters. |
assertion.transactionBinding | object | no | Hash your channel computed over the request this assertion authorizes; a mismatch with the request received is refused. |
assertion.transactionBinding.hash | string | yes | SHA-256 hex (64 characters) over the canonical string of this request. |
assertion.transactionBinding.algorithm | enum | yes | One of sha256. |
assertion.transactionBinding.canonical | enum | yes | The field template hashed: the six values joined with |, UTF-8. The fifth is the payout destination, the mandate for a debit, or empty; clientReference is empty when absent. One of `customerUid |
feeQuoteId | object | no | A quoteId from POST /tenant/fees/quote. While the quote is valid the payout is charged the quoted amounts; if they no longer apply, or the quote was made for a different request, the payout is refused (422). An expired quote is ignored and the current charges apply. At most 64 characters. |
Example (placeholder values)
json
{
"walletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"destinationIdentifierUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"amount": "2500.00",
"currency": "ETB",
"railCode": "string",
"clientReference": "string",
"assertion": {
"customerUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"assuranceLevel": "single_factor",
"factor1Type": "string",
"authenticatedAt": "2026-09-03T10:15:30.000Z",
"expiresAt": "2026-09-03T10:15:30.000Z",
"channelCode": "string",
"sourceReference": "string"
}
}Response · 202
| Field | Type | Description |
|---|---|---|
uid | string | |
type | string | Operation type, e.g. wallet_credit, wallet_debit, payout_b2c or topup_c2b. |
state | string | Operation state: created, awaiting_authorization, authorized, held, processing, pending_provider, confirmed, failed, reversed or completed. |
amount | string | A decimal amount with two decimal places. |
fee | string | A decimal amount with two decimal places. |
tax | string | A decimal amount with two decimal places. |
currency | string | ISO-4217. |
clientReference | string | null | Your reference, exactly as you supplied it. |
sourceWalletUid | string | null | Wallet transfers only: the wallet the money left. |
destinationWalletUid | string | null | Wallet transfers only: the wallet the money reached. |
createdAt | string | |
completedAt | string | null | When the operation first reached a final state. |
links | object | |
links.self | string | API-relative path of this operation (GET /tenant/operations/{uid}). |
links.receipt | string | API-relative path of its receipt (GET /tenant/operations/{uid}/receipt). |
Example (placeholder values)
json
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"type": "string",
"state": "string",
"amount": "2500.00",
"fee": "12.50",
"tax": "1.88",
"currency": "ETB",
"clientReference": "string",
"sourceWalletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"destinationWalletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"createdAt": "2026-09-03T10:15:30.000Z",
"completedAt": "2026-09-03T10:15:30.000Z",
"links": {
"self": "string",
"receipt": "string"
}
}Errors
400 401 403 404 409 422 429 500 503 — one body shape for all of them, see Errors.