Transfers
Move money wallet → wallet inside one org. One synchronous call, one balanced ledger transaction, one transfer.completed webhook. The cleanest money movement Acute does.
A transfer is the atom of the whole system: move amount from one wallet to another,
inside the same org, right now. No bank rail, no async resolver, no waiting. The call
returns completed or it returns an error. There is no in-between, because the ledger
posts in a single transaction or not at all.
If you understand this one posting, every other money movement is a variation on it.
# wallet → wallet transfer
curl -X POST https://sandbox.api.acute.network/v1/wallets/acuinf661043820917wlt/transfer \
-H "Authorization: Bearer acuinf_test_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"destinationWalletId": "acuinf830192847561wlt",
"amount": 100000,
"reason": "Refund of overcharge"
}'The sender is the wallet in the path; the recipient is destinationWalletId. The sender
pays the fee on top of the amount, so a ₦1,000.00 transfer with a ₦15.00 fee debits the
sender ₦1,015.00.
{
"success": true,
"statusCode": 201,
"data": {
"id": "acuinf228940163755trf",
"sourceWalletId": "acuinf661043820917wlt",
"destinationWalletId": "acuinf830192847561wlt",
"amount": 100000,
"fee": 1500,
"status": "completed",
"description": "Refund of overcharge",
"currency": "NGN",
"createdAt": "2026-06-26T12:00:00.000Z"
}
}This is the canonical internal transfer: the shape that virtual_wallet payments and
payout items both reuse. The sender is debited amount + fee; the destination is credited
amount; the fee is remitted. Three legs, balanced to the kobo.
Wallet → wallet transfer
₦1,000.00 moved · ₦15.00 Acute fee
- DRsender pays amount + fee
- CRrecipient receives the amount
- CRAcute fee remitted
The fee is clamp(₦10, 1.5%, ₦100): at least ₦10.00, at most ₦100.00, 1.5% in between. A
₦1,000.00 transfer is 1.5% = ₦15.00, comfortably inside the band. Move ₦100 and you pay
the ₦10.00 floor; move ₦1,000,000 and you pay the ₦100.00 ceiling.
A transfer is synchronous and atomic, so its "lifecycle" is two states with no waiting
room. It posts and it's completed, or the post fails and it's failed.
completedentry statusterminal statusThe ledger transaction posted in full: source debited, destination credited, fee remitted. Returned synchronously from the call.
Transitions out
completedcompletedPosts atomically on a successful POST; there is no pending phase to leave
fires
transfer.completed
Reached from
Failed transfers don't get a webhook
A failed transfer surfaces as a 4xx/422 on the API call itself: you find out
immediately, in the response. The transfer.completed webhook fires only on success.
There is no transfer.failed event, because there's nothing async to tell you about.
On success, we emit:
{
"id": "acuinf340218865093evt",
"type": "transfer.completed",
"createdAt": "2026-06-26T12:00:00.000Z",
"data": {
"id": "acuinf228940163755trf",
"sourceWalletId": "acuinf661043820917wlt",
"destinationWalletId": "acuinf830192847561wlt",
"amount": 100000,
"fee": 1500,
"status": "completed",
"currency": "NGN",
"createdAt": "2026-06-26T12:00:00.000Z"
}
}- Same org only. Both wallets must belong to your organization. Cross-org transfer is impossible by design: money never leaves the tenant it lives in.
- No self-transfer. Source and destination must differ (
422). - Both wallets
tier1. AkycStatus: nonewallet can't send or receive (403WALLET_KYC_REQUIRED). - Tier-1 limit on the destination. End-user wallets carry a held-balance cap; a
transfer that would breach it is rejected (
WALLET_TIER1_LIMIT_EXCEEDED). Use a settlement wallet (exempt) if you're aggregating. Idempotency-Keyrequired. Retry safely with the same key; you'll get the same transfer, never a double.