Creating a Webhook Listener

Create a webhook listener to receive information on your EPM accounts and any payment lifecycle changes.

When you set up a new Embedded Partner Merchant (EPM) account, BVNK will send you the accountReference and other relevant information about the account through a webhook.

❗️

The webhook URL, to which the EPM webhook is sent, is configured by your Integration Manager.

Moreover, BVNK will continuously send webhooks to notify you of any status changes in payment transactions. It's beneficial to implement a listener within your service to fully leverage our Crypto Payments API. These webhooks are delivered as HTTP POST requests with Content-Type: application/json, containing the transaction's details.

Webhook URLs are specified within the settings of each Merchant ID (MID) you create on the BVNK platform. By this point, you should have already configured your first MID within your Embedded Partner account. For EPM accounts, MIDs must be established using our API endpoints.

🚧

It's recommended to verify the payment status through the API after receiving a webhook, as additional details, such as the final transaction amount, might be necessary.

Acknowledging Webhooks

Our Crypto Payments product expects a 200 HTTP status code in response to a successfully received and validated webhook.

🚧

Should your webhook script conduct any operations upon receiving a webhook, ensure to respond with a 200 status code immediately to avoid timeout errors, before executing any further logic.

Webhook Validation

Webhooks from BVNK include a x-signature header, enabling servers to authenticate the request using a secret key specific to your server.

How the signature is calculated

  1. Concatenate the webhook URL, content-type, and payload directly.
  2. Hash the concatenated string using the Secret Key associated with your MID. For EP accounts, this key is accessible under 'Manage account' -> 'Manage merchants' -> selected merchant section. Alternatively, it can be retrieved from the GET Merchant IDs endpoint for EPMs.

🚧

Be careful!

Avoid parsing the HTTP request into an object and then re-serializing it. Instead, capture the raw payload of the webhook as a string.

Signature Generation Code Examples

Below are code examples for the most common programming languages. These can be used directly, but you'll need to update the secretKey, webhookUrl, and payload.

import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;

import java.net.URI;
import java.net.URISyntaxException;


/**
 * This is a utility class used for validating webhooks from the Crypto Payments product.
 * It provides a way to verify the authenticity of the incoming webhook request.
 */
public class Main {

