curl --request POST \
--url https://sandbox-api.sakawave.example/v1/transfer-destinations \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"type": "external",
"routeId": "01K2Z3AB4M5N6P7Q8RSTVWXYZA",
"currency": "KES",
"country": "KE",
"accountName": "Ada Okafor",
"recipientType": "individual",
"recipientRelationship": "vendor",
"recipientEmail": "ada@example.com",
"recipientAddressLine1": "14 Marina Road",
"recipientCity": "Nairobi",
"bankCode": "001",
"accountNumber": "0123456789",
"saveRecipient": false
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
type: 'external',
routeId: '01K2Z3AB4M5N6P7Q8RSTVWXYZA',
currency: 'KES',
country: 'KE',
accountName: 'Ada Okafor',
recipientType: 'individual',
recipientRelationship: 'vendor',
recipientEmail: 'ada@example.com',
recipientAddressLine1: '14 Marina Road',
recipientCity: 'Nairobi',
bankCode: '001',
accountNumber: '0123456789',
saveRecipient: false
})
};
fetch('https://sandbox-api.sakawave.example/v1/transfer-destinations', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://sandbox-api.sakawave.example/v1/transfer-destinations"
payload = {
"type": "external",
"routeId": "01K2Z3AB4M5N6P7Q8RSTVWXYZA",
"currency": "KES",
"country": "KE",
"accountName": "Ada Okafor",
"recipientType": "individual",
"recipientRelationship": "vendor",
"recipientEmail": "ada@example.com",
"recipientAddressLine1": "14 Marina Road",
"recipientCity": "Nairobi",
"bankCode": "001",
"accountNumber": "0123456789",
"saveRecipient": False
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "01K2Z3AB4F5G6H7J8KMNPQRSTV",
"type": "own_account",
"currency": "NGN",
"displayName": "Treasury NGN",
"routeId": null,
"recipientId": null,
"institutionName": null,
"accountNumberLast4": null,
"expiresAt": "2026-08-15T09:05:00.000Z"
}{
"statusCode": 400,
"message": "Must be a valid identifier.",
"code": "validation_failed",
"errors": [
{
"message": "Must be a valid identifier.",
"field": "accountId",
"code": "custom",
"path": [
"accountId"
]
}
]
}{
"statusCode": 401,
"code": "INVALID_API_KEY",
"message": "Invalid or inactive API credentials."
}{
"statusCode": 404,
"code": "resource_not_found",
"message": "Resource not found."
}{
"statusCode": 409,
"code": "resource_conflict",
"message": "The request conflicts with the current resource state."
}{
"statusCode": 422,
"message": "Payment detail validation failed.",
"code": "payment_detail_invalid",
"errors": [
{
"message": "Account number is required.",
"field": "accountNumber",
"code": "required",
"path": [
"fields",
"accountNumber"
]
}
]
}{
"statusCode": 429,
"message": "Too many API requests. Try again later.",
"code": "api_key_rate_limit_exceeded",
"retryAfterSeconds": 17
}{
"statusCode": 500,
"code": "internal_server_error",
"message": "An unexpected error occurred."
}{
"statusCode": 503,
"code": "provider_unavailable",
"message": "We could not issue an account number just now. Try again shortly."
}Create a transfer destination
Resolves where money is going, and proves it can get there. type selects one of three shapes: your own account, a Sakawave tag, or an external account described inline. The result expires after 24 hours and can be spent once.
curl --request POST \
--url https://sandbox-api.sakawave.example/v1/transfer-destinations \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"type": "external",
"routeId": "01K2Z3AB4M5N6P7Q8RSTVWXYZA",
"currency": "KES",
"country": "KE",
"accountName": "Ada Okafor",
"recipientType": "individual",
"recipientRelationship": "vendor",
"recipientEmail": "ada@example.com",
"recipientAddressLine1": "14 Marina Road",
"recipientCity": "Nairobi",
"bankCode": "001",
"accountNumber": "0123456789",
"saveRecipient": false
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
type: 'external',
routeId: '01K2Z3AB4M5N6P7Q8RSTVWXYZA',
currency: 'KES',
country: 'KE',
accountName: 'Ada Okafor',
recipientType: 'individual',
recipientRelationship: 'vendor',
recipientEmail: 'ada@example.com',
recipientAddressLine1: '14 Marina Road',
recipientCity: 'Nairobi',
bankCode: '001',
accountNumber: '0123456789',
saveRecipient: false
})
};
fetch('https://sandbox-api.sakawave.example/v1/transfer-destinations', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://sandbox-api.sakawave.example/v1/transfer-destinations"
payload = {
"type": "external",
"routeId": "01K2Z3AB4M5N6P7Q8RSTVWXYZA",
"currency": "KES",
"country": "KE",
"accountName": "Ada Okafor",
"recipientType": "individual",
"recipientRelationship": "vendor",
"recipientEmail": "ada@example.com",
"recipientAddressLine1": "14 Marina Road",
"recipientCity": "Nairobi",
"bankCode": "001",
"accountNumber": "0123456789",
"saveRecipient": False
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "01K2Z3AB4F5G6H7J8KMNPQRSTV",
"type": "own_account",
"currency": "NGN",
"displayName": "Treasury NGN",
"routeId": null,
"recipientId": null,
"institutionName": null,
"accountNumberLast4": null,
"expiresAt": "2026-08-15T09:05:00.000Z"
}{
"statusCode": 400,
"message": "Must be a valid identifier.",
"code": "validation_failed",
"errors": [
{
"message": "Must be a valid identifier.",
"field": "accountId",
"code": "custom",
"path": [
"accountId"
]
}
]
}{
"statusCode": 401,
"code": "INVALID_API_KEY",
"message": "Invalid or inactive API credentials."
}{
"statusCode": 404,
"code": "resource_not_found",
"message": "Resource not found."
}{
"statusCode": 409,
"code": "resource_conflict",
"message": "The request conflicts with the current resource state."
}{
"statusCode": 422,
"message": "Payment detail validation failed.",
"code": "payment_detail_invalid",
"errors": [
{
"message": "Account number is required.",
"field": "accountNumber",
"code": "required",
"path": [
"fields",
"accountNumber"
]
}
]
}{
"statusCode": 429,
"message": "Too many API requests. Try again later.",
"code": "api_key_rate_limit_exceeded",
"retryAfterSeconds": 17
}{
"statusCode": 500,
"code": "internal_server_error",
"message": "An unexpected error occurred."
}{
"statusCode": 503,
"code": "provider_unavailable",
"message": "We could not issue an account number just now. Try again shortly."
}Three shapes, one endpoint
type | Send | Resolves to |
|---|---|---|
own_account | accountId, currency | One of your own accounts. |
sakawave_tag | tag | Any Sakawave account, by its public handle. |
external | routeId, currency, accountName, plus the route’s routing fields | A bank or mobile-money account elsewhere. |
Every “not found” looks the same
Forown_account and sakawave_tag, one code covers every reason the target cannot receive: it does not exist, it is not active, it cannot be credited, its currency differs, or — for your own accounts — it is a customer wallet or is not yet transferReady.
{ "statusCode": 404, "message": "Destination account not found.", "code": "destination_account_not_found" }
status, canCredit, currency, and transferReady yourself.
own_account to pay a customer wallet. Wallets are excluded from that shape; pay them by sakawave_tag.The counterparty profile
On an external route that settles through the international provider, four fields are required by the payout rail even though the parser treats them as optional:recipientRelationship, recipientEmail, recipientAddressLine1, and recipientCity, plus a resolved country. Omitting them fails with 422 recipient_profile_incomplete, listing each one.
Domestic NGN routes do not ask for them.
Verification
Where the route supports it, Sakawave looks the account up with the provider and replacesaccountName with the verified name. The displayName you get back is therefore the name the bank holds, not the name you sent — show it to the payer before they confirm.
Error codes
| Status | code | Cause |
|---|---|---|
400 | validation_failed | A field failed validation, or a field this shape does not accept was sent. |
404 | destination_account_not_found | The own-account or tag target cannot receive. See above. |
404 | transfer_route_not_found | No active route with that id delivers to that currency. |
409 | currency_capability_unavailable | The destination currency is closed for this kind of transfer. |
409 | route_country_mismatch | The route does not serve the country you named. |
409 | destination_country_required | A bank-transfer route needs a destination country. |
409 | destination_field_required | A field the route requires was not sent. The field name is in field. |
409 | transfer_provider_unavailable | No provider is currently bound to the route. |
422 | recipient_profile_incomplete | One or more counterparty fields are missing; each is listed in errors. |
503 | destination_provider_unavailable | The route is bound to a provider that cannot serve it. |
503 | provider_unavailable | The provider could not be reached. Retry shortly. |
401 | INVALID_API_KEY | The key is missing, invalid, inactive, expired, from the other environment, or blocked by its IP allowlist. |
429 | api_key_rate_limit_exceeded | The per-minute limit for this key was exceeded. |
Authorizations
A secret API key, sent as Authorization: Bearer sk_test_… in sandbox or Bearer sk_live_… in live.
Body
Response
Successful response.
Send this as destinationId when previewing or creating a transfer.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
own_account, sakawave_tag, external NGN, USD, GBP, EUR, GHS, ZAR, UGX, CAD, MXN, AED, AUD, SGD, HKD, KES, TZS, BRL, CNY, XAF, PHP, USDC The verified account name for an external destination, the account name for your own, or the resolved holder for a tag.
The payout route, for external destinations.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
The saved recipient this was built from, when it was.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
The only part of the destination account number ever returned.
4"2026-08-14T09:00:00.000Z"