Payout API
ENDPOINTS:
POST /v1/payout
GET /v1/payout/:id
GET /v1/payouts/search
POST /v1/payout-batch
GET /v1/payouts-batch/:id
POST /v1/payout/:id/reverse
Wave's Payout API provides a programmatic way to send money from your business to one or more recipients. Payout recipients are identified by mobile, so using the API is as simple as telling Wave:
"Send amount
to mobile number
."
See also:
- Balance API to query the balance of your business wallet.
- Checkout API to securely receive payments from customers.
Base URL
All the endpoint paths referenced in the API document are relative to a base URL, https://api.wave.com.
Authentication
Authenticating to the API is done via API keys. These keys are bound to a single business wallet and can only interact with it.
If you need to interact with more than one wallet in your network, then you will have to obtain one key per wallet.
Each request must be sent over HTTPS and contain an authorization header specifying the bearer scheme with the api key:
Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6
Note that the actual key is much longer but for documentation purposes most of the characters have been replaced by an ellipses.
Obtaining your API key
You can manage API keys in the developer's section in the Wave Business Portal. You can create, view, and revoke keys, and define which specific APIs each key has access to.
When you create a new API key, you will only see the full key once. Make sure you copy it without missing any characters, since it will be masked afterwards for security reasons.
Wave doesn't know your full key, but if you contact API support we can identify them by the last 4 letters that you see displayed in the business portal.
Error codes
In the authentication phase, one of the following errors can cause your request to return early:
Status | Code | Descriptions |
---|---|---|
401 | missing-auth-header |
Your request should include an HTTP auth header. |
401 | invalid-auth |
Your HTTP auth header can't be processed. |
401 | api-key-not-provided |
Your request should include an API key. |
401 | no-matching-api-key |
The key you provided doesn't exist in our system. |
401 | api-key-revoked |
Your API key has been revoked. |
403 | invalid-wallet |
Your wallet can't be used in this particular API. |
403 | disabled-wallet |
Your wallet has been temporarily disabled. You should still be able to use the endpoint to check your balance. |
Idempotency
Idempotency-Key: 65f735b4-b44b-429d-b0a8-550701e2393a
Every request that modifies data must provide an idempotency key in order to guarantee safe retries without accidentally sending money twice. It identifies what you consider the same request.
Generating random strings
Shell
openssl rand -hex 8
PHP
$idem_key = uniqid(more_entropy: True)
Python
import uuid
idem_key = str(uuid.uuid4())
JavaScript
let idem_key = crypto.randomUUID();
- If you want to retry a request that you've sent before: use the same idempotency key.
- If you want to send a new, different request: use a new random string as the idempotency key.
The idempotency key is passed in the HTTP header of a request, in the form Idempotency-Key: <STRING_OF_YOUR_CHOICE>
. You will find complete request examples in the API section.
The idempotency key is generated by you, the client. It should be a unique value of up to 255 characters. We suggest using v4 UUIDs or other random strings with sufficient entropy to avoid collisions.
We suggest waiting a few seconds between retries. Please refer to the Retries section for more detailed information on what and how to implement retries.
Rate limiting
Wave APIs are rate limited to prevent abuse that would degrade performance for all users. If you send many requests in a short period of time, you may receive 429 error responses.
API
Create a payout
POST /v1/payout
curl -X POST \
--url https://api.wave.com/v1/payout \
-H 'Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6' \
-H 'Content-Type: application/json' \
-H 'idempotency-key: 65f735b4-b44b-429d-b0a8-550701e2393a' \
-d '{ "currency": "XOF",
"receive_amount": "500",
"name": "Fatou Ndiaye",
"mobile": "+221555110219"
}'
Send a single payout, i.e. transfer money from your business wallet to a specific recipient, identified by phone number. Executes synchronously, meaning that the Wave will attempt to execute the transaction immediately and return the result in the response of this request.
Parameters
Key | Type | Description |
---|---|---|
aggregated_merchant_id |
String (required for Aggregators) | The id of an aggregated merchant identity to use for this payout. This is only available to specific businesses and will return an error if you provide it but don't have permission. If you would like to use this feature, please contact your Wave support representative. |
client_reference |
String (optional, up to 255 characters) | A unique string that you provide which can be used to correlate the payout in your system. |
currency |
Currency code | The amount currency. Note that your business wallet and the recipient must be in the same country. |
mobile |
Phone number | The recipient mobile phone number. |
name |
String (optional), up to 255 characters | The recipient name, may be used for user verification. |
national_id |
String (optional, up to 255 characters) | An optional field to save the recipient's national ID, for further user verification. |
payment_reason |
String (optional), up to 40 characters | An optional message with a payment reason that is shown to customers in the payment receipt. |
receive_amount |
Amount | The amount to be paid out to the recipient, net of fees. |
Successful result example
{
"id": "pt-185b5e4b8100c",
"currency": "XOF",
"receive_amount": "500",
"fee": "5",
"mobile": "+221555110219",
"name": "Fatou Ndiaye",
"status": "succeeded",
"timestamp": "2022-06-20T17:17:11Z"
}
In case there aren't any validation or pre-check errors, posting a payout returns the same fields that you submitted in addition to the following:
Key | Type | Description |
---|---|---|
id |
String | A unique identifier for the payout object. Up to 20 characters. |
fee |
Amount | The fee for sending the payout. |
payout_error |
Payout error (optional) | Details about the reason for a failed payout. Only populated when the status is failed . |
status |
Payout state | The status of the payout: processing , failed , or succeeded . |
timestamp |
Timestamp | The time and date that this payout request was recorded in our system. Note that this doesn't describe when payments were executed, only when they were submitted. |
In case the request fails to pass validation rules or other checks where no Payout object can be created, then a plain top-level error is returned with the fields code
and message
.
Error result example
{
"code": "request-validation-error",
"message": "An 'Idempotency-Key' header is required for POST requests"
}
See the Errors section for a complete list and explanation of each possible error code.
Retrieve a payout
GET /v1/payout/:id
curl -X GET \
--url https://api.wave.com/v1/payout/pt-185sewgm8100t \
-H 'Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6'
Example response
{
"id": "pt-185sewgm8100t",
"currency": "XOF",
"receive_amount": "15000",
"fee": "150",
"mobile": "+221555110233",
"name": "Moustapha Mbaye",
"national_id": "1751197904376",
"client_reference": "FAH.4827.1734",
"payment_reason": "Salary November 2022",
"status": "succeeded",
"timestamp": "2022-06-21T09:56:29Z"
"aggregated_merchant_id": "am-7lks22ap113t4",
}
Retrieves a single payout.
Return attributes
Key | Type | Description |
---|---|---|
id |
String | A unique identifier for the payout object. Up to 20 characters. |
currency |
Currency code | The amount currency. Note that your business wallet and the recipient must be in the same country. |
fee |
Amount | The fee for sending the payout. |
mobile |
Phone number | The recipient mobile phone number. |
name |
String (optional), up to 255 characters | The recipient name, may be used for user verification. |
national_id |
String (optional), up to 255 characters | An optional field to save the recipient's national ID, for further user verification. |
payout_error |
Payout error (optional) | Details about the reason for a failed payout. Only populated when the status is failed . |
receive_amount |
Amount | The amount to be paid out to the recipient, net of fees. |
status |
Payout state | The status of the payout: processing , failed , or succeeded . |
timestamp |
Timestamp | The time and date that this payout request was recorded in our system. Note that this doesn't describe when payments were executed, only when they were submitted. |
client_reference |
String (optional), up to 255 characters | A unique string that you provide which can be used to correlate the payout in your system. |
payment_reason |
String (optional), up to 40 characters | An optional message with a payment reason that is shown to customers in the payment receipt. |
aggregated_merchant_id |
String (optional) | The aggregated merchant ID used for this payout, if any. |
Search payouts
GET /v1/payouts/search
curl -X GET \
--url https://api.wave.com/v1/payouts/search?client_reference=FAH.4827.1734 \
-H 'Authorization : Bearer wave_sn_prod_YhUNb9d...i4bA6'
Example response
{
"result": [
{
"id": "pt-185sewgm8100t",
"currency": "XOF",
"receive_amount": "15000",
"fee": "150",
"mobile": "+221555110233",
"name": "Moustapha Mbaye",
"national_id": "1751197904376",
"client_reference": "FAH.4827.1734",
"payment_reason": "Salary November 2022",
"status": "succeeded",
"timestamp": "2022-06-21T09:56:29Z"
"aggregated_merchant_id": "am-7lks22ap113t4",
}
]
}
Retrieves a list of payouts based on the provided query parameters. Currently supports only search by client reference (client_reference
).
Return attributes
Key | Type | Description |
---|---|---|
result |
List of payouts | The list of payouts that match the search criteria. |
Create a payout batch
POST /v1/payout-batch/
curl -X POST \
--url https://api.wave.com/v1/payout-batch \
-H "authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6" \
-H 'content-type: application/json' \
-H 'idempotency-key: 65f735b4-b44b-429d-b0a8-550701e2393a' \
-d '{"payouts": [
{ "currency": "XOF",
"receive_amount": "1000",
"name": "Fatou Ndiaye",
"mobile": "+221555110219"
},
{ "currency": "XOF",
"receive_amount": "1200",
"name": "Moustapha Mbaye",
"mobile": "+221555110233",
"aggregated_merchant_id": "am-7lks22ap113t4",
},
{ "currency": "XOF",
"receive_amount": "16000",
"name": "Mame Diop",
"mobile": "+221555144081"
}
]}'
Example response
{
"id": "pb-185skxq8g1006"
}
Submitting a payout batch means that you request one or multiple payouts to be executed. The processing of these transactions is asynchronous, meaning that this endpoint will not immediately return the resulting payouts.
Instead, you receive an ID that you can then use to retrieve the payout batch result, to see which transactions have completed successfully. We recommend that you poll this endpoint every couple of seconds, depending on the size of your batch.
Parameters
Key | Type | Description |
---|---|---|
payouts | List of payout requests | The list of payouts to be sent. Each item should have the same structure used for a single payout request. |
Return attributes
Key | Type | Description |
---|---|---|
id | String | The ID of the payout batch that you can use to poll the get payout batch endpoint. |
Retrieve a payout batch
GET /v1/payout-batch/:id
curl -X GET \
--url https://api.wave.com/v1/payout-batch/pb-185skxq8g1006 \
-H 'Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6'
Example response
{
"id": "pb-185skxq8g1006",
"status": "complete",
"payouts": [
{
"id": "pt-185skxq9g100w",
"currency": "XOF",
"receive_amount": "1000",
"fee": "10",
"mobile": "+221555110219",
"name": "Fatou Ndiaye",
"status": "succeeded",
"timestamp": "2022-06-21T10:07:30Z"
},
{
"id": "pt-185skxqa0100y",
"currency": "XOF",
"receive_amount": "1200",
"fee": "10",
"mobile": "+221555110233",
"name": "Moustapha Mbaye",
"status": "processing",
"aggregated_merchant_id": "am-7lks22ap113t4",
"timestamp": "2022-06-21T10:07:30Z"
},
{
"id": "pt-185sw98jg1016",
"currency": "XOF",
"receive_amount": "16000",
"fee": "160",
"mobile": "+221555144081",
"name": "Mame Diop",
"status": "failed",
"payout_error": {
"error_code": "recipient-limit-exceeded",
"error_message": "The recipient has reached their monthly limit."
},
"timestamp": "2022-06-21T10:25:46Z"
}
]
}
Retrieves a payout batch.
Return attributes
Key | Type | Description |
---|---|---|
id | String | The ID of the payout batch. |
payouts |
List of payout results. | The list of payouts, with each item having the same structure as a single retrieved payout. |
status |
Payout batch state | The status of the payout batch: processing or complete . |
There is no success
or failed
status on payout batches, because within the same batch, various individual payouts can succeed or fail. In order to handle errors, each payout in payouts
should be inspected to check if it contains a payout_error
field.
Reverse a payout
POST /v1/payout/:id/reverse
curl -X POST \
--url https://api.wave.com/v1/payout/pt-185sewgm8100t/reverse \
-H 'Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6'
Example response
200 OK
Reverses a previously executed payout, including fees. Currently there is an exact 3 days limit (to allow 1 day margin after the weekend) for reversing payouts, counting from the time they were created. The reference for this is the timestamp
field on the Payout object.
This endpoint's idempotency means that if you try to reverse an already reversed payout, then you will get a success return code, but no additional transaction will be created. You can therefore never reverse a payout twice by accident.
Return attributes
None
If the reversal succeeds, this endpoint returns a 200 HTTP code, and no body.
If the reversal fails, an HTTP code above 400 is returned:
error_code |
explanation |
---|---|
insufficient-funds |
The recipient wallet doesn't have enough balance to cover the reversal |
payout-reversal-time-limit-exceeded |
The time window for reversing a payout has passed. |
payout-reversal-account-terminated |
The recipient wallet has been terminated in Wave system. |
not-found |
No payout was found under the wallet linked to your API key. |
Every payout can only be reversed once, but since the endpoint is idempotent you can safely retry.
Types
Amount
All amounts are represented as a string. The amount has to be a round number, so it cannot contain decimal places. The following rules apply to valid amounts:
- No leading zeroes where the value is one or greater.
- Must be positive for requests.
Currency code
Standard ISO 4217 three-letter codes in
upper case are used to specify currency. Note: the code for the West African Franc is XOF
, not CFA
.
Phone number
Phone numbers follow the E.164 standard.
They must include a country code preceded by +
.
Timestamp
An ISO 8601 date and time.
- The time zone is UTC.
- The precision is in seconds.
- The format is
YYYY-MM-DDThh:mm:ssZ
. Example:2022-06-20T17:17:11Z
.
Payout state
A payout state is represented as a string that matches exactly one of the following values:
processing
The payout request has been submitted, but is still in the process of being executed.succeeded
The payout has been successfully executed, and the money has reached the user.failed
The payout execution encountered an error. Please see Errors for detailed information on the different types.reversed
The payout has been reversed.
Payout batch state
A payout batch state is represented as a string that matches exactly one of the following values:
processing
The payout request has been submitted and is in the process of being executed.complete
All the payouts in the patch have been processed. Individual payouts might have succeeded or failed, this needs to be checked on each payout inside of the batch.
Payout error
Example
{
"error_code": "insufficient-funds",
"error_message": "Insufficient funds in wallet."
}
Payout error is an object. Errors can be returned from the API in two places: either on the top-level when a validation or pre-check fails, or as a payout_error
field on an individual payout if something went wrong during the execution.
Key | Type | Explanation |
---|---|---|
error_code |
String | You can match on this in your system, to decide how to handle the error. You can find a list of all possible error codes under Errors. |
error_message |
String (optional) |
Errors
Payout API errors
Example errors returned by the API
Request error
{
"error": "request-validation-error",
"message": "Invalid phone number."
}
Validation error
{
"code": "request-validation-error",
"message": "Request invalid",
"details": [
{
"loc": ["currency"],
"msg": "Unknown currency identifier: ABC. We require the currency to be a three-letter ISO 4217 code.",
"type": "value_error"
}
]
}
Error when trying to execute a payment
{
"error_code": "insufficient-funds",
"error_message": "Insufficient funds in wallet."
}
Error on a Payout object, for example as part of a batch
{
"id": "pt-185sw98jg1016",
"currency": "XOF",
"receive_amount": "16000",
"fee": "160",
"mobile": "+221555144081",
"name": "Mame Diop",
"status": "failed",
"payout_error": {
"error_code": "recipient-limit-exceeded",
"error_message": "The recipient has reached their monthly limit."
},
"timestamp": "2022-06-21T10:25:46Z"
}
The following is the list of errors that can occur when using this API.
error_code |
explanation |
---|---|
country-mismatch |
The recipient must be in the same country as your business. |
currency-mismatch |
The currency you specified doesn't match that of your and the recipient's wallet. |
idempotency-mismatch |
You submitted a request with an idempotency key that you have used in a previous request, but the request contents don't match. |
insufficient-funds |
Your business wallet doesn't have the necessary balance to cover the total amount including fees. |
internal-server-error |
A technical error has occurred in Wave's system. We work very hard to avoid this, so it is only reserved for unexpected cases. Please get in touch with your Wave partner representative if you encounter this. You can still retry the request automatically with the same idempotency key. We suggest you wait a couple of seconds between retries. |
invalid-aggregated-merchant-id |
An aggregated merchant ID provided with this request does not exist or cannot be used from this account. |
missing-auth-header |
The request is missing a Bearer token in the Authorization header. See Authentication for instructions. |
not-found |
You requested a payout or a payout batch by ID that we can't match to anything in our system. You should double-check that the ID corresponds to one that you've received in a response, and that the request came from the same business wallet. |
recipient-minor |
The recipient is a minor and cannot receive a payout from this source. |
recipient-account-blocked |
The Wave account you are sending money to is blocked. This is usually due to lost phones or fraud reasons. |
recipient-account-inactive |
The Wave account for the person you are sending money to is inactive. They can re-activate their account by calling Wave support. |
recipient-limit-exceeded |
The person you are sending money to has reached their monthly limits. They can often raise their limits by verifying their identity at a Wave agent. |
request-not-json |
Your request either doesn't have a JSON body, or is lacking the Content-Type header "application/json". |
request-parsing-error |
The JSON body of this request is invalid, generally because of misplaced brackets, commas, or quotes. The error_message will help you with details. |
request-validation-error |
Your request doesn't match the object type required. These are things like a missing field or an invalid type in a provided field like an invalid phone number. The error_message will help you with details. |
service-unavailable |
In some rare and short cases Wave services are down for maintenance. You can try the request again later. |
too-many-requests |
You've sent more requests than we can process in a short time span. You can process your payouts in batches to circumvent this. |
aggregated-merchant-required |
An aggregated merchant identity is required to process the payout. You must provided an aggregated merchant ID. |
We suggest that you write code that can handle the errors above. We might occasionally add a new error type.
The list above only covers known errors that are within Wave's control. There could be other technical issues like connection errors or timeouts if there are any network issues between your servers and Wave's. These are too numerous to list, but in any of those cases, requests can be safely retried, as long as the same idempotency key is used.
HTTP Status Codes
The general reason for the failure is reflected in the HTTP response status code. 4xx codes signal a problem with the request from the client and 5xx codes indicate a problem on the server end. Specific details of the problem are provided in the message body.
Some of the status codes we return are listed below.
Code | Title | Descriptions |
---|---|---|
400 | Bad Request | The server cannot process the request because it is badly formed. |
401 | Unauthorized | The API key is invalid. |
403 | Forbidden | The API key doesn't have appropriate permissions for the request. |
404 | Not Found | You requested an object or page that could not be found. |
405 | Method Not Allowed | The combination of URL path and REST method doesn't exist on this API. |
408 | Request Timeout | The request took too long to process. Try again later. |
409 | Conflict | The request could not be completed due to a conflict with the current state of the resource. |
422 | Unprocessable Entity | The request was well-formed, but the server could not process it. |
429 | Too Many Requests | The rate limit was exceeded i.e. the server received too many requests in a given period of time. |
500 | Internal Server Error | The server encountered an error. Try again later, but the error may persist. |
503 | Service Unavailable | The server is unable to handle the request due to a temporary overload or scheduled maintenance. Try again later. |
504 | Gateway Timeout | A timeout occurred on the network while processing a request. Try again later. |
Status of a payout
Once a payout is entered in our system, it will be created with the status processing
. The payout will be immediately start processing and attempt to send the selected amount to the final recipient, after which the status of the payout will be updated to succeeded
or failed
. The status of a payout can be obtained at all times through the GET /v1/payout endpoint, by providing the id returned at creation time.
A failed
payout will also include the reason why the transfer failed as a payout_error, and you can use this information to determine if it should be retried.
If a payout could not be entered in our system correctly, then querying the GET /v1/payout
endpoint with the affected id will result on a not-found
error to be returned.
In extraordinary cases, Wave might experiment an outage affecting the APIs, resulting on a 5XX error, such as 500 (Internal Server Error) or 503 (Service Unavailable). If you get this response from the POST /v1/payout
endpoint, a payout might still be created on our system but without returning an associated id. You can safely retry the payout in these cases by ensuring the same idempotency key is used.
Retrying transactions
The following are Wave's recommendations on retry logic. Please carefully read the section on Errors and Idempotency.
System errors
Some errors are unexpected, which means the transaction is in an unknown state. This can for example happen during an outage or when there are internet connectivity issues.
- It is important that you mark your transactions internally as being pending.
- You should then retry those transactions using the same idempotency key. There is no time limit for retrying.
- Use retries that start at 1-second intervals and then use exponential backoff.
- If you have an payout ID or a
client_reference
, then you can also fetch the payout to inspect its state - The following are the error codes where this can occur:
408
Request Timeout500
Internal Server Error.503
Service Unavailable. The server is unable to handle the request due to a temporary overload or scheduled maintenance. Try again later.5xx
: You should retry on all internal server errors.
Rate limiting errors
Rate limiting errors of type "429
Too Many Requests" should always be retried. Of course you should wait a few seconds before retrying. You should still mark them internally as pending instead of failed.
Validation and balance errors
All other errors are final, so your system can mark the transaction as failed.
- Example 1: if the recipient has reached an account limit, it doesn't make sense to immediately retry.
- Example 2: if the request was invalid because a field was missing, there is no sense in retrying.
To summarize: You should generally retry on unexpected errors like connection problems and unavailable services. Always use the same idempotency key when retrying a payout or batch.
Changelog
2023-06-16
- Added
recipient-minor
error code.
2022-11-24
- New
payment_reason
field.
2022-10-17
- Added the
aggregated_merchant_id
field and relatedinvalid-aggregated-merchant-id
error code.
2022-09-23
- Added the
recipient-account-blocked
error code.
2022-09-21
- we are now handling rate limiting issues in a more transparent way, returning HTTP
429
"Too Many Requests" instead of500
"Internal Server Error". - added the
too-many-requests
error code.
2022-09-08
- Added the
recipient-account-inactive
error code.
2022-06-27
- initial release