    /**
     * This is the main method which drives the process of webhook validation.
     * @param args Unused.
     * @throws URISyntaxException If the provided webhook URL is malformed.
     */
    public static void main(String[] args) throws URISyntaxException {
        //EXAMPLE TEST DATA
        /* Secret key that can be found under Settings -> Manage Merchants -> Merchant Name
        in your BVNK merchant account
        */
        String secretKey = "LWExNWYtYjRjYzJiZjRmZGZhNDU5NmFjNWMtYjhmNS00OTFhLWE0YTAtMWUwZjU5YjAwZjJk";

        /* Webhook URL that you've set under Settings -> Manage Merchants -> Webhook URL */
        String webhookUrl = "https://webhook.site/23dcv4a41-d4935-4e16-9ed6-ce812f7bddcd8";

        /* Content-Type needed for concatenating 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\":\"e605bdf8-ef54-456a-909c-07af117239c7\",\"merchantDisplayName\":\"Fortuna ETH\",\"merchantId\":\"f0006b3a-3fda-47c6-ac21-367a482f3499\",\"dateCreated\":1685022891000,\"expiryDate\":1685195689000,\"quoteExpiryDate\":1685023804000,\"acceptanceExpiryDate\":1685022924000,\"quoteStatus\":\"PAYMENT_IN_RECEIVED\",\"reference\":\"REF319526\",\"type\":\"OUT\",\"subType\":\"merchantPayOut\",\"status\":\"PROCESSING\",\"displayCurrency\":{\"currency\":\"ETH\",\"amount\":0.02,\"actual\":0.02},\"walletCurrency\":{\"currency\":\"ETH\",\"amount\":0.02,\"actual\":0.02},\"paidCurrency\":{\"currency\":\"ETH\",\"amount\":0.02,\"actual\":0.020000000000000000},\"feeCurrency\":{\"currency\":\"ETH\",\"amount\":0.0002,\"actual\":0.0002},\"displayRate\":{\"base\":\"ETH\",\"counter\":\"ETH\",\"rate\":1},\"exchangeRate\":{\"base\":\"ETH\",\"counter\":\"ETH\",\"rate\":1},\"address\":{\"protocol\":null,\"address\":\"0x2588a1002Cc4d7EEC1333bE459471571bbe2EE36\",\"tag\":null,\"uri\":null,\"alternatives\":[]},\"redirectUrl\":\"https://pay.sandbox.bvnk.com/payout?uuid=e605bdf8-ef54-456a-909c-07af117239c7\",\"returnUrl\":\"\",\"transactions\":[{\"dateCreated\":1685022909993,\"dateConfirmed\":null,\"hash\":null,\"amount\":0.020000000000000000,\"risk\":{\"level\":\"LOW\",\"resourceName\":\"UNKNOWN\",\"resourceCategory\":\"UNKNOWN\",\"alerts\":[]},\"networkFeeCurrency\":\"ETH\",\"networkFeeAmount\":0.000000000000000000,\"sources\":null,\"exchangeRate\":{\"base\":\"ETH\",\"counter\":\"ETH\",\"rate\":1},\"displayRate\":{\"base\":\"ETH\",\"counter\":\"ETH\",\"rate\":1}}],\"refund\":null,\"refunds\":[]}}";

        // Concatenate the webhook URL, Content-Type, and payload
        String hashBody = getBodyToHash(webhookUrl, contentType, payload);

        // Generate the signature
        String signature = generateSignature(secretKey, hashBody);

        // Output the signature
        System.out.println("Generated signature: " + signature);
    }

    /**
     * This method creates a concatenated string from the provided webhook URL, content type, and payload.
     * @param webhookUrl The URL where the webhook will be sent.
     * @param contentType The MIME type of the content.
     * @param payload The actual content body of the webhook.
     * @return A concatenated string of the provided parameters.
     * @throws URISyntaxException If the provided webhook URL is malformed.
     */
    public static String getBodyToHash(String webhookUrl, String contentType, String payload) throws URISyntaxException {
        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();
    }

    /**
     * This method generates a signature from the provided secret and the hashed body.
     * It uses the HMAC-SHA256 hashing algorithm.
     * @param secret The secret key used to generate the hash.
     * @param hashBody The body that needs to be hashed.
     * @return The HMAC-SHA256 hash of the body, encoded as a hexadecimal string.
     */
    public static String generateSignature(final String secret, String hashBody) {
        return toHex(new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secret).hmac(hashBody));
    }

    /**
     * This is a helper method that converts a byte array to a hexadecimal string.
     * @param bytes The byte array that needs to be converted.
     * @return The hexadecimal representation of the byte array.
     */
    static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}
import hashlib
import hmac
import urllib.parse

def get_body_to_hash(webhook_url, content_type, payload):
    """
    This method creates a concatenated string from the provided webhook URL, content type, and payload.
    :param webhook_url: The URL where the webhook will be sent.
    :param content_type: The MIME type of the content.
    :param payload: The actual content body of the webhook.
    :return: A concatenated string of the provided parameters.
    """
    request_url = urllib.parse.urlparse(webhook_url)
    body_to_hash = f"{request_url.path}{content_type}{payload}"
    return body_to_hash

def generate_signature(secret_key, hash_body):
    """
    This method generates a signature from the provided secret and the hashed body.
    It uses the HMAC-SHA256 hashing algorithm.
    :param secret_key: The secret key used to generate the hash.
    :param hash_body: The body that needs to be hashed.
    :return: The HMAC-SHA256 hash of the body, encoded as a hexadecimal string.
    """
    hashed = hmac.new(secret_key.encode(), hash_body.encode(), hashlib.sha256)
    return hashed.hexdigest()

