Webhooks
Wave uses webhooks to notify your application when an event occurs in your account. Webhooks are particularly useful for asynchronous events like when a checkout is completed, a checkout payment fails, or a merchant payment is received.
To create a webhook, you must specify a URL to which Wave will send the event data and also specify the types of events you want to receive on the given url.
Working with Wave webhooks
- Create one or more webhook endpoints on your server to receive events from Wave.
- Register the URLs of your webhook endpoints and choose which events you want to be notified about for each.
- When an event occurs, Wave sends a POST request to the URLs that have been registered for the event. The body of the request is an Event object
- Upon receiving an event notification, your webhook must verify that the event was sent by Wave by following one of our available security strategies.
- Respond to the event with a successful status code (HTTP
2xx
) before performing any business logic. - Perform business logic in response to the event.
Duplicate Webhooks
Webhook registration
You can manage webhooks in the Business Portal, in the developers section . Here you can also test the health of your endpoints, and get feedback on error rates.
To register a webhook, you need to provide the URL of your server's endpoint, the security strategy and check the types of events you want to be notified about on that URL.
After the webhook is registered you will be shown the secret associated with it. You must keep this secret secured and ensure it's not saved in plaintext anywhere nor leaked in logs.
Webhook security
To ensure that an event notification is from Wave webhooks are associated with a secret that is used in the header of each request as a signature or bearer token, depending on the strategy selected.
Shared Secret
This strategy is the simplest way to verify the origin of the request. The webhook secret will be included on the Authorization
header, requiring you to compare it with the one saved on your system. If they match, then you can be assured the request is coming from Wave, and proceed with computing the payment.
Example of webhook with Shared Secret strategy
Webhook secret
wave_sn_WHS_xz4m6g8rjs9bshxy05xj4khcvjv7j3hcp4fbpvv6met0zdrjvezg
Authorization
header of the webhook's POST request to your server
Bearer wave_sn_WHS_xz4m6g8rjs9bshxy05xj4khcvjv7j3hcp4fbpvv6met0zdrjvezg
Signing Secret
The signing-secret strategy utilizes the webhook secret to sign the hashed body of the request, including it on the Wave-Signature
header.
This strategy allows you to verify not only the origin of the request, but also the integrity of the body. We recommend this strategy if you require extra security.
Here's an example of the
Wave-Signature
header. Newlines have been added for readability. The actual header would appear on one line:
Wave-Signature:
t=1639081943,
v1=942119aedf9fa377844cf010785fe14ef8478c72af0b73d62ea3941335b526a8,
v1=f0219658e485a20af77bee4ecfec77a900ee14380f9f4894ede11e33c465c32e
The Wave-Signature
header consists of a timestamp and one or more signatures. The elements of the header are separated by commas. The timestamp is prefixed by t=
and each signature is prefixed by v1
. The timestamp itself is expressed in Unix time.
The header contains a signature for each secret on your webhook. Usually you would only have one active secret, but for a time after you change secrets, you may have more than one that is active. The signature is a hash-based message authentication code (HMAC) generated from the payload using a SHA256 hash function.
To verify the webhook signature, you compute the expected HMAC value based on the body of the event message and the webhook secret and make sure that the result is provided as one of the signatures in the header. To prevent replay attacks you may reject messages where the timestamp is too old (five minutes is a reasonable interval).
The steps to verify the Wave-Signature
header are:
- Split the header using
,
as the separator to get a list of the timestamp and signature elements. - Split each element using
=
as the separator to get a prefix and value. The prefix for the timestamp ist
and the prefix for the signatures isv1
. - Construct the payload by concatenating the timestamp value with the body of the request (that is, the timestamp value then the request body).
- Compute the expected HMAC value using the SHA256 hash function, the webhook secret as the key and the payload as the message.
- Look for a match between the expected HMAC value and one of the signatures provided in the header.
- If a match is found, compute the difference between the current timestamp and the one received in the header to make sure it isn't too old.
Signature verification implementation:
<?php
function webhook_verify_signature_and_decode($wave_webhook_secret, $wave_signature, $webhook_body) {
// Uncomment var_dump if you need to debug the 3 input values:
// var_dump($wave_webhook_secret, $wave_signature, $webhook_body);
$parts = explode(",", $wave_signature);
$timestamp = explode("=", $parts[0])[1];
$signatures = array();
foreach (array_slice($parts, 1) as $signature) {
$signatures[] = explode("=", $signature)[1];
}
$computed_hmac = hash_hmac("sha256", $timestamp . $webhook_body, $wave_webhook_secret);
$valid = in_array($computed_hmac, $signatures);
if($valid) {
# The request is valid, you can proceed using the decoded contents.
return json_decode($webhook_body);
} else {
die("Invalid webhook signature.");
}
}
// This key is given to you by a Wave Representative when we register
// your webhook URL. (This is different from the API key)
$wave_webhook_secret = "wave_sn_WHS_xz4m6g8rjs9bshxy05xj4khcvjv7j3hcp4fbpvv6met0zdrjvezg";
// # This header is sent by Wave in each webhook POST request.
// (The plain HTTP header is 'Wave-Signature')
$wave_signature = $_SERVER['HTTP_WAVE_SIGNATURE'];
// The body of the request, as a plain string, not yet parsed to JSON:
$webhook_body = file_get_contents('php://input');
$webhook_json = webhook_verify_signature_and_decode($wave_webhook_secret, $wave_signature, $webhook_body)
// You can now proceed to use the webhook data to process the request:
$webhook_type = $webhook_json->type;
$webhook_data = $webhook_json->data;
?>
const crypto = require('crypto');
const validateWaveSignature = (waveSignature, rawBody, webhookSecret) => {
const parts = waveSignature.split(',');
const timestampPart = parts.find(comp => comp.startsWith('t='));
const timestamp = timestampPart?.split("=")[1];
const signatureParts = parts.filter(comp => comp.startsWith('v1='));
const signatures = signatureParts.map(s => {return s.split("=")[1]})
const payload = timestamp + rawBody;
const calculatedSignature = crypto.createHmac('sha256', webhookSecret).update(payload).digest("hex");
return signatures.includes(calculatedSignature);
};
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class WaveWebhookSignatureVerifier {
private static final byte[] HEX_ARRAY = "0123456789abcdef".getBytes(StandardCharsets.US_ASCII);
public static boolean verifySignature(String waveSignature, String body, String webhookSecret) throws GeneralSecurityException {
final String[] waveSignatureParts = waveSignature.split(",");
String timestamp = "";
List<String> signatures = new ArrayList<>();
for (String elem : waveSignatureParts) {
String[] keyval = elem.split("=");
String key = keyval[0];
String val = keyval[1];
// Uncomment this if you want to see which values were extracted:
// System.out.printf("key: %s, val: %s %n", key, val);
if (key.equals("t")) {
timestamp = val;
} else {
signatures.add(val);
}
}
String data = timestamp + body;
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(webhookSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] hash = sha256_HMAC.doFinal(data.getBytes(StandardCharsets.UTF_8));
String calculatedSignature = bytesToHex(hash);
// Uncomment this if you want to see what was calculated:
// System.out.printf("Signature: %s %n", calculatedSignature);
return signatures.contains(calculatedSignature);
}
private static String bytesToHex(byte[] bytes) {
byte[] hexChars = new byte[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars, StandardCharsets.UTF_8);
}
}
require 'openssl'
def validate_wave_signature(wave_signature, raw_body, webhook_secret)
parts = wave_signature.split(',')
timestamp = parts.find { |comp| comp.start_with?('t=') }&.split('=')&.last
signatures = parts.select { |comp| comp.start_with?('v1=') }.map { |s| s.split('=').last }
payload = timestamp + raw_body
calculated_signature = OpenSSL::HMAC.hexdigest('sha256', webhook_secret, payload)
signatures.include?(calculated_signature)
end
import hmac
import hashlib
def validate_wave_signature(wave_signature, raw_body, webhook_secret):
parts = wave_signature.split(',')
timestamp = parts[0].split('=')[1]
signatures = [s.split('=')[1] for s in parts[1:]]
payload = timestamp + raw_body
calculated_signature = hmac.new(webhook_secret.encode('utf-8'), payload.encode('utf-8'), hashlib.sha256).hexdigest()
return calculated_signature in signatures
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
)
func validateWaveSignature(waveSignature, rawBody, webhookSecret string) bool {
parts := strings.Split(waveSignature, ",")
timestamp := strings.Split(parts[0], "=")[1]
signatures := []string{}
for _, part := range parts[1:] {
signatures = append(signatures, strings.Split(part, "=")[1])
}
payload := timestamp + rawBody
h := hmac.New(sha256.New, []byte(webhookSecret))
h.Write([]byte(payload))
calculatedSignature := hex.EncodeToString(h.Sum(nil))
for _, signature := range signatures {
if signature == calculatedSignature {
return true
}
}
return false
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
public class WaveSignatureValidator
{
public static bool ValidateSignature(string waveSignature, string body, string webhookSecret)
{
var parts = waveSignature.Split(",");
var timestamp = parts.First(p => p.StartsWith("t=")).Split("=")[1];
var signatures = parts.Where(p => p.StartsWith("v1=")).Select(p => p.Split("=")[1]).ToList();
var payload = timestamp + body;
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(webhookSecret)))
{
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var calculatedSignature = BitConverter.ToString(hash).Replace("-", "").ToLower();
return signatures.Contains(calculatedSignature);
}
}
}
Help, the signature validation fails
The most common reason for this is that some frameworks automatically parse the body of the webhook you receive. This gives the wrong result when you calculate the hash. The reason for this is that the signature is only unique if the key order in the JSON hasn't changed, there are no additional or missing spaces, and there are no additional line breaks.
The solution is to not parse the body, but instead do the signature validation against the raw string you received.
Here are some common examples that indicate this problem.
Example Wave-Signature validation
Webhook secret
'wave_sn_WHS_xz4m6g8rjs9bshxy05xj4khcvjv7j3hcp4fbpvv6met0zdrjvezg'
Wave-Signature
header of the webhook's POST request to your server
't=1667920421,v1=53c971695230e9c51b1030d673eee76e70bbcdf8a7c5b8c1d44e0b8b1329647b'
Webhook body of the webhook's POST request to your server
'{"id": "AE_ijzo7oGgrlM7", "type": "checkout.session.completed", "data": {"id": "cos-1b01sghpg100j", "amount": "100", "checkout_status": "complete", "client_reference": null, "currency": "XOF", "error_url": "https://example.com/error", "last_payment_error": null, "business_name": "Annas Apiaries", "payment_status": "succeeded", "success_url": "https://example.com/success", "wave_launch_url": "https://pay.wave.com/c/cos-1b01sghpg100j?a=100&c=XOF&m=Annas%20Apiaries", "when_completed": "2022-11-08T15:05:45Z", "when_created": "2022-11-08T15:05:32Z", "when_expires": "2022-11-09T15:05:32Z", "transaction_id": "TCN4Y4ZC3FM"}}'
The following is a full example for webhook signature validation. The 3 things you need are
- The webhook secret (given to you by wave when your webhook URL is registered)
- The Wave-Signature header sent in every webhook request. This is a POST request by Wave, to your server.
- The body of the POST request.
Note that 3 examples are quoted with '
, since they are strings, but the outer quotes aren't part of the string.
Follow the instructions under Wave-Signature validation to verify the signature in the example.
Example 1
This is a correct payload:
'{"id": "AE_ijzo7oGgrlM7", "type": "checkout.session.completed", "data": {"id": "cos-1b01sghpg100j", "amount": "100", "checkout_status": "complete", "client_reference": null, "currency": "XOF", "error_url": "https://example.com/error", "last_payment_error": null, "business_name": "Annas Apiaries", "payment_status": "succeeded", "success_url": "https://example.com/success", "wave_launch_url": "https://pay.wave.com/c/cos-1b01sghpg100j?a=100&c=XOF&m=Annas%20Apiaries", "when_completed": "2022-11-08T15:05:45Z", "when_created": "2022-11-08T15:05:32Z", "when_expires": "2022-11-09T15:05:32Z", "transaction_id": "TCN4Y4ZC3FM"}}'
Example 2
The missing spaces indicate that the body was parsed as JSON and then converted into string again:
'{"id":"AE_ijzo7oGgrlM7","type":"checkout.session.completed","data":{"id":"cos-1b01sghpg100j","amount":"100","checkout_status":"complete","client_reference":null,"currency":"XOF","error_url":"https://example.com/error","last_payment_error":null,"business_name":"AnnasApiaries","payment_status":"succeeded","success_url":"https://example.com/success","wave_launch_url":"https://pay.wave.com/c/cos-1b01sghpg100j?a=100&c=XOF&m=Annas%20Apiaries","when_completed":"2022-11-08T15:05:45Z","when_created":"2022-11-08T15:05:32Z","when_expires":"2022-11-09T15:05:32Z","transaction_id":"TCN4Y4ZC3FM"}}'
Example 3
You only hash the data
payload, but you need the full body:
{"id": "cos-1b01sghpg100j", "amount": "100", "checkout_status": "complete", "client_reference": null, "currency": "XOF", "error_url": "https://example.com/error", "last_payment_error": null, "business_name": "Annas Apiaries", "payment_status": "succeeded", "success_url": "https://example.com/success", "wave_launch_url": "https://pay.wave.com/c/cos-1b01sghpg100j?a=100&c=XOF&m=Annas%20Apiaries", "when_completed": "2022-11-08T15:05:45Z", "when_created": "2022-11-08T15:05:32Z", "when_expires": "2022-11-09T15:05:32Z", "transaction_id": "TCN4Y4ZC3FM"}
Example 4
We don't send line breaks in the body, so the JSON below is not the exact body string content:
{
"id": "AE_ijzo7oGgrlM7",
"type": "checkout.session.completed",
"data": {
"id": "cos-1b01sghpg100j",
"amount": "100",
"checkout_status": "complete",
"client_reference": null,
"currency": "XOF",
"error_url": "https://example.com/error",
"last_payment_error": null,
"business_name": "Annas Apiaries",
"payment_status": "succeeded",
"success_url": "https://example.com/success",
"wave_launch_url": "https://pay.wave.com/c/cos-1b01sghpg100j?a=100&c=XOF&m=Annas%20Apiaries",
"when_completed": "2022-11-08T15:05:45Z",
"when_created": "2022-11-08T15:05:32Z",
"when_expires": "2022-11-09T15:05:32Z",
"transaction_id": "TCN4Y4ZC3FM"
}
}
IP Whitelisting
For enhanced security, we require whitelisting of all our public IP addresses.
- 104.155.43.220/32
- 34.140.23.175/32
- 34.22.138.147/32
- 34.76.157.22/32
- 34.78.253.137/32
- 34.79.119.200/32
- 35.189.207.30/32
- 35.195.255.192/32
- 35.205.122.113/32
- 35.205.190.121/32
- 35.233.61.130/32
- 35.240.61.196/32
- 35.240.75.65/32
- 35.241.190.127/32
- 35.241.219.1/32
Secret Rotation
It is a security best practice to occasionally rotate webhook secrets. In situations where your webhook secret has been exposed, you should always immediately rotate the secret. Wave may also prompt you to rotate the secrets if the webhook is too old, as shown in the following screenshot.
Rotating a secret requires recreating a webhook to obtain a new secret and then removing the old webhook. The simplest way to do this is using the duplicate option from the webhook list.
After confirming the duplication, you will be shown the secret of this new webhook. In the list of webhooks you should now see two webhooks with the same URL, secret strategy and event subscription. Their only difference will be the creation date.
While these two webhooks are configured at the same time, your system will receive two notifications per event, one secured with the old secret and one with the new one.
Until you exchange these secrets in your system, the notifications related to the new webhook will be rejected by the validation logic. After changing the old secret for the new one, then the notifications related to the old secret will start being rejected by the validation, while the new ones will be now accepted. After changing the secret and ensuring you are accepting requests with the new webhook secret, you can remove the old secret by using the delete option from the webhook list.
Step by step secret rotation
- Log into the business portal
- Go to Developers -> Webhooks
- On the list of webhooks, identify the one you want to rotate can select the option Duplicate from the three dots menu on the right
- Confirm the webhook duplication
- From this point on, your system will receive two webhook notifications per event
- Copy the new webhook secret
- On your system, exchange the old webhook secret used for validation with the new one
- Send a test event from the new webhook and confirm on your server that you can receive and validate its header correctly
- Delete the old webhook by selecting the option Delete from the three dots menu on the right
- Rotation is completed, your system will receive again only one webhook notification per event
Webhook requests
When you've registered a webhook to receive notifications of some events and one of those events occurs, Wave sends a request to the webhook endpoint. The body of the request is an Event object. It contains an event type which describes the event and an object which describes the changed state.
As an example, when a checkout session is completed successfully, the event type would be checkout.session.completed
and the data object would be a Checkout Session object containing the details of the checkout with a status of complete
.
Event Types
In the list below of supported events, the event type is listed in bold followed by the name of the associated data object in italics.
checkout.session.completed Checkout Session
Occurs when a checkout session is completed.
checkout.session.payment_failed Checkout Session
Occurs when a payment for a checkout session fails. Note that you may receive several failures before a success occurs.
b2b.payment_received B2B Payment
Occurs when a B2B payment is received.
b2b.payment_failed B2B Payment
Occurs when a B2B payment failure occurred. Note that you may receive several failures before a success occurs.
merchant.payment_received
Occurs when a merchant payment is received.
Events
Wave webhooks use events to notify you about changes related to your account. When a notification is sent to your webhook, the body of the request consists of an Event object.
The data property of the Event object depends on the type of event that occurred. For example, if the event type is merchant.payment.received
the data property will contain a Merchant Payment object.
The Event object
The Event object provides information about a change that occurred on your account and information about the state of the change.
Attributes
Attribute | Type | Description |
---|---|---|
id | string | Unique identifier for the Event object. |
data | hash | Object containing data related to the event. |
type | string | Name of the event. |
Event object examples
The following are examples of Event objects for different event types.
FOR CHECKOUT SESSION COMPLETED
{
"id": "EV_QvEZuDSQbLdI",
"type": "checkout.session.completed",
"data": {
"id": "cos-18qq25rgr100a",
"amount": "1000",
"checkout_status": "complete",
"client_reference": "1f31dfd7-aec8-4adf-84ff-4a9c1981be2a",
"currency": "XOF",
"error_url": "https://example.com/error",
"payment_status": "succeeded",
"success_url": "https://example.com/success",
"wave_launch_url": "https://pay.wave.com/c/cos-18qq25rgr100a",
"when_completed": "2021-12-08T10:15:32Z",
"when_created": "2021-12-08T10:13:04Z",
"when_expires": "2021-12-09T10:13:04Z"
}
}
FOR MERCHANT PAYMENT RECEIVED WITH CUSTOM FIELDS
{
"id": "AE_ijzo7oGgrlM8",
"type": "merchant.payment_received",
"data": {
"id": "T_46HS5COOWE",
"amount": "1000",
"currency": "XOF",
"sender_mobile": "+221761110001",
"custom_fields": {
"account_number": "abc-123"
},
"when_created": "2021-12-08T10:13:04Z"
}
}
FOR B2B PAYMENT RECEIVED
{
"id": "AE_ijzo7oGgrlM8",
"type": "b2b.payment_received",
"data": {
"amount": "39800",
"client_reference": "1f31dfd7-aec8-4adf-84ff-4a9c1981be2a",
"currency": "XOF",
"id": "b2b-1ndjb8dj81008",
"sender_id": "M_qn0zhfcKV1Tl",
"when_created": "2022-08-10T14:28:15.585392"
}
}
FOR B2B PAYMENT FAILED
{
"id": "AE_8bO0d7TwW6Eq",
"type": "b2b.payment_failed",
"data": {
"amount": "39800",
"client_reference": "1f31dfd7-aec8-4adf-84ff-4a9c1981be2a",
"currency": "XOF",
"id": "b2b-1ndj717m0100e",
"last_payment_error": {
"code": "insufficient-funds",
"message": "Insufficient balance. Please visit a Wave agent to deposit."
},
"sender_id": "M_yk7rFUnaA9n8",
"when_created": "2022-08-10T14:28:15.987217"
}
}
FOR CHECKOUT SESSION PAYMENT FAILED
{
"id": "EV_QvEZuDSQbLdI",
"type": "checkout.session.payment_failed",
"data": {
"id": "cos-18qq25rgr100a",
"amount": "1000",
"checkout_status": "failed",
"client_reference": "1f31dfd7-aec8-4adf-84ff-4a9c1981be2a",
"currency": "XOF",
"error_url": "https://example.com/error",
"payment_status": "failed",
"success_url": "https://example.com/success",
"wave_launch_url": "https://pay.wave.com/c/cos-18qq25rgr100a",
"when_created": "2021-12-08T10:13:04Z",
"when_expires": "2021-12-09T10:13:04Z"
}
}
FOR MERCHANT PAYMENT RECEIVED
{
"id": "AE_ijzo7oGgrlM8",
"type": "merchant.payment_received",
"data": {
"id": "T_46HS5COOWE",
"amount": "1000",
"currency": "XOF",
"sender_mobile": "+221761110001",
"when_created": "2021-12-08T10:13:04Z"
}
}
Testing your webhook
You can test your webhook by sending a test event to your webhook endpoint. This is useful for verifying that your webhook endpoint is working correctly and that it can handle the events that you want to receive.
Using the Business Portal
You can also use the Webhook Tester in the Business Portal. Here you can click on your registered webhook endpoint and send a test event to see the response.
Manual Testing
You can use the example requests to make sure that your server can reliably receive and process webhooks.
Replace <your webhook URL>
with your server's address, and test if you receive the request and the parse the data in the body.
Make sure to test the following:
- Your test server can receive the
{"test_key": "test_value"}
object in the basic example. - Your server returns the HTTP 200 Success code.
- Your server can receive and parse the
Wave-Signature
header of the advanced example. - Your server can parse the body of the webhook, especially the
data
field.
Once your test server can process these successfully, you are ready to switch on signature validation. This will make sure only webhooks originating from Wave can trigger events in your system.
Basic Example
Basic webhook test example:
curl -i -X POST \
--url <your webhook URL> \
-d '{"test_key": "test_value"}'
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, '<your webhook URL>');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
$data = array(
'test_key' => 'test_value'
);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$headers = array();
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$result = curl_exec($ch);
if (curl_errno($ch)) {
echo 'Error:' . curl_error($ch);
}
curl_close($ch);
?>
var axios = require('axios');
var data = JSON.stringify({"test_key":"test_value"});
var config = {
method: 'post',
url: '<your webhook URL>',
headers: {
'Content-Type': 'application/json'
},
data : data
};
axios(config)
.then(function (response) {
console.log(JSON.stringify(response.data));
})
.catch(function (error) {
console.log(error);
});
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) {
try {
URL url = new URL("<your webhook URL>");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
OutputStream os = conn.getOutputStream();
byte[] input = "{\"test_key\": \"test_value\"}".getBytes("utf-8");
os.write(input, 0, input.length);
BufferedReader br = new BufferedReader(new InputStreamReader((conn.getInputStream())));
String output;
while ((output = br.readLine()) != null) {
System.out.println(output);
}
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
require "uri"
require "net/http"
url = URI("<your webhook URL>")
https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = "application/json"
request.body = "{\"test_key\":\"test_value\"}"
response = https.request(request)
puts response.read_body
import requests
url = '<your webhook URL>'
payload = {"test_key": "test_value"}
headers = {
'Content-Type': 'application/json'
}
response = requests.request("POST", url, headers=headers, json = payload)
print(response.text.encode('utf8'))
package main
import (
"bytes"
"fmt"
"net/http"
)
func main() {
url := "<your webhook URL>"
method := "POST"
payload := &bytes.Buffer{}
payload.WriteString(`{"test_key": "test_value"}`)
client := &http.Client{}
req, err := http.NewRequest(method, url, payload)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
fmt.Println(res)
}
using System;
using System.IO;
using System.Net;
using System.Text;
class Program
{
static void Main()
{
var request = (HttpWebRequest)WebRequest.Create("<your webhook URL>");
request.Method = "POST";
request.ContentType = "application/json";
var data = Encoding.ASCII.GetBytes("{\"test_key\":\"test_value\"}");
using (var stream = request.GetRequestStream())
{
stream.Write(data, 0, data.Length);
}
var response = (HttpWebResponse)request.GetResponse();
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
Console.WriteLine(responseString);
}
}
Advanced Example
Webhook example with a realistic body:
curl -i -X POST <your webhook URL> \
-H 'Content-Type: application/json' \
-H "Wave-Signature: t=123,v1=abc" \
-d '{
"id": "EV_QvEZuDSQbLdI",
"type": "checkout.session.completed",
"data": {
"id": "cos-18qq25rgr100a",
"amount": "100",
"checkout_status": "complete",
"client_reference": "cbd2c580",
"currency": "XOF",
"payment_status": "succeeded",
"success_url": "https://example.com/success",
"error_url": "https://example.com/error",
"wave_launch_url": "https://pay.wave.com/c/cos-18qq25rgr100a",
"when_completed": "2022-12-08T10:15:32Z",
"when_created": "2022-12-08T10:13:04Z",
"when_expires": "2022-12-09T10:13:04Z"
}
}'
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, '<your webhook URL>');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
$data = array(
'id' => 'EV_QvEZuDSQbLdI',
'type' => 'checkout.session.completed',
'data' => array(
'id' => 'cos-18qq25rgr100a',
'amount' => '100',
'checkout_status' => 'complete',
'client_reference' => 'cbd2c580',
'currency' => 'XOF',
'payment_status' => 'succeeded',
'success_url' => 'https://example.com/success',
'error_url' => 'https://example.com/error',
'wave_launch_url' => 'https://pay.wave.com/c/cos-18qq25rgr100a',
'when_completed' => '2022-12-08T10:15:32Z',
'when_created' => '2022-12-08T10:13:04Z',
'when_expires' => '2022-12-09T10:13:04Z'
)
);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$headers = array();
$headers[] = 'Content-Type: application/json';
$headers[] = 'Wave-Signature: t=123,v1=abc';
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$result = curl_exec($ch);
if (curl_errno($ch)) {
echo 'Error:' . curl_error($ch);
}
curl_close($ch);
?>
var axios = require('axios');
var data = JSON.stringify({"id":"EV_QvEZuDSQbLdI","type":"checkout.session.completed","data":{"id":"cos-18qq25rgr100a","amount":"100","checkout_status":"complete","client_reference":"cbd2c580","currency":"XOF","payment_status":"succeeded","success_url":"https://example.com/success","error_url":"https://example.com/error","wave_launch_url":"https://pay.wave.com/c/cos-18qq25rgr100a","when_completed":"2022-12-08T10:15:32Z","when_created":"2022-12-08T10:13:04Z","when_expires":"2022-12-09T10:13:04Z"}});
var config = {
method: 'post',
url: '<your webhook URL>',
headers: {
'Content-Type': 'application/json',
'Wave-Signature': 't=123,v1=abc'
},
data: data
};
axios(config)
.then(function (response) {
console.log(JSON.stringify(response.data));
})
.catch(function (error) {
console.log(error);
});
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpRequest.BodyPublisher;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.Map;
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("<your webhook URL>"))
.header("Content-Type", "application/json")
.header("Wave-Signature", "t=123,v1=abc")
.POST(BodyPublishers.ofString("{\"id\":\"EV_QvEZuDSQbLdI\",\"type\":\"checkout.session.completed\",\"data\":{\"id\":\"cos-18qq25rgr100a\",\"amount\":\"100\",\"checkout_status\":\"complete\",\"client_reference\":\"cbd2c580\",\"currency\":\"XOF\",\"payment_status\":\"succeeded\",\"success_url\":\"https://example.com/success\",\"error_url\":\"https://example.com/error\",\"wave_launch_url\":\"https://pay.wave.com/c/cos-18qq25rgr100a\",\"when_completed\":\"2022-12-08T10:15:32Z\",\"when_created\":\"2022-12-08T10:13:04Z\",\"when_expires\":\"2022-12-09T10:13:04Z\"}}"))
.build();
try {
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
System.out.println(response.body());
} catch (Exception e) {
e.printStackTrace();
}
}
}
require 'net/http'
require 'uri'
require 'json'
uri = URI.parse("<your webhook URL>")
request = Net::HTTP::Post.new(uri)
request.content_type = "application/json"
request["Wave-Signature"] = "t=123,v1=abc"
request.body = JSON.dump({
"id" => "EV_QvEZuDSQbLdI",
"type" => "checkout.session.completed",
"data" => {
"id" => "cos-18qq25rgr100a",
"amount" => "100",
"checkout_status" => "complete",
"client_reference" => "cbd2c580",
"currency" => "XOF",
"payment_status" => "succeeded",
"success_url" => "https://example.com/success",
"error_url" => "https://example.com/error",
"wave_launch_url" => "https://pay.wave.com/c/cos-18qq25rgr100a",
"when_completed" => "2022-12-08T10:15:32Z",
"when_created" => "2022-12-08T10:13:04Z",
"when_expires" => "2022-12-09T10:13:04Z"
}
})
req_options = {
use_ssl: uri.scheme == "https",
}
response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
http.request(request)
end
puts response.body
import requests
url = '<your webhook URL>'
payload = {
"id": "EV_QvEZuDSQbLdI",
"type": "checkout.session.completed",
"data": {
"id": "cos-18qq25rgr100a",
"amount": "100",
"checkout_status": "complete",
"client_reference": "cbd2c580",
"currency": "XOF",
"payment_status": "succeeded",
"success_url": "https://example.com/success",
"error_url": "https://example.com/error",
"wave_launch_url": "https://pay.wave.com/c/cos-18qq25rgr100a",
"when_completed": "2022-12-08T10:15:32Z",
"when_created": "2022-12-08T10:13:04Z",
"when_expires": "2022-12-09T10:13:04Z"
}
}
headers = {
'Content-Type': 'application/json',
'Wave-Signature': 't=123,v1=abc'
}
response = requests.request("POST", url, headers=headers, json=payload)
print(response.text)
package main
import (
"bytes"
"fmt"
"net/http"
)
func main() {
url := "<your webhook URL>"
method := "POST"
payload := &bytes.Buffer{}
payload.WriteString(`{"id": "EV_QvEZuDSQbLdI", "type": "checkout.session.completed", "data": {"id": "cos-18qq25rgr100a", "amount": "100", "checkout_status": "complete", "client_reference": "cbd2c580", "currency": "XOF", "payment_status": "succeeded", "success_url": "https://example.com/success", "error_url": "https://example.com/error", "wave_launch_url": "https://pay.wave.com/c/cos-18qq25rgr100a", "when_completed": "2022-12-08T10:15:32Z", "when_created": "2022-12-08T10:13:04Z", "when_expires": "2022-12-09T10:13:04Z"}}`)
client := &http.Client{}
req, err := http.NewRequest(method, url, payload)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Wave-Signature", "t=123,v1=abc")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
fmt.Println("response Status:", res.Status)
}
using System;
using System.IO;
using System.Net;
using System.Text;
class Program
{
static void Main()
{
var request = (HttpWebRequest)WebRequest.Create("<your webhook URL>");
request.Method = "POST";
request.ContentType = "application/json";
request.Headers.Add("Wave-Signature", "t=123,v1=abc");
var data = Encoding.ASCII.GetBytes("{\"id\":\"EV_QvEZuDSQbLdI\",\"type\":\"checkout.session.completed\",\"data\":{\"id\":\"cos-18qq25rgr100a\",\"amount\":\"100\",\"checkout_status\":\"complete\",\"client_reference\":\"cbd2c580\",\"currency\":\"XOF\",\"payment_status\":\"succeeded\",\"success_url\":\"https://example.com/success\",\"error_url\":\"https://example.com/error\",\"wave_launch_url\":\"https://pay.wave.com/c/cos-18qq25rgr100a\",\"when_completed\":\"2022-12-08T10:15:32Z\",\"when_created\":\"2022-12-08T10:13:04Z\",\"when_expires\":\"2022-12-09T10:13:04Z\"}}");
request.ContentLength = data.Length;
using (var stream = request.GetRequestStream())
{
stream.Write(data, 0, data.Length);
}
var response = (HttpWebResponse)request.GetResponse();
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
Console.WriteLine(responseString);
}
}
Full Webhook Example
Full webhook example, including a valid signature. The signature needs to be validated against the webhook secret
wave_sn_WHS_xz4m6g8rjs9bshxy05xj4khcvjv7j3hcp4fbpvv6met0zdrjvezg
:
curl -i -X POST <your webhook URL> \
-H 'Content-Type: application/json' \
-H "Wave-Signature: t=1667920421,v1=53c971695230e9c51b1030d673eee76e70bbcdf8a7c5b8c1d44e0b8b1329647b" \
-d '{
"id": "AE_ijzo7oGgrlM7",
"type": "checkout.session.completed",
"data": {
"id": "cos-1b01sghpg100j",
"amount": "100",
"checkout_status": "complete",
"client_reference": null,
"currency": "XOF",
"error_url": "https://example.com/error",
"last_payment_error": null,
"business_name": "Annas Apiaries",
"payment_status": "succeeded",
"success_url": "https://example.com/success",
"wave_launch_url": "https://pay.wave.com/c/cos-1b01sghpg100j?a=100&c=XOF&m=Annas%20Apiaries",
"when_completed": "2022-11-08T15:05:45Z",
"when_created": "2022-11-08T15:05:32Z",
"when_expires": "2022-11-09T15:05:32Z",
"transaction_id": "TCN4Y4ZC3FM"
}
}'
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, '<your webhook URL>');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
$data = array(
'id' => 'AE_ijzo7oGgrlM7',
'type' => 'checkout.session.completed',
'data' => array(
'id' => 'cos-1b01sghpg100j',
'amount' => '100',
'checkout_status' => 'complete',
'client_reference' => null,
'currency' => 'XOF',
'error_url' => 'https://example.com/error',
'last_payment_error' => null,
'business_name' => 'Annas Apiaries',
'payment_status' => 'succeeded',
'success_url' => 'https://example.com/success',
'wave_launch_url' => 'https://pay.wave.com/c/cos-1b01sghpg100j?a=100&c=XOF&m=Annas%20Apiaries',
'when_completed' => '2022-11-08T15:05:45Z',
'when_created' => '2022-11-08T15:05:32Z',
'when_expires' => '2022-11-09T15:05:32Z',
'transaction_id' => 'TCN4Y4ZC3FM'
)
);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$headers = array();
$headers[] = 'Content-Type: application/json';
$headers[] = 'Wave-Signature: t=1667920421,v1=53c971695230e9c51b1030d673eee76e70bbcdf8a7c5b8c1d44e0b8b1329647b';
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$result = curl_exec($ch);
if (curl_errno($ch)) {
echo 'Error:' . curl_error($ch);
}
curl_close($ch);
?>
var axios = require('axios');
var data = JSON.stringify({"id":"AE_ijzo7oGgrlM7","type":"checkout.session.completed","data":{"id":"cos-1b01sghpg100j","amount":"100","checkout_status":"complete","client_reference":null,"currency":"XOF","error_url":"https://example.com/error","last_payment_error":null,"business_name":"Annas Apiaries","payment_status":"succeeded","success_url":"https://example.com/success","wave_launch_url":"https://pay.wave.com/c/cos-1b01sghpg100j?a=100&c=XOF&m=Annas%20Apiaries","when_completed":"2022-11-08T15:05:45Z","when_created":"2022-11-08T15:05:32Z","when_expires":"2022-11-09T15:05:32Z","transaction_id":"TCN4Y4ZC3FM"}});
var config = {
method: 'post',
url: '<your webhook URL>',
headers: {
'Content-Type': 'application/json',
'Wave-Signature': 't=1667920421,v1=53c971695230e9c51b1030d673eee76e70bbcdf8a7c5b8c1d44e0b8b1329647b'
},
data : data
};
axios(config)
.then(function (response) {
console.log(JSON.stringify(response.data));
})
.catch(function (error) {
console.log(error);
});
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) {
try {
URL url = new URL("<your webhook URL>");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Wave-Signature", "t=1667920421,v1=53c971695230e9c51b1030d673eee76e70bbcdf8a7c5b8c1d44e0b8b1329647b");
conn.setDoOutput(true);
OutputStream os = conn.getOutputStream();
byte[] input = "{\"id\": \"AE_ijzo7oGgrlM7\", \"type\": \"checkout.session.completed\", \"data\": {\"id\": \"cos-1b01sghpg100j\", \"amount\": \"100\", \"checkout_status\": \"complete\", \"client_reference\": null, \"currency\": \"XOF\", \"error_url\": \"https://example.com/error\", \"last_payment_error\": null, \"business_name\": \"Annas Apiaries\", \"payment_status\": \"succeeded\", \"success_url\": \"https://example.com/success\", \"wave_launch_url\": \"https://pay.wave.com/c/cos-1b01sghpg100j?a=100&c=XOF&m=Annas%20Apiaries\", \"when_completed\": \"2022-11-08T15:05:45Z\", \"when_created\": \"2022-11-08T15:05:32Z\", \"when_expires\": \"2022-11-09T15:05:32Z\", \"transaction_id\": \"TCN4Y4ZC3FM\"}}".getBytes("utf-8");
os.write(input, 0, input.length);
BufferedReader br = new BufferedReader(new InputStreamReader((conn.getInputStream())));
String output;
while ((output = br.readLine()) != null) {
System.out.println(output);
}
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
require "uri"
require "net/http"
url = URI("<your webhook URL>")
https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = "application/json"
request["Wave-Signature"] = "t=1667920421,v1=53c971695230e9c51b1030d673eee76e70bbcdf8a7c5b8c1d44e0b8b1329647b"
request.body = "{\"id\":\"AE_ijzo7oGgrlM7\",\"type\":\"checkout.session.completed\",\"data\":{\"id\":\"cos-1b01sghpg100j\",\"amount\":\"100\",\"checkout_status\":\"complete\",\"client_reference\":null,\"currency\":\"XOF\",\"error_url\":\"https://example.com/error\",\"last_payment_error\":null,\"business_name\":\"Annas Apiaries\",\"payment_status\":\"succeeded\",\"success_url\":\"https://example.com/success\",\"wave_launch_url\":\"https://pay.wave.com/c/cos-1b01sghpg100j?a=100&c=XOF&m=Annas%20Apiaries\",\"when_completed\":\"2022-11-08T15:05:45Z\",\"when_created\":\"2022-11-08T15:05:32Z\",\"when_expires\":\"2022-11-09T15:05:32Z\",\"transaction_id\":\"TCN4Y4ZC3FM\"}"
response = https.request(request)
puts response.read_body
import requests
url = '<your webhook URL>'
payload = "{\"id\": \"AE_ijzo7oGgrlM7\", \"type\": \"checkout.session.completed\", \"data\": {\"id\": \"cos-1b01sghpg100j\", \"amount\": \"100\", \"checkout_status\": \"complete\", \"client_reference\": null, \"currency\": \"XOF\", \"error_url\": \"https://example.com/error\", \"last_payment_error\": null, \"business_name\": \"Annas Apiaries\", \"payment_status\": \"succeeded\", \"success_url\": \"https://example.com/success\", \"wave_launch_url\": \"https://pay.wave.com/c/cos-1b01sghpg100j?a=100&c=XOF&m=Annas%20Apiaries\", \"when_completed\": \"2022-11-08T15:05:45Z\", \"when_created\": \"2022-11-08T15:05:32Z\", \"when_expires\": \"2022-11-09T15:05:32Z\", \"transaction_id\": \"TCN4Y4ZC3FM\"}"
headers = {
'Content-Type': 'application/json',
'Wave-Signature': 't=1667920421,v1=53c971695230e9c51b1030d673eee76e70bbcdf8a7c5b8c1d44e0b8b1329647b'
}
response = requests.request("POST", url, headers=headers, data = payload)
print(response.text.encode('utf8'))
package main
import (
"bytes"
"fmt"
"net/http"
)
func main() {
url := "<your webhook URL>"
method := "POST"
payload := &bytes.Buffer{}
payload.WriteString(`{"id": "AE_ijzo7oGgrlM7", "type": "checkout.session.completed", "data": {"id": "cos-1b01sghpg100j", "amount": "100", "checkout_status": "complete", "client_reference": null, "currency": "XOF", "error_url": "https://example.com/error", "last_payment_error": null, "business_name": "Annas Apiaries", "payment_status": "succeeded", "success_url": "https://example.com/success", "wave_launch_url": "https://pay.wave.com/c/cos-1b01sghpg100j?a=100&c=XOF&m=Annas%20Apiaries", "when_completed": "2022-11-08T15:05:45Z", "when_created": "2022-11-08T15:05:32Z", "when_expires": "2022-11-09T15:05:32Z", "transaction_id": "TCN4Y4ZC3FM"}`)
client := &http.Client{}
req, err := http.NewRequest(method, url, payload)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Wave-Signature", "t=1667920421,v1=53c971695230e9c51b1030d673eee76e70bbcdf8a7c5b8c1d44e0b8b1329647b")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
fmt.Println(res)
}
using System;
using System.IO;
using System.Net;
using System.Text;
class Program
{
static void Main()
{
var request = (HttpWebRequest)WebRequest.Create("<your webhook URL>");
request.Method = "POST";
request.ContentType = "application/json";
request.Headers.Add("Wave-Signature", "t=1667920421,v1=53c971695230e9c51b1030d673eee76e70bbcdf8a7c5b8c1d44e0b8b1329647b");
var data = Encoding.ASCII.GetBytes("{\"id\": \"AE_ijzo7oGgrlM7\", \"type\": \"checkout.session.completed\", \"data\": {\"id\": \"cos-1b01sghpg100j\", \"amount\": \"100\", \"checkout_status\": \"complete\", \"client_reference\": null, \"currency\": \"XOF\", \"error_url\": \"https://example.com/error\", \"last_payment_error\": null, \"business_name\": \"Annas Apiaries\", \"payment_status\": \"succeeded\", \"success_url\": \"https://example.com/success\", \"wave_launch_url\": \"https://pay.wave.com/c/cos-1b01sghpg100j?a=100&c=XOF&m=Annas%20Apiaries\", \"when_completed\": \"2022-11-08T15:05:45Z\", \"when_created\": \"2022-11-08T15:05:32Z\", \"when_expires\": \"2022-11-09T15:05:32Z\", \"transaction_id\": \"TCN4Y4ZC3FM\"}");
using (var stream = request.GetRequestStream())
{
stream.Write(data, 0, data.Length);
}
var response = (HttpWebResponse)request.GetResponse();
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
Console.WriteLine(responseString);
}
}
Retries
If your server does not respond with a successful status code (HTTP 2xx
) to a webhook notification, Wave will retry sending the notification for up to 3 days. This is to ensure that you do not miss any important events.
When you receive a webhook notification, you should respond with a successful status code (HTTP 2xx
) before performing any business logic. This is to ensure that Wave does not retry sending the notification.
Wave will make every attempt to successfully deliver a webhook event only a single time. However, due to the nature of distributed systems, we cannot guarantee that a webhook event will be delivered at all, that it will only be delivered once, and or that it will be delivered in any particular order in relation to other any other events. Your system must be robust to missing, duplicate, and out-of-order webhook events.