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
| Code | Status | What happened |
|---|---|---|
insufficient_funds | 422 | Available balance plus any credit line does not cover amount + fee |
tier_limit_exceeded | 422 | The sender's daily send limit would be breached |
self_transfer | 400 | Sender and recipient are the same wallet |
sender_not_found | 404 | from_user is not a user in this tenant |
recipient_not_found | 404 | to_user or to_alias matched nothing |
idempotency_key_reuse | 409 | Same 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
- Payments and refunds for paying a merchant rather than a person.
- Limits for the caps.