Skip to main content
POST
Create a transfer

Idempotency

externalReference is unique within your business and is the idempotency key. Persist it before sending the request, and reuse it on every retry of the same intended payment. Reusing it returns the existing transfer as it stands, without comparing the rest of the payload. Two consequences to design around:
  • A different payment sent under a reference you have already used returns the old transfer and moves no money. Never reuse a reference for a different payment.
  • A transfer that failed keeps its reference. Retrying with the same reference returns the failed transfer, not a new attempt. To genuinely retry a failed payment, mint a new reference — and resolve a fresh destination for it.
A failed create still creates a transfer. If execution fails — most often insufficient_funds — the response is an error, and a transfer row exists in failed status holding that externalReference.

What comes back

A transfer awaiting approval cannot be approved through this API. Approval and rejection are dashboard actions. If your business has an approval policy switched on, API-initiated transfers stop here until someone approves them.
The transient submitting status is never published to a webhook, and you will rarely see it in a response.

Amounts

You send minor units as strings — "2500000" — and get back display decimals — "25000.00". totalDebit is sourceAmount plus fee, and is what actually leaves the account.

Error codes

Authorizations

Authorization
string
header
required

A secret API key, sent as Authorization: Bearer sk_test_… in sandbox or Bearer sk_live_… in live.

Body

application/json
sourceAccountId
string
required

Must be active, debitable, in sourceCurrency, and not a customer wallet.

Required string length: 26
Pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
Example:

"01K2Z3AB4C5D6E7F8G9HJKMNPQ"

destinationId
string
required

An unexpired, unspent destination.

Required string length: 26
Pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
Example:

"01K2Z3AB4C5D6E7F8G9HJKMNPQ"

sourceCurrency
enum<string>
required
Available options:
NGN,
USD,
GBP,
EUR,
GHS,
ZAR,
UGX,
CAD,
MXN,
AED,
USDC
destinationCurrency
enum<string>
required
Available options:
NGN,
USD,
GBP,
EUR,
GHS,
ZAR,
UGX,
CAD,
MXN,
AED,
AUD,
SGD,
HKD,
KES,
TZS,
BRL,
CNY,
XAF,
PHP,
USDC
amountSide
enum<string>
required
Available options:
source,
destination
externalReference
string
required
Required string length: 1 - 120
sourceAmount
string

A positive integer number of minor units.

Pattern: ^[1-9]\d*$
Example:

"2500000"

destinationAmount
string

A positive integer number of minor units.

Pattern: ^[1-9]\d*$
Example:

"2500000"

fxQuoteId
string

Required when the currencies differ, and rejected as a mismatch if it was quoted for a different account, destination, side, or amount.

Required string length: 26
Pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
Example:

"01K2Z3AB4C5D6E7F8G9HJKMNPQ"

narration
string
Maximum string length: 500

Response

Successful response.

id
string
required

A 26-character ULID.

Required string length: 26
Pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
Example:

"01K2Z3AB4C5D6E7F8G9HJKMNPQ"

externalReference
string
required
transferReference
string
required
destinationType
enum<string>
required
Available options:
own_account,
sakawave_tag,
external
sourceAccountId
string
required

A 26-character ULID.

Required string length: 26
Pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
Example:

"01K2Z3AB4C5D6E7F8G9HJKMNPQ"

destinationId
string
required

A 26-character ULID.

Required string length: 26
Pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
Example:

"01K2Z3AB4C5D6E7F8G9HJKMNPQ"

sourceCurrency
enum<string>
required
Available options:
NGN,
USD,
GBP,
EUR,
GHS,
ZAR,
UGX,
CAD,
MXN,
AED,
USDC
destinationCurrency
enum<string>
required
Available options:
NGN,
USD,
GBP,
EUR,
GHS,
ZAR,
UGX,
CAD,
MXN,
AED,
AUD,
SGD,
HKD,
KES,
TZS,
BRL,
CNY,
XAF,
PHP,
USDC
amountSide
enum<string>
required
Available options:
source,
destination
sourceAmount
string
required

A decimal display amount returned by transfer APIs. Preserve it as a string.

Pattern: ^-?\d+(?:\.\d+)?$
Example:

"25000.00"

destinationAmount
string
required

A decimal display amount returned by transfer APIs. Preserve it as a string.

Pattern: ^-?\d+(?:\.\d+)?$
Example:

"25000.00"

fee
string
required

A decimal display amount returned by transfer APIs. Preserve it as a string.

Pattern: ^-?\d+(?:\.\d+)?$
Example:

"25000.00"

totalDebit
string
required

A decimal display amount returned by transfer APIs. Preserve it as a string.

Pattern: ^-?\d+(?:\.\d+)?$
Example:

"25000.00"

routeId
string | null
required

A 26-character ULID.

Required string length: 26
Pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
Example:

"01K2Z3AB4C5D6E7F8G9HJKMNPQ"

scheme
string
required
fxQuoteId
string | null
required

A 26-character ULID.

Required string length: 26
Pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
Example:

"01K2Z3AB4C5D6E7F8G9HJKMNPQ"

destination
object | null
required
status
enum<string>
required
Available options:
awaiting_approval,
submitting,
pending,
completed,
failed,
reversed,
rejected
narration
string | null
required
failureCode
string | null
required
failureReason
string | null
required
initiatedByUserId
string | null
required

A 26-character ULID.

Required string length: 26
Pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
Example:

"01K2Z3AB4C5D6E7F8G9HJKMNPQ"

initiatedByApiKeyId
string | null
required

A 26-character ULID.

Required string length: 26
Pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
Example:

"01K2Z3AB4C5D6E7F8G9HJKMNPQ"

approvedByUserId
string | null
required

A 26-character ULID.

Required string length: 26
Pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
Example:

"01K2Z3AB4C5D6E7F8G9HJKMNPQ"

approvedAt
string<date-time> | null
required
Example:

"2026-08-14T09:00:00.000Z"

completedAt
string<date-time> | null
required
Example:

"2026-08-14T09:00:00.000Z"

createdAt
string<date-time>
required
Example:

"2026-08-14T09:00:00.000Z"

updatedAt
string<date-time>
required
Example:

"2026-08-14T09:00:00.000Z"