Creating a Webhook Listener

Create a webhook listener to receive information on any payment lifecycle changes.

Beem 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 platform. By this point, you should have already configured your first MID using our API endpoints.


Webhook Validation

The validation process requires a public key, which can be obtained here:

  1. Navigate to the "Hooks" section:

  2. View your Webhook details:

  3. Your public key:


The validation involves two primary steps:

  1. Loading the public key from a Base64-encoded string.
  2. Verifying the payload signature against the public key.

Requirements

  • Public Key: A Base64-encoded RSA public key provided by our team. This key is required to validate the signature.
  • Webhook Payload: The exact JSON payload received from the webhook.
  • Signature Header: The Base64-encoded signature is provided in the x-signature header of the webhook request.

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 providerPublicKey, payload and the x-signature.

import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

/**
 * Utility class for validating webhook signatures using RSA and SHA256.
 * This ensures the integrity and authenticity of the received payload.
 */
public class WebhookValidator {

    /**
     * Loads a public key from a Base64-encoded string.
     *
     * @param publicKeyBase64 The Base64-encoded RSA public key.
     * @return A {@link PublicKey} instance created from the provided Base64 string.
     * @throws Exception If an error occurs while parsing the key.
     */
    public static PublicKey loadPublicKey(String publicKeyBase64) throws Exception {
        // Decode the Base64 public key string
        byte[] decoded = Base64.getDecoder().decode(publicKeyBase64);

        // Generate a PublicKey instance using RSA algorithm
        X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePublic(spec);
    }

    /**
     * Verifies a signature using the RSA public key and SHA256 algorithm.
     *
     * @param payload        The exact JSON payload (body) received from the webhook.
     * @param signatureBase64 The Base64-encoded signature from the `x-signature` header.
     * @param publicKey      The RSA public key used to verify the signature.
     * @return {@code true} if the signature is valid; {@code false} otherwise.
     * @throws Exception If an error occurs during signature verification.
     */
    public static boolean verifySignature(String payload, String signatureBase64, PublicKey publicKey) throws Exception {
        // Decode the Base64-encoded signature
        byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64);

        // Initialize the Signature object with the RSA and SHA256 algorithm
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(publicKey);
        signature.update(payload.getBytes());

