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.

201 Created
json
{
  "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

ledger_transaction kind: transfer. There is no provider leg; nothing leaves Acute, so there is no bank rail and no provider fee.

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 status

The ledger transaction posted in full: source debited, destination credited, fee remitted. Returned synchronously from the call.

Transitions out

  • completedcompleted

    Posts atomically on a successful POST; there is no pending phase to leave

    firestransfer.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:

webhook: transfer.completed
json
{
  "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. A kycStatus: none wallet can't send or receive (403 WALLET_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-Key required. Retry safely with the same key; you'll get the same transfer, never a double.