def validate_webhook():
    """
    This function prints out the signature from inputs.
    """
    # Secret key that can be found under Settings -> Manage Merchants -> Merchant Name in your BVNK merchant account
    secret_key = 'YTczZmM5YTEtZDFhNi00YWU5LTk4ODktMmEzODI1NzBkMDI2ZWFjNDcwNzgtNTRkYi00OTY0LWI5NjAtMzRmOTQ0ZTE5NDQ3'

    # Webhook URL that you've set under Settings -> Manage Merchants -> Webhook URL
    webhook_url = 'https://webhook.site/11488aba-735e-4906-9809-f1a11a91f49f'

    # Content-Type needed for concatenating the values
    content_type = 'application/json'

    # Body of the webhook.
    payload = '{"source":"payment","event":"statusChanged","data":{"uuid":"83e96598-dd76-471c-a990-57a64468c436","merchantDisplayName":"Webhooks testing merchant","merchantId":"7cc7256f-124e-4b0b-aad4-251e131c0977","dateCreated":1685004317000,"expiryDate":1685177117000,"quoteExpiryDate":1685177117000,"acceptanceExpiryDate":1685004365000,"quoteStatus":"ACCEPTED","reference":"REF971604","type":"OUT","subType":"merchantPayOut","status":"COMPLETE","displayCurrency":{"currency":"EUR","amount":22.5,"actual":22.5},"walletCurrency":{"currency":"ETH","amount":0.01353,"actual":0.01353},"paidCurrency":{"currency":"ETH","amount":0.01353,"actual":0.01353},"feeCurrency":{"currency":"ETH","amount":0.00014,"actual":0.00014},"displayRate":{"base":"ETH","counter":"EUR","rate":1663.25022749567},"exchangeRate":{"base":"ETH","counter":"ETH","rate":1},"address":{"address":"0x84A4a239805d06c685219801B82BEA7c76702214","tag":null,"protocol":null,"uri":"ethereum:0x84A4a239805d06c685219801B82BEA7c76702214?value=1.352773E+16","alternatives":[]},"returnUrl":"","redirectUrl":"https://pay.sandbox.bvnk.com/payout?uuid=83e96598-dd76-471c-a990-57a64468c436","transactions":[{"dateCreated":1685004367300,"dateConfirmed":1685004367300,"hash":"0x2ac8db80e1d18a15656a564c3de7f085f1a42005cfa4d6fab9a2439ab7bd6f76","amount":0.01352773,"risk":null,"networkFeeCurrency":"ETH","networkFeeAmount":0.00000000,"sources":[],"displayRate":{"base":"ETH","counter":"EUR","rate":1663.25022749567},"exchangeRate":{"base":"ETH","counter":"ETH","rate":1}}],"refund":null,"refunds":[]}}'

    # Concatenate the webhook URL, Content-Type, and payload
    hash_body = get_body_to_hash(webhook_url, content_type, payload)

    # Generate the signature
    signature = generate_signature(secret_key, hash_body)

    # Output the signature
    print(f"Generated signature: {signature}")

validate_webhook()
<?php

/**
 * This method creates a concatenated string from the provided webhook URL, content type, and payload.
 * @param string $webhookUrl The URL where the webhook will be sent.
 * @param string $contentType The MIME type of the content.
 * @param string $payload The actual content body of the webhook.
 * @return string A concatenated string of the provided parameters.
 */
function getBodyToHash($webhookUrl, $contentType, $payload) {
  $requestURL = parse_url($webhookUrl);
  $bodyToHash = $requestURL['path'] . $contentType . $payload;
  

  return $bodyToHash;
}

/**
 * This method generates a signature from the provided secret and the hashed body.
 * It uses the HMAC-SHA256 hashing algorithm.
 * @param string $secret The secret key used to generate the hash.
 * @param string $hashBody The body that needs to be hashed.
 * @return string The HMAC-SHA256 hash of the body, encoded as a hexadecimal string.
 */
function generateSignature($secretKey, $hashBody) {
  $hash = hash_hmac('sha256', $hashBody, $secretKey);

  return $hash;
}

/**
 * This function prints out the signature from inputs.
 */
