Creating a Webhook Listener

BVNK sends webhooks for status changes on payment transactions, so it's a good idea to set up a listener in your service to get the most out of the Payments API.

You can specify your Webhook URL inside the settings of each Merchant you create on the BVNK platform.

10561056

Change the URL here at anytime.

Webhooks will be sent as an HTTP POST with Content-Type: application/json containing the payload of the object.

๐Ÿšง

We recommend that you always check the status of the payment on the API after you receive a webhook as further details may be required, such as the final amount paid for the transaction you have been notified about.

๐Ÿ“˜

You should stop updating transactions in your system after receiving the final webhook from BVNK - this prevents duplicate transaction IDs from affecting your customers.

Acknowledging events

The Payments API product is expecting a 200 status code upon receipt of a webhook that has been successfully sent.

๐Ÿšง

If your webhook script performs any logic upon receiving a webhook, you should return a 200 status code before completing the logic to prevent timeouts.

Handling duplicate events

Callback endpoints might occasionally receive the same event more than once. We advise you to guard against duplicated event receipts. One way of doing this is logging the events youโ€™ve processed, and then not processing the logged ones.

Webhook validation

Webhooks you receive from the Payments API product contain a signature to allow servers to verify the authenticity of the request using a server-side secret key.

How the signature is calculated

  1. Concatenate the webhook url, content-type and payload of the webhook
  2. Generate the signature using the Secret Key and hashedBody components. The secret key can be obtained in your BVNK merchant profile under Settings -> Manage Merchants -> Merchant Name
23662366

Below are examples in some popular programming languages

//Referenced libraries
//Download the apache.commons.codec here -> https://commons.apache.org/proper/commons-codec/download_codec.cgi
import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

//1. CREATE CONCATENATED BODY HASH
public String getBodyToHash(String webhookUrl, String contentType, String payload) {
    URI uri = new URI(webhookUrl);
        StringBuilder hashBody = new StringBuilder();
        hashBody.append(uri.getPath());
        if (uri.getRawQuery() != null) {
            hashBody.append(uri.getRawQuery());
        }
        if (contentType != null) {
            hashBody.append(contentType);
        }
        if (payload != null) {
            hashBody.append(payload);
        }

        return hashBody.toString();
    }

//2. GENERATE SIGNATURE USING THE SECRET KEY AND HASHED BODY FROM STEP 1
public String generateSignature(final String secret, String hashBody) {
        return toHex(new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secret).hmac(hashBody));
    }

//3. HELPER toHex METHOD TO RETURN THE HEX VALUE OF THE SIGNATURE IN STEP 2
 static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

//EXAMPLE TEST DATA
    /* Secret key that can be found under Settings -> Manage Merchants -> Merchant Name 
  in your BVNK merchant account*/
    String secretKey = "NjQwNTUwODMtODhjMS00MmQ1LThhZDItMWM1ZGRmZGFhNzAxMTk4MDA2YjktMzcxOC00MGIwLWI5ZmUtYzVhZmVjZmZhZDBh";

    /* Webhook URL that you've set under Settings -> Manage Merchants -> Webhook URL */
    String webhookUrl = "https://webhook.site/7b6aa49e-65cf-4f0a-9146-15c818102c56"; 

    /* Content-Type needed for concatentaing the values */
    String contentType = "application/json"; 

    /* Body of the webhook. Make sure the webhook payload is escaped correctly with no whitespaces*/
    String payload = "{\"source\":\"payment\",\"event\":\"statusChanged\",\"data\":{\"uuid\":\"5e3c0984-c724-426a-889f-ca91ada1e344\",\"merchantDisplayName\":\"TestETHMerchant\",\"merchantId\":\"d2994e42-0d85-4365-b450-deef31fbf4df\",\"dateCreated\":1666047483000,\"expiryDate\":1666048683000,\"quoteExpiryDate\":1666058283000,\"acceptanceExpiryDate\":1666047503000,\"quoteStatus\":\"PAYMENT_OUT_PROCESSED\",\"reference\":\"s949c03ss37s-d4881-4s4bs2s-bbs40d-df064cb5e8ac\",\"type\":\"IN\",\"subType\":\"merchantPayIn\",\"status\":\"COMPLETE\",\"displayCurrency\":{\"currency\":\"ETH\",\"amount\":0.015,\"actual\":0.015},\"walletCurrency\":{\"currency\":\"ETH\",\"amount\":0.015,\"actual\":0.015},\"paidCurrency\":{\"currency\":\"ETH\",\"amount\":0.015,\"actual\":0.015},\"feeCurrency\":{\"currency\":\"ETH\",\"amount\":0.00015,\"actual\":0.00015},\"displayRate\":{\"base\":\"ETH\",\"counter\":\"ETH\",\"rate\":1},\"exchangeRate\":{\"base\":\"ETH\",\"counter\":\"ETH\",\"rate\":1},\"address\":{\"protocol\":\"ETH\",\"address\":\"0x09e34DBc31249e9a9E6EF6A3929a15be501Df05d\",\"tag\":null,\"uri\":\"ethereum:0x09e34DBc31249e9a9E6EF6A3929a15be501Df05d?value={amount}\",\"alternatives\":[]},\"redirectUrl\":\"https://pay.sandbox.bvnk.com/payin?uuid=5e3c0984-c724-426a-889f-ca91ada1e344&flow=direct\",\"returnUrl\":\"https://yourwebsitename.com\",\"transactions\":[{\"dateCreated\":1666047585000,\"dateConfirmed\":1666047609006,\"hash\":\"0x43eda446ee1c64af388b360a9ac7c4a7a895c5d05b26a10fd59b05f66ee47113\",\"amount\":0.015000000000000000,\"risk\":{\"level\":\"LOW\",\"resourceName\":\"UNKNOWN\",\"resourceCategory\":\"UNKNOWN\",\"alerts\":[]},\"networkFeeCurrency\":\"ETH\",\"networkFeeAmount\":0.000266237675025000,\"sources\":[\"0xCc6655AA23a1F4a211710fE873B3ad1be73eD518\"],\"exchangeRate\":{\"base\":\"ETH\",\"counter\":\"ETH\",\"rate\":1},\"displayRate\":{\"base\":\"ETH\",\"counter\":\"ETH\",\"rate\":1}}],\"refund\":null,\"refunds\":[]}}";