        // Perform the verification
        return signature.verify(signatureBytes);
    }

    /**
     * Main method for testing the webhook validation logic.
     * Simulates a received webhook payload and signature.
     *
     * @param args Command-line arguments (not used).
     */
    public static void main(String[] args) {
        try {
            // The public key provided by the webhook provider
            String providerPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA16QGX+FwSFT6CFHRtLM36S8xRfQqX4ylFZmVv8tCBONQnfudMw9R/5DfkjvoYg8wHiqDfs+foo59kFZRM/IRN5u+suohd0L8xnZJIFFj/aChYEkPPvOfg0DznYox1rKD5RWFKoJ4z16sPvGEtd60HKr5OB9YCC5InhdGgnxgn/whF391YKqkyZGGAP2Oe9FBW+J6BgT4gFLbprHHvf95taWU42wAc9itCd+X3/txyQIx7yN6cXX4LJqnTxnSZ2AG5CZ4YJFdq220zFHc92mAWGZmBDMC5av6wXGwHd8+kuJh8+ZF+r97bPdz6tY73f/yiNADu0jzZatVi6YM5C2uRQIDAQAB";

            // The webhook payload received (example payload for testing)
            String payload = "{\"event\":\"layer1:payment:channel:transaction-confirmed\",\"eventId\":\"019390f7-83e3-7e01-98d2-c38912094105\",\"timestamp\":\"2024-12-04T09:19:20.547757183Z\",\"data\":{\"tag\":null,\"hash\":\"0x5b7ee3b9471dd917d058e51fd6a439a871dd702a0c086d2ac955f7a13567f874\",\"risk\":{\"level\":\"UNKNOWN\",\"alerts\":[],\"resourceName\":\"UNKNOWN\",\"resourceCategory\":\"UNKNOWN\"},\"uuid\":\"019390f7-7829-7009-9b2a-2d9eae8de317\",\"status\":\"COMPLETE\",\"address\":\"0x5daa8be8552560126cfae73db1beb3e6ee4af871\",\"sources\":[\"0xddcd0aa2c21d2d02ec0977565d037abc67f7f151\"],\"channelId\":\"5bbeb024-930b-4efc-9332-1b6f6399696b\",\"feeAmount\":9.808E-5,\"reference\":\"Test-ETH-Mid-Chan-ETH-1\",\"merchantId\":\"cd513db7-0b5b-4e35-8420-957059f2324f\",\"networkFee\":{\"paidAmount\":1.38352775169E-4,\"paidCurrency\":\"ETH\",\"displayAmount\":0.47,\"displayCurrency\":\"EUR\"},\"paidAmount\":0.002,\"dateCreated\":1733303957545,\"displayRate\":{\"base\":\"ETH\",\"rate\":3405.0,\"counter\":\"EUR\"},\"feeCurrency\":\"ETH\",\"lastUpdated\":1733303957578,\"exchangeRate\":{\"base\":\"ETH\",\"rate\":1.0,\"counter\":\"ETH\"},\"paidCurrency\":\"ETH\",\"walletAmount\":0.002,\"displayAmount\":6.81,\"walletCurrency\":\"ETH\",\"displayCurrency\":\"EUR\",\"merchantDisplayName\":\"eth wallet\"}}";

            // The x-signature header value from the webhook
            String xSignature = "I7iZhnaSehO4fxjZFXaGklxKMcusOzKeVwEeeGQNpYneQ8qTogoR15mU/G7no2pJwok9tihC16gIKwHgCNGIAU3w6QtoTqi/H8YR7ZdCjtNzInAiL4uhJWx9iRJTaxcGhS9SvqVtMIRQ/2bf3MuOno3dnu7pTxI+YgCoRiAS978qtjHJHb1aybzMq1wkO8pLtau7oK6AZmI8F52EIB4rbXppPnyOa1HEaGDggbu0+Kh4ANtW1eAqg6eU4yozMMXirwyeQBzLKIw6Qw2jNHd4VUvCVpHDAx26KkeroGUKruwKPgK520ALOOrJE0wVvFv56erd7JjivgTJIxa5eZiASQ==";

            // Load the public key from the provider
            PublicKey publicKey = loadPublicKey(providerPublicKey);

            // Verify the signature
            boolean isValid = verifySignature(payload, xSignature, publicKey);

            if (isValid) {
                System.out.println("Webhook is valid.");
            } else {
                System.out.println("Webhook is invalid!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
<?php

function verifyECDSASignatureExample(): bool
{
    // Example data embedded in the function
    $publicKeyBase64 = "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAExn8LhKa3YnVvGHeyT+siyu9+B5knDRtigP4R08nw7Fp0lbXtwoiAO1N0LOj7k39JY5iM385BJrRV2u5Y4N0Qxg==";
    $signatureBase64 = "MEYCIQCtvKgMTivqsT3S2G3qD46lK0+FD7ECW4dK2MtaivfWvwIhALJly6ZqemabK+gYGNWpZACzj1ApJ6immVuIQ0MxONXV";
    $message = "hello world";
    
    try {
        // Decode the base64-encoded public key
        $publicKeyDer = base64_decode($publicKeyBase64);
        if ($publicKeyDer === false) {
            throw new Exception("Failed to decode public key");
        }

        // Create a PEM-formatted public key from the DER data
        $publicKeyPem = "-----BEGIN PUBLIC KEY-----\n" . 
                        chunk_split(base64_encode($publicKeyDer), 64, "\n") . 
                        "-----END PUBLIC KEY-----\n";

        // Load the public key
        $publicKey = openssl_pkey_get_public($publicKeyPem);
        if ($publicKey === false) {
            throw new Exception("Failed to load public key: " . openssl_error_string());
        }

        // Decode the signature
        $signature = base64_decode($signatureBase64);
        if ($signature === false) {
            throw new Exception("Failed to decode signature");
        }

        // Verify the signature
        $result = openssl_verify($message, $signature, $publicKey, OPENSSL_ALGO_SHA256);
        
        // Clean up
        openssl_free_key($publicKey);
        
        if ($result === 1) {
            echo "Signature verification succeeded!\n";
            return true;
        } elseif ($result === 0) {
            echo "Signature verification failed!\n";
            return false;
        } else {
            throw new Exception("Error during signature verification: " . openssl_error_string());
        }
        
    } catch (Exception $e) {
        echo "Error: " . $e->getMessage() . "\n";
        return false;
    }
}

// Run the example
verifyECDSASignatureExample();

?>
📘

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.