function validateWebhook() {
  // EXAMPLE TEST DATA
  $secretKey = 'YTczZmM5YTEtZDFhNi00YWU5LTk4ODktMmEzODI1NzBkMDI2ZWFjNDcwNzgtNTRkYi00OTY0LWI5NjAtMzRmOTQ0ZTE5NDQ3';
  $webhookUrl = 'https://webhook.site/11488aba-735e-4906-9809-f1a11a91f49f';
  $contentType = 'application/json';
  $payload = '{"source":"payment","event":"statusChanged","data":{"uuid":"83e96598-dd76-471c-a990-57a64468c436","merchantDisplayName":"Webhooks testing merchant","merchantId":"7cc7256f-124e-4b0b-aad4-251e131c0977","dateCreated":1685004317000,"expiryDate":1685177117000,"quoteExpiryDate":1685177117000,"acceptanceExpiryDate":1685004365000,"quoteStatus":"ACCEPTED","reference":"REF971604","type":"OUT","subType":"merchantPayOut","status":"COMPLETE","displayCurrency":{"currency":"EUR","amount":22.5,"actual":22.5},"walletCurrency":{"currency":"ETH","amount":0.01353,"actual":0.01353},"paidCurrency":{"currency":"ETH","amount":0.01353,"actual":0.01353},"feeCurrency":{"currency":"ETH","amount":0.00014,"actual":0.00014},"displayRate":{"base":"ETH","counter":"EUR","rate":1663.25022749567},"exchangeRate":{"base":"ETH","counter":"ETH","rate":1},"address":{"address":"0x84A4a239805d06c685219801B82BEA7c76702214","tag":null,"protocol":null,"uri":"ethereum:0x84A4a239805d06c685219801B82BEA7c76702214?value=1.352773E+16","alternatives":[]},"returnUrl":"","redirectUrl":"https://pay.sandbox.bvnk.com/payout?uuid=83e96598-dd76-471c-a990-57a64468c436","transactions":[{"dateCreated":1685004367300,"dateConfirmed":1685004367300,"hash":"0x2ac8db80e1d18a15656a564c3de7f085f1a42005cfa4d6fab9a2439ab7bd6f76","amount":0.01352773,"risk":null,"networkFeeCurrency":"ETH","networkFeeAmount":0.00000000,"sources":[],"displayRate":{"base":"ETH","counter":"EUR","rate":1663.25022749567},"exchangeRate":{"base":"ETH","counter":"ETH","rate":1}}],"refund":null,"refunds":[]}}';

  // Concatenate the webhook URL, Content-Type, and payload
  $hashBody = getBodyToHash($webhookUrl, $contentType, $payload);

  // Generate the signature
  $signature = generateSignature($secretKey, $hashBody);

  // Output the signature
  echo "Generated signature: " . $signature;
}

validateWebhook();

?>
const CryptoJS = require("crypto-js");

/**
 * This method creates a concatenated string from the provided webhook URL, content type, and payload.
 * @param webhookUrl The URL where the webhook will be sent.
 * @param contentType The MIME type of the content.
 * @param payload The actual content body of the webhook.
 * @return A concatenated string of the provided parameters.
 */
function getBodyToHash(webhookUrl, contentType, payload) {
  const requestURL = new URL(webhookUrl);
  const bodyToHash = `${requestURL.pathname}${contentType}${payload}`;

  return bodyToHash;
}

/**
 * This method generates a signature from the provided secret and the hashed body.
 * It uses the HMAC-SHA256 hashing algorithm.
 * @param secret The secret key used to generate the hash.
 * @param hashBody The body that needs to be hashed.
 * @return The HMAC-SHA256 hash of the body, encoded as a hexadecimal string.
 */
function generateSignature(secretKey, hashBody) {
    const hasher = CryptoJS.HmacSHA256(hashBody, secretKey);
    const hashInHex = CryptoJS.enc.Hex.stringify(hasher);
  
    return hashInHex;
}

/**
 * This function prints out the signature from inputs.
 */
