Appearance
Wallet transfers
Transfer money between two of your customers' wallets
POST
/api/v1/tenant/wallet-transfersScope wallet-transfers:write · Write — send Idempotency-Key and X-Request-Id
Moves the amount from one customer's wallet to another wallet of yours immediately: another customer's wallet (p2p) or your business collection wallet (merchant). No payment provider is involved. The assertion is the sending customer's, is validated on every call and must be bound to this request, with destinationWalletUid as the binding target (see the Customer authentication guide); above the 2FA threshold of the sender's KYC level both factors are required. Both wallets must be ones you can see — anything else is a 404 — and both wallets' profiles must allow transfers. The transfer is all-or-nothing: a limit, a profile, a full destination wallet or available funds short of the amount plus any fee are a 422 and nothing moves. The response state is terminal immediately and transfer.completed is delivered to your webhook.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
sourceWalletUid | string | yes | The wallet to take the money from; one of your customers' wallets. |
destinationWalletUid | string | yes | The wallet to pay; one of your customers' wallets, or your business collection wallet for a merchant transfer. |
amount | string | yes | A positive amount with at most two decimal places. |
currency | string | yes | ISO-4217; must equal both wallets' currency. |
transferCategory | enum | yes | p2p pays another customer's wallet; merchant pays a business collection wallet. Each counts against its own daily limit of the sender's KYC level. One of p2p, merchant. |
clientReference | string | yes | Your own reference; stored and returned verbatim, never interpreted; unique within your account. |
assertion | object | yes | The sending customer's authentication for this transfer. |
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 |
Example (placeholder values)
json
{
"sourceWalletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"destinationWalletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"amount": "1000.00",
"currency": "ETB",
"transferCategory": "p2p",
"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 · 201
| 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.