var CryptoJS = require("crypto-js");

//Example data
var payload = '{"source":"payment","event":"statusChanged","data":{"uuid":"5e3c0984-c724-426a-889f-ca91ada1e344","merchantDisplayName":"TestETHMerchant","merchantId":"d2994e42-0d85-4365-b450-deef31fbf4df","dateCreated":1666047483000,"expiryDate":1666048683000,"quoteExpiryDate":1666058283000,"acceptanceExpiryDate":1666047503000,"quoteStatus":"PAYMENT_OUT_PROCESSED","reference":"s949c03ss37s-d4881-4s4bs2s-bbs40d-df064cb5e8ac","type":"IN","subType":"merchantPayIn","status":"COMPLETE","displayCurrency":{"currency":"ETH","amount":0.015,"actual":0.015},"walletCurrency":{"currency":"ETH","amount":0.015,"actual":0.015},"paidCurrency":{"currency":"ETH","amount":0.015,"actual":0.015},"feeCurrency":{"currency":"ETH","amount":0.00015,"actual":0.00015},"displayRate":{"base":"ETH","counter":"ETH","rate":1},"exchangeRate":{"base":"ETH","counter":"ETH","rate":1},"address":{"protocol":"ETH","address":"0x09e34DBc31249e9a9E6EF6A3929a15be501Df05d","tag":null,"uri":"ethereum:0x09e34DBc31249e9a9E6EF6A3929a15be501Df05d?value={amount}","alternatives":[]},"redirectUrl":"https://pay.sandbox.bvnk.com/payin?uuid=5e3c0984-c724-426a-889f-ca91ada1e344&flow=direct","returnUrl":"https://yourwebsitename.com","transactions":[{"dateCreated":1666047585000,"dateConfirmed":1666047609006,"hash":"0x43eda446ee1c64af388b360a9ac7c4a7a895c5d05b26a10fd59b05f66ee47113","amount":0.015000000000000000,"risk":{"level":"LOW","resourceName":"UNKNOWN","resourceCategory":"UNKNOWN","alerts":[]},"networkFeeCurrency":"ETH","networkFeeAmount":0.000266237675025000,"sources":["0xCc6655AA23a1F4a211710fE873B3ad1be73eD518"],"exchangeRate":{"base":"ETH","counter":"ETH","rate":1},"displayRate":{"base":"ETH","counter":"ETH","rate":1}}],"refund":null,"refunds":[]}}';
const secretKey = "NjQwNTUwODMtODhjMS00MmQ1LThhZDItMWM1ZGRmZGFhNzAxMTk4MDA2YjktMzcxOC00MGIwLWI5ZmUtYzVhZmVjZmZhZDBh";
var requestUri = new URL('https://webhook.site/7b6aa49e-65cf-4f0a-9146-15c818102c56');
var contentType = "application/json";