function validateWebhook(){
  // EXAMPLE TEST DATA
  // Secret key that can be found under Settings -> Manage Merchants -> Merchant Name in your BVNK merchant account
  const secretKey = 'YTczZmM5YTEtZDFhNi00YWU5LTk4ODktMmEzODI1NzBkMDI2ZWFjNDcwNzgtNTRkYi00OTY0LWI5NjAtMzRmOTQ0ZTE5NDQ3';

  // Webhook URL that you've set under Settings -> Manage Merchants -> Webhook URL
  const webhookUrl = 'https://webhook.site/11488aba-735e-4906-9809-f1a11a91f49f';

  // Content-Type needed for concatenating the values
  const contentType = 'application/json';

  // Body of the webhook.
  const payload = '{"source":"payment","event":"statusChanged","data":{"uuid":"83e96598-dd76-471c-a990-57a64468c436","merchantDisplayName":"Webhooks testing merchant","merchantId":"7cc7256f-124e-4b0b-aad4-251e131c0977","dateCreated":1685004317000,"expiryDate":1685177117000,"quoteExpiryDate":1685177117000,"acceptanceExpiryDate":1685004365000,"quoteStatus":"ACCEPTED","reference":"REF971604","type":"OUT","subType":"merchantPayOut","status":"COMPLETE","displayCurrency":{"currency":"EUR","amount":22.5,"actual":22.5},"walletCurrency":{"currency":"ETH","amount":0.01353,"actual":0.01353},"paidCurrency":{"currency":"ETH","amount":0.01353,"actual":0.01353},"feeCurrency":{"currency":"ETH","amount":0.00014,"actual":0.00014},"displayRate":{"base":"ETH","counter":"EUR","rate":1663.25022749567},"exchangeRate":{"base":"ETH","counter":"ETH","rate":1},"address":{"address":"0x84A4a239805d06c685219801B82BEA7c76702214","tag":null,"protocol":null,"uri":"ethereum:0x84A4a239805d06c685219801B82BEA7c76702214?value=1.352773E+16","alternatives":[]},"returnUrl":"","redirectUrl":"https://pay.sandbox.bvnk.com/payout?uuid=83e96598-dd76-471c-a990-57a64468c436","transactions":[{"dateCreated":1685004367300,"dateConfirmed":1685004367300,"hash":"0x2ac8db80e1d18a15656a564c3de7f085f1a42005cfa4d6fab9a2439ab7bd6f76","amount":0.01352773,"risk":null,"networkFeeCurrency":"ETH","networkFeeAmount":0.00000000,"sources":[],"displayRate":{"base":"ETH","counter":"EUR","rate":1663.25022749567},"exchangeRate":{"base":"ETH","counter":"ETH","rate":1}}],"refund":null,"refunds":[]}}';

  // Concatenate the webhook URL, Content-Type, and payload
  let hashBody = getBodyToHash(webhookUrl, contentType, payload);

  // Generate the signature
  let signature = generateSignature(secretKey, hashBody);

  // Output the signature
  console.log(`Generated signature: ${signature}`);
}

validateWebhook();
using System;
using System.Security.Cryptography;
using System.Text;
using System.Web;

class Program
{
    static void Main()
    {
        ValidateWebhook();
    }

    /*
     * This method creates a concatenated string from the provided webhook URL, content type, and payload.
     */
    static string GetBodyToHash(string webhookUrl, string contentType, string payload)
    {
        Uri requestUrl = new Uri(webhookUrl);
        string bodyToHash = $"{requestUrl.AbsolutePath}{contentType}{payload}";

        return bodyToHash;
    }

    /*
     * This method generates a signature from the provided secret and the hashed body.
     * It uses the HMAC-SHA256 hashing algorithm.
     */
    static string GenerateSignature(string secretKey, string hashBody)
    {
        using (var hmac = new HMACSHA256(Encoding.ASCII.GetBytes(secretKey)))
        {
            var hash = hmac.ComputeHash(Encoding.ASCII.GetBytes(hashBody));
            return BitConverter.ToString(hash).Replace("-", "").ToLower();
        }
    }

