Skip to content

Wallet transfers ​

A wallet transfer moves money from one of your customers' wallets to any Neo Wallet, named by its account number, immediately. No payment provider is involved and nothing leaves Neo Wallet.

transferCategoryPaysCounts against
p2pAny Neo Wallet, by its account numberThe sender's daily person-to-person limit
merchantYour business collection walletThe sender's daily merchant-payment limit

Both limits belong to the sender's KYC level and apply on top of the sender's general daily and monthly limits. The receiving wallet takes the amount whatever it already holds: no KYC level or limit applies on the receiving side.

Before you start ​

  • Your account needs the wallet-transfers:write scope. It is not part of any existing account's grants; ask for it.
  • Your wallet profile for the sending customer must allow transfers, and so must the receiving wallet's own profile. A profile is set when you onboard the customer — see Choosing a wallet profile.
  • The sending wallet must be one of your customers'. Anything else is a 404, the same as a wallet that does not exist.

Confirm the payee ​

The customer types the account number of the wallet they want to pay: 13 digits, the last one a check digit. Before they confirm, show them whose wallet it is with GET /wallets/lookup:

GET /api/v1/tenant/wallets/lookup?accountNumber=6695731439148
json
{ "maskedDisplayName": "Abebe B." }
  • The answer is the holder's name, masked — the first name, then the initials of the rest — and nothing else. Receipts show the other side of a transfer the same way.
  • A number that names no wallet able to receive a transfer is a 404.
  • A number that is not 13 digits with a valid check digit is a 400, so a mistyped digit is caught here.
  • Lookups are limited per account per minute. Beyond the limit the answer is 429 until the next minute.
  • Looking the same number up twice within one second signs the same request twice: give the second the next second's timestamp — see What gets a 401.

Make a transfer ​

POST /wallet-transfers with an Idempotency-Key:

json
{
  "sourceWalletUid": "01J0WALLETSENDER0000000000",
  "destinationAccountNumber": "6695731439148",
  "amount": "1000.00",
  "currency": "ETB",
  "transferCategory": "p2p",
  "clientReference": "TRF-2026-000042",
  "assertion": { "...": "the sending customer's authentication" }
}

An account number that is not 13 digits with a valid check digit is a 400 before anything else is checked.

The assertion is the sending customer's, and its transaction binding uses destinationAccountNumber as the target:

canonical = customerUid | sourceWalletUid | amount | currency | destinationAccountNumber | clientReference

The customer therefore authenticates who is paid, not only how much: an assertion made for one account number is refused for another. Above the 2FA threshold of the sender's KYC level, both factors are required.

The answer is 201 with the operation, already completed, carrying sourceWalletUid and destinationAccountNumber. fee and tax are what the sender paid on top of the amount; the receiving wallet gets the amount in full.

When it is refused ​

A transfer is all-or-nothing. Each of these is a 422 and nothing moves:

  • the account number names no wallet, or a wallet that cannot receive money now;
  • merchant names a wallet that is not your business collection wallet, or p2p names a collection wallet;
  • either wallet's profile does not allow transfers;
  • the two wallets are the same, or not in currency;
  • a limit of the sender's KYC level would be exceeded;
  • the sender's available balance is short of the amount plus any fee;
  • the sending wallet or customer is blocked.

Every one of these returns the same 422 body, and none of them describes the receiving customer. A refused transfer can be retried with a new Idempotency-Key once the cause is fixed; retrying with the same key returns the same answer — see Safe retries.

Afterwards ​

  • Webhook — transfer.completed, carrying the same operation as the 201.
  • Your records — the one operation is the sender's debit. When the receiving wallet is also one of your customers', it is that customer's credit too: match it by the account number. There is no statement route: your writes and the webhook feed are the history.
  • Receipts — GET /operations/{uid}/receipt is the sender's receipt, with the fee and tax itemized and the payee's masked name. When the receiving wallet is one of your customers', add ?walletUid= with that wallet for the receiving side's receipt, which shows the amount alone.