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:
-
Navigate to the "Hooks" section:
-
View your Webhook details:

-
Your public key:
The validation involves two primary steps:
- Loading the public key from a Base64-encoded string.
- 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 uniquex-signatureheader 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.
Updated 10 months ago
