WalletD developers
Guides

Transfers: wallet to wallet

Wallet to wallet inside one tenant: synchronous settlement, aliases, a fee charged on top of the sender, and the caps that refuse a send.

A transfer moves money between two wallets in the same tenant. It settles synchronously: when the call returns 201, the money has moved and both balances already reflect it. There is no pending state to poll.

Send

curl -sS -X POST "$WALLETD_API/v1/transfers" \
  -H "Authorization: Bearer $WALLETD_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: transfer-order-8891" \
  -d '{"from_user": "'$SENDER'", "to_user": "'$RECIPIENT'", "amount": 1500, "note": "lunch"}'
{
  "id": "019fee21-5e82-78ab-a274-772da3148c72",
  "status": "completed",
  "from_user": "019fee0e-38f0-7630-b211-19ecd59d2825",
  "to_user": "019fee0e-3908-7aad-8b3b-a468e0491f30",
  "amount": 1500,
  "fee": 10,
  "note": "lunch",
  "created_at": "2026-08-11T05:24:18.771Z"
}

Note the arithmetic: the sender is debited 1510 for a 1500 transfer, because this tenant charges a 10-minor-unit P2P fee. The recipient always receives exactly amount; the fee is charged on top, to the sender. Show amount + fee on the sender's confirmation screen or your numbers will not match their balance.

For the full ledger postings behind a transfer, read the transaction back from /v1/transactions or the user's history.

Send to an alias

Users rarely know each other's UUIDs. Send to a handle, phone, or email instead of to_user:

curl -sS -X POST "$WALLETD_API/v1/transfers" \
  -H "Authorization: Bearer $WALLETD_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: transfer-order-8892" \
  -d '{"from_user": "'$SENDER'", "to_alias": {"handle": "ada"}, "amount": 1500}'

to_alias takes exactly one of handle, phone, or email. An alias that matches nothing is 404 recipient_not_found; pass to_user when you already have the id, since it skips a lookup.

Metadata

metadata is a flat string map that rides along with the transaction and comes back on reads. Use it for your own correlation ids:

{
  "from_user": "019f...",
  "to_alias": { "handle": "ada" },
  "amount": 1500,
  "metadata": { "order_id": "8891", "channel": "ios" }
}

Do not put anything secret in it. It is returned to anyone who can read the transaction, which includes support staff in the back office.

What can go wrong

CodeStatusWhat happened
insufficient_funds422Available balance plus any credit line does not cover amount + fee
tier_limit_exceeded422The sender's daily send limit would be breached
self_transfer400Sender and recipient are the same wallet
sender_not_found404from_user is not a user in this tenant
recipient_not_found404to_user or to_alias matched nothing
idempotency_key_reuse409Same key, different parameters. See Idempotency

insufficient_funds and tier_limit_exceeded are decisions, not failures. Show them to the user: one means "top up first", the other means "verify the account to raise the limit".

From a user token

A wallet app usually sends transfers on the user's own token rather than through your backend. The call is identical, but the token confines it: from_user must be that user. Any other value is 403 forbidden, whatever scopes the token holds.

That is the point of user tokens. Your app cannot move someone else's money even if it is compromised. See Authentication.

Velocity limits

Send volume is summed per user per day and checked inside the transfer's own transaction, so eight simultaneous sends against a five-send cap land exactly five. You cannot beat it by racing. See Limits.

Next

On this page