    /*
     * This function prints out the signature from inputs.
     */
    static void ValidateWebhook()
    {
        // Secret key that can be found under Settings -> Manage Merchants -> Merchant Name in your BVNK merchant account
        const string secretKey = "YTczZmM5YTEtZDFhNi00YWU5LTk4ODktMmEzODI1NzBkMDI2ZWFjNDcwNzgtNTRkYi00OTY0LWI5NjAtMzRmOTQ0ZTE5NDQ3";

        // Webhook URL that you've set under Settings -> Manage Merchants -> Webhook URL
        const string webhookUrl = "https://webhook.site/11488aba-735e-4906-9809-f1a11a91f49f";

        // Content-Type needed for concatenating the values
        const string contentType = "application/json";

        // Body of the webhook.
        const string payload = "{\"source\":\"payment\",\"event\":\"statusChanged\",\"data\":{\"uuid\":\"83e96598-dd76-471c-a990-57a64468c436\",\"merchantDisplayName\":\"Webhooks testing merchant\",\"merchantId\":\"7cc7256f-124e-4b0b-aad4-251e131c0977\",\"dateCreated\":1685004317000,\"expiryDate\":1685177117000,\"quoteExpiryDate\":1685177117000,\"acceptanceExpiryDate\":1685004365000,\"quoteStatus\":\"ACCEPTED\",\"reference\":\"REF971604\",\"type\":\"OUT\",\"subType\":\"merchantPayOut\",\"status\":\"COMPLETE\",\"displayCurrency\":{\"currency\":\"EUR\",\"amount\":22.5,\"actual\":22.5},\"walletCurrency\":{\"currency\":\"ETH\",\"amount\":0.01353,\"actual\":0.01353},\"paidCurrency\":{\"currency\":\"ETH\",\"amount\":0.01353,\"actual\":0.01353},\"feeCurrency\":{\"currency\":\"ETH\",\"amount\":0.00014,\"actual\":0.00014},\"displayRate\":{\"base\":\"ETH\",\"counter\":\"EUR\",\"rate\":1663.25022749567},\"exchangeRate\":{\"base\":\"ETH\",\"counter\":\"ETH\",\"rate\":1},\"address\":{\"address\":\"0x84A4a239805d06c685219801B82BEA7c76702214\",\"tag\":null,\"protocol\":null,\"uri\":\"ethereum:0x84A4a239805d06c685219801B82BEA7c76702214?value=1.352773E+16\",\"alternatives\":[]},\"returnUrl\":\"\",\"redirectUrl\":\"https://pay.sandbox.bvnk.com/payout?uuid=83e96598-dd76-471c-a990-57a64468c436\",\"transactions\":[{\"dateCreated\":1685004367300,\"dateConfirmed\":1685004367300,\"hash\":\"0x2ac8db80e1d18a15656a564c3de7f085f1a42005cfa4d6fab9a2439ab7bd6f76\",\"amount\":0.01352773,\"risk\":null,\"networkFeeCurrency\":\"ETH\",\"networkFeeAmount\":0.00000000,\"sources\":[],\"displayRate\":{\"base\":\"ETH\",\"counter\":\"EUR\",\"rate\":1663.25022749567},\"exchangeRate\":{\"base\":\"ETH\",\"counter\":\"ETH\",\"rate\":1}}],\"refund\":null,\"refunds\":[]}}";

        // Concatenate the webhook URL, Content-Type, and payload
        string hashBody = GetBodyToHash(webhookUrl, contentType, payload);

        // Generate the signature
        string signature = GenerateSignature(secretKey, hashBody);

        // Output the signature
        Console.WriteLine($"Generated signature: {signature}");
    }
}
require 'openssl'
require 'uri'
require 'json'

# This method creates a concatenated string from the provided webhook URL, content type, and payload.
# @param webhook_url [String] The URL where the webhook will be sent.
# @param content_type [String] The MIME type of the content.
# @param payload [String] The actual content body of the webhook.
# @return [String] A concatenated string of the provided parameters.
def get_body_to_hash(webhook_url, content_type, payload)
  request_url = URI.parse(webhook_url)
  body_to_hash = "#{request_url.path}#{content_type}#{payload}"

  return body_to_hash
