B2B Payout API
ENDPOINTS:
POST /v1/b2b/payout
GET /v1/b2b/payout/:id
Wave's B2B Payout API provides a programmatic way to send money from your business to another business.
See also:
- Payout API - Wave's B2C (business to customer) Payout API
- Balance API - API to query the balance and transaction history of your business wallet.
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. |
Obtaining your B2B recipient ID
Every Wave business wallet is identified by a unique ID. The business you are sending to has to communicate their B2B ID with you. Conversely, if you want to receive B2B payments you have to tell the sender your ID.
You can find your ID in the Wave Business Portal under Settings - Account Info: https://business.wave.com/settings/account-info
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 B2B payout
POST /v1/b2b/payout
Send a single payout to a business recipient. Executes synchronously, meaning that the Wave will attempt to execute the transaction immediately and return the result in the response of this request.
The sender of a B2B payout gets to specify who bears the fees. The sender can choose to pay the fees themselves, or have the recipient pay the fees.
If the sender chooses to pay the fees, the receive_amount
parameter will be required and the recipient will receive the full amount specified in the receive_amount
field.
If the recipient is to pay the fees, then the send_amount
parameter will be required and the amount that will be received will be the send_amount
minus the transaction fee.
You can specify who bears the fees by setting the fee_payment_method
field to either SENDER_PAYS
OR RECIPIENT_PAYS
.
If the fee_payment_method
is not set, the default value is SENDER_PAYS
.
Fee payment methods
The fee payment is used to determine who bears the cost of the transaction fees. The following are the possible values for the fee_payment_method
field:
SENDER_PAYS
The sender pays the fees. The recipient receives the full amount specified in thereceive_amount
field.RECIPIENT_PAYS
The recipient pays the fees. The sender sends the full amount specified in thesend_amount
field.
Example requests
Sender pays fee
curl -X POST \
--url https://api.wave.com/v1/b2b/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": "50000",
"recipient_id": "M_ABCDE12345",
"client_reference": "AF127HXKK.12"
"fee_payment_method": "SENDER_PAYS"
}'
The response will have the fee
value charged on the transaction and the receive_amount
.
{
"id": "b2b-1nvk5g1e0100c",
"currency": "XOF",
"fee_payment_method": "sender_pays",
"status": "succeeded",
"timestamp": "2024-05-01T12:25:58Z",
"receive_amount": "122",
"fee": "1"
}
Recipient pays fee
curl -X POST \
--url https://api.wave.com/v1/b2b/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",
"send_amount": "50000",
"recipient_id": "M_ABCDE12345",
"client_reference": "AF127HXKK.12"
"fee_payment_method": "RECIPIENT_PAYS"
}'
The response will have the fee
value of Zero (0), indicating the sender pays no fee on the transaction and the send_amount
.
{
"id": "b2b-1nvn9rcbr100e",
"currency": "XOF",
"fee_payment_method": "recipient_pays",
"status": "succeeded",
"timestamp": "2024-05-01T14:55:05Z",
"send_amount": "122",
"fee": "0"
}
Parameters
Key | Type | Description |
---|---|---|
client_reference |
String (up to 255 characters) | A unique string that you have to provide. Ideally this is a bill or payment ID internally meaningful to your system, so that you can use it for reconciliation between your payments and their Wave transactions. |
currency |
Currency code | The amount currency. Note that your business wallet and the recipient must be in the same country. |
payment_reason |
String (optional) (up to 255 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. Required if sender pays the fee. |
send_amount |
Amount | The amount to be paid out of the sender's wallet, gross of fees. Required if the recipient pays the fee. |
recipient_id |
String | The B2B ID of the business you're sending money to. |
fee_payment_method |
String | The method of payment for the transaction fees. Possible values are SENDER_PAYS or RECIPIENT_PAYS . If not set, the default value is SENDER_PAYS . |
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/b2b/payout/:id
curl -X GET \
--url https://api.wave.com/v1/b2b/payout/b2b-185sewgm8100t \
-H 'Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6'
Example response
{
"id": "b2b-185sewgm8100t",
"currency": "XOF",
"receive_amount": "150000",
"fee": "150",
"recipient_id": "M_ABCDE12345",
"client_reference": "FAH.4827.1734",
"payment_reason": "Cashews 2024-01-05",
"status": "succeeded",
"timestamp": "2022-06-21T09:56:29Z"
}
Retrieves a single payout.
Return attributes
Key | Type | Description |
---|---|---|
client_reference |
String, 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. |
fee |
Amount | The fee for sending the payout. |
id |
String | A unique identifier for the payout object. Up to 20 characters. |
payment_reason |
String (optional), up to 255 characters | An optional message with a payment reason that is shown to customers in the payment receipt. |
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. |
recipient_id |
String | The B2B ID of the business you're sending money to. |
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. |
Fetch recipient information
GET /v1/b2b/recipient-info
curl -X GET \
--url https://api.wave.com/v1/b2b/recipient-info?account_number=SN12233445 \
-H 'Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6'
Example response
{
"name": "Cashew Inc.",
"m_id": "M_ABCDE12345",
"account_number": "SN 12 23 34 45"
}
Sometimes you might want to fetch information about the recipient before making a transaction.
Wave B2B recipients agree to share simple account name information with B2B senders. Keep in mind that we are only returning the name of the account as registered with Wave, so the name of the legal entity of the receiving company might be different.
More info on the Recipient Info API page.
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
.
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 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 recipient ID."
}
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
{
"id": "b2b-185sw98jg1016",
"currency": "XOF",
"receive_amount": "160000",
"fee": "1600",
"recipient_id": "M_ABCDE12345",
"client_reference": "AF127HXKK.12",
"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 business must be in the same country as your business. |
currency-mismatch |
The currency you specified doesn't match that of your or 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. See the section on retrying transactions for instructions on how to safely retry. |
missing-auth-header |
The request is missing a Bearer token in the Authorization header. See Authentication for instructions. |
not-found |
You requested a payout 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-limit-exceeded |
The business you are sending money to has reached an account limit. |
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 recipient ID. The error_message will help you with details. |
service-unavailable |
Sometimes Wave services are down. See the section on retrying transactions for instructions on how to safely retry. |
too-many-requests |
You've sent more requests than we can process in a short time span. |
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/b2b/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/b2b/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/b2b/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.