Webhooks

Webhooks are critical for real-time notifications within our Layer1 platform, enabling automated updates related to payments, conversions, or new address creation events. When such events occur, Layer1 makes an HTTP request to the URL configured by the user for the webhooks.

Acknowledging Webhooks

The Layer1 webhooks service expects to receive a 200 HTTP status code as an acknowledgment of a successfully received and validated webhook. This response should be sent immediately upon receipt of the webhook to confirm its delivery and avoid timeout errors.

Webhook retry policy

If BVNK does not receive a 200 status code, the webhook retry policy will kick in.

The retry policy adds a delay before each retry, starting with a base delay and increasing the time between retries. The delay grows larger after each attempt, but will not exceed 15 minutes. The system will attempt up to 100 times, and after that, no further retries will occur.

Webhook Validation

Webhooks include an x-signature header, which enables servers to authenticate the request using a secret key specific to your server setup.

Signature Calculation with ECDSA

The process to calculate and verify the signature using ECDSA

🚧

Capture the raw payload

Do not parse the incoming HTTP request into an object or re-serialize it. Instead, capture the raw payload as a string to ensure that the signature verification process is accurate.


import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;

import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;

public class JavaSignatureExample {
  public static void main(String[] args) throws Exception {
    // If you don't already have BouncyCastle as a provider, add it
    if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
      Security.addProvider(new BouncyCastleProvider());
    }

    // Public key provided from webhook configuration
    String publicKeyBase64 = "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAExn8LhKa3YnVvGHeyT+siyu9+B5knDRtigP4R08nw7Fp0lbXtwoiAO1N0LOj7k39JY5iM385BJrRV2u5Y4N0Qxg==";

    // From X-Signature header
    String signatureBase64 = "MEYCIQCtvKgMTivqsT3S2G3qD46lK0+FD7ECW4dK2MtaivfWvwIhALJly6ZqemabK+gYGNWpZACzj1ApJ6immVuIQ0MxONXV";

    // Body of the request
    String body = "hello world";

    KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", "BC");
    X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.decode(publicKeyBase64));


    PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);

    Signature signature = Signature.getInstance("SHA256withECDSA");
    signature.initVerify(publicKey);

    // The body of the request
    signature.update(body.getBytes());

    boolean verified = signature.verify(Base64.decode(signatureBase64));

    System.out.println("Signature is valid: " + verified);
  }
}
<?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();

?>
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
import base64

# Replace this with your webhook public key
public_key_base64 = "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAExn8LhKa3YnVvGHeyT+siyu9+B5knDRtigP4R08nw7Fp0lbXtwoiAO1N0LOj7k39JY5iM385BJrRV2u5Y4N0Qxg=="

# This comes from X-Signature header
signature_base64 = "MEYCIQCtvKgMTivqsT3S2G3qD46lK0+FD7ECW4dK2MtaivfWvwIhALJly6ZqemabK+gYGNWpZACzj1ApJ6immVuIQ0MxONXV"

# This is the body of the request sent to you
message = b"hello world"

# Load the public key from the base64-encoded string
public_key_loaded = serialization.load_der_public_key(
    base64.b64decode(public_key_base64)
)

# Verify the signature using the loaded public key
try:
    public_key_loaded.verify(
        base64.b64decode(signature_base64),
        message,
        ec.ECDSA(hashes.SHA256())
    )
    print("\nSignature verification succeeded!")
except InvalidSignature:
    print("\nSignature verification failed!")

Webhook details

For the detailed description of webhooks and their fields, see the Layer1 webhook reference.