end

# This method generates a signature from the provided secret and the hashed body.
# It uses the HMAC-SHA256 hashing algorithm.
# @param secret_key [String] The secret key used to generate the hash.
# @param hash_body [String] The body that needs to be hashed.
# @return [String] The HMAC-SHA256 hash of the body, encoded as a hexadecimal string.
def generate_signature(secret_key, hash_body)
  digest = OpenSSL::Digest.new('sha256')
  hmac = OpenSSL::HMAC.hexdigest(digest, secret_key, hash_body)

  return hmac
end

# This function prints out the signature from inputs.
def validate_webhook
  # EXAMPLE TEST DATA
  # Secret key that can be found under Settings -> Manage Merchants -> Merchant Name in your BVNK merchant account
  secret_key = 'YTczZmM5YTEtZDFhNi00YWU5LTk4ODktMmEzODI1NzBkMDI2ZWFjNDcwNzgtNTRkYi00OTY0LWI5NjAtMzRmOTQ0ZTE5NDQ3'

  # Webhook URL that you've set under Settings -> Manage Merchants -> Webhook URL
  webhook_url = 'https://webhook.site/11488aba-735e-4906-9809-f1a11a91f49f'

  # Content-Type needed for concatenating the values
  content_type = 'application/json'

  # Body of the webhook.
  payload = '{"source":"payment","event":"statusChanged","data":{"uuid":"83e96598-dd76-471c-a990-57a64468c436","merchantDisplayName":"Webhooks testing merchant","merchantId":"7cc7256f-124e-4b0b-aad4-251e131c0977","dateCreated":1685004317000,"expiryDate":1685177117000,"quoteExpiryDate":1685177117000,"acceptanceExpiryDate":1685004365000,"quoteStatus":"ACCEPTED","reference":"REF971604","type":"OUT","subType":"merchantPayOut","status":"COMPLETE","displayCurrency":{"currency":"EUR","amount":22.5,"actual":22.5},"walletCurrency":{"currency":"ETH","amount":0.01353,"actual":0.01353},"paidCurrency":{"currency":"ETH","amount":0.01353,"actual":0.01353},"feeCurrency":{"currency":"ETH","amount":0.00014,"actual":0.00014},"displayRate":{"base":"ETH","counter":"EUR","rate":1663.25022749567},"exchangeRate":{"base":"ETH","counter":"ETH","rate":1},"address":{"address":"0x84A4a239805d06c685219801B82BEA7c76702214","tag":null,"protocol":null,"uri":"ethereum:0x84A4a239805d06c685219801B82BEA7c76702214?value=1.352773E+16","alternatives":[]},"returnUrl":"","redirectUrl":"https://pay.sandbox.bvnk.com/payout?uuid=83e96598-dd76-471c-a990-57a64468c436","transactions":[{"dateCreated":1685004367300,"dateConfirmed":1685004367300,"hash":"0x2ac8db80e1d18a15656a564c3de7f085f1a42005cfa4d6fab9a2439ab7bd6f76","amount":0.01352773,"risk":null,"networkFeeCurrency":"ETH","networkFeeAmount":0.00000000,"sources":[],"displayRate":{"base":"ETH","counter":"EUR","rate":1663.25022749567},"exchangeRate":{"base":"ETH","counter":"ETH","rate":1}}],"refund":null,"refunds":[]}}'

  # Concatenate the webhook URL, Content-Type, and payload
  hash_body = get_body_to_hash(webhook_url, content_type, payload)

  # Generate the signature
  signature = generate_signature(secret_key, hash_body)

  # Output the signature
  puts "Generated signature: #{signature}"
end

validate_webhook

📘

Each new webhook will have a unique x-signature header value. Ensure you're verifying the hash against the correct header.

Handling Duplicate Events

There might be instances where your callback endpoints receive the same event more than once. To prevent processing duplicate events, consider logging each event you process. By doing this, you can easily identify and skip events that have already been logged.