//Data required to return x-signature
dataToHash = requestUri.pathname; /* The URI of this webhook eg: /my-path on https://mydomain.com/my-path */
dataToHash += contentType; /* eg: application/json */
dataToHash += payload; /* json body payload from webhook. The payload must be passed without whitespaces*/ 

//Hash the data
var hasher = CryptoJS.HmacSHA256(dataToHash, secretKey);
var hashInHex = CryptoJS.enc.Hex.stringify(hasher);
console.log("x-signature should be: " + hashInHex);
using System;
using System.Security.Cryptography;
using System.Text;

//Generate a string with the body, content-type and url to hash
 String getBodyToHash(String webhookUrl, String contentType, String payload)
    {
        String path = new Uri(webhookUrl).AbsolutePath;

        StringBuilder hashBody = new StringBuilder();
        if (path != null)
        {
            hashBody.Append(path);
        }
        if (contentType != null)
        {
            hashBody.Append(contentType);
        }
        if (payload != null)
        {
            hashBody.Append(payload);
        }

        return hashBody.ToString();
    }


//Generate the signature
    String generateSignature(String key, String data) {
                byte[] secretKey = Encoding.UTF8.GetBytes(key);
        HMACSHA256 hmac = new HMACSHA256(secretKey);
        hmac.Initialize();
        byte[] bytes = Encoding.UTF8.GetBytes(data);
        byte[] rawHmac = hmac.ComputeHash(bytes);

        return toHex(rawHmac);
    }

//Helper toHex method to return the proper value
    String toHex(byte[] ba)
    {
        StringBuilder hex = new StringBuilder(ba.Length * 2);
        foreach (byte b in ba)
            hex.AppendFormat("{0:x2}", b);
        return hex.ToString();
    }
<?php

function validateSignature(){
//DEFINE VARIABLES
$payload = '{"event":"transactionConfirmed","source":"channel","data":{"channelId":"be1bb5e2-eb02-4c99-ba4d-d7680571973f","merchantId":"d2994e42-0d85-4365-b450-deef31fbf4df","merchantDisplayName":"TestETHMerchant","reference":"Channel2-testETH","dateCreated":1669014474000,"lastUpdated":1669014477791,"status":"COMPLETE","uuid":"14ac4bc8-a5c6-42b1-9ee6-5181e0faa232","hash":"0x3775fdfa33f09238318a7db7df6871b70e1bac1dddf32dc0ce9843fbfeba98b4","address":"0xdfE224972f1E8Cd2D27210A48D3eB86f51f9eC7d","tag":null,"paidCurrency":"ETH","displayCurrency":"EUR","walletCurrency":"ETH","feeCurrency":"ETH","paidAmount":0.03,"displayAmount":35.17,"walletAmount":0.03,"feeAmount":0.0003,"exchangeRate":{"base":"ETH","counter":"ETH","rate":1},"displayRate":{"base":"ETH","counter":"EUR","rate":1172.33},"risk":{"level":"LOW","resourceName":"UNKNOWN","resourceCategory":"UNKNOWN","alerts":[]},"sources":["0xCc6655AA23a1F4a211710fE873B3ad1be73eD518"],"networkFee":{"paidCurrency":"ETH","paidAmount":0.000245980360437000,"displayCurrency":"EUR","displayAmount":0.270000000000000000}}}';
$secretKey = 'NjQwNTUwODMtODhjMS00MmQ1LThhZDItMWM1ZGRmZGFhNzAxMTk4MDA2YjktMzcxOC00MGIwLWI5ZmUtYzVhZmVjZmZhZDBh';
$requestUri = 'https://webhook.site/7b6aa49e-65cf-4f0a-9146-15c818102c56';
$contentType = 'application/json';  
  
//GET WEBHOOK PATH
$path = parse_url($requestUri, PHP_URL_PATH);
  
//CONCATENATE DATA THAT NEEDS TO BE HASHED
$dataToHash = $path."".$contentType."".$payload;

//CREATE HMAC SIGNATURE
$xsign = hash_hmac("sha256",$dataToHash,$secretKey,false);

return $xsign;
}

?>

The signature will be available in the header of the webhook, x-signature.

Every new webhook will have a new value of the x-signature header. Make sure you are comparing the hash to the right header.


Whatโ€™s Next