Skip to main content
Creating a transfer is a small call. Getting to the point where you can make it is most of the integration, and that is deliberate: every check that can happen before money moves does.

What each step buys

Skip preview and you are asking a payer to approve an amount you have not calculated. Skip the destination and there is nothing to spend — a transfer takes a destinationId, never an account number.

Where things fail

Failures cluster, and knowing which step owns which failure makes them much easier to handle. The middle row is the one to plan for. Preview does not check the balance, so a preview can succeed and the transfer can still fail with insufficient_funds a moment later.

Same currency or not

The two paths differ by one object.
Omit fxQuoteId when the currencies differ and you get 409 fx_quote_required. Send one that was quoted for a different account, destination, side, or amount and you get a mismatch.

Where the money goes

Three destination shapes, decided when the destination was created, not here:
  • own_account — between your own accounts. Settles internally, usually completed immediately.
  • sakawave_tag — to any Sakawave account by its public handle, including your customers’ wallets.
  • external — out to a bank or mobile-money account, over a route. This is the one that takes time and can fail at a provider.
The transfer object reports which it was in destinationType, and carries a snapshot of the destination in destination — so history stays readable even after a recipient is deleted.

Asynchronous by default

An external transfer returns pending. It has been accepted and submitted; settlement happens afterwards. Do not poll it in a loop. Subscribe to fiat.withdrawal.completed and fiat.withdrawal.failed, and treat GET /v1/transfers/{transferId} as the authority when you need to check — a webhook can arrive out of order, the resource cannot be wrong.

The shape of the API

There is no cancel, no amend, and no approve. A transfer is created and then observed.