Validate and verify webhook events

BoldSign will sign all the webhook events that are delivered to your endpoint by including a signature computed using HMAC with SHA256 in the X-BoldSign-Signature header. This signature can be used to verify the origin of the webhook and that the event was sent by BoldSign.

Retrieve endpoint secret

Before you can verify signatures, you need to retrieve your endpoint's secret from your manage webhook settings. The webhook signing secret key will be assigned once you have configured your webhook. You can reveal the secret by visiting the webhook overview page and clicking the "Reveal" button.

Step 1: Go to the webhook dashboard page and select a webhook item.

Webhook Step 1

Step 2: Click the "Reveal" button to reveal the signing secret key that is used to generate the HMAC SHA256 signatures from the webhook payload.

Webhook Step 2

Verify signatures

All webhook events have a signature in the header called X-BoldSign-Signature. This header's value is an HMAC with SHA256 signature generated with your webhook secret key, the time of the generated event, and the raw body of the webhook event.

Examples of signature from the request header:

Case 1: A regular webhook event signature header.

x-boldsign-signature: t=1668693823, s0=9b7adf82fbd470cce0cdb8106d7cb023e547999ada5e8c9ad02306cd22ee7ca9

Case 2: A webhook event signature header, when the secret is rolled with the option selected to keep the old secret valid for a specified time.

x-boldsign-signature: t=1668708521, s0=c10e26bf9c87a082f2e8b266fe95ed6e487e47851d9f5c5de40da0cb49b454ab, s1=62a1abe13bde9b464d0798fd9adb1b813109d1dba9e21a2bbb0a86a22a0621f6

Structure of the signature

x-boldsign-signature: t=timestamp-of-event, s0=signature-generated-by-current-key, s1=signature-generated-by-old-key-if-its-valid

Extract the timestamp and signature from the header

Extract the timestamp and signatures by splitting with , as a character separator. Next, split each item by = to get the key-value pair.

The t corresponds to the epoch timestamp of the event generated.

The s0 corresponds to the HMAC SHA256 signature generated using the current secret key.

The s1 corresponds to the HMAC SHA256 signature generated using the old secret key only if it is still valid at the time of event generation. If it is not valid, then the s1 will not be present.

The signed payload is created by concatenating the following:

  • The timestamp t
  • The character .
  • The raw JSON payload from the request body.

An example of the signed payload will be shown below.

1668708521.{"event":{"id":"ca7bf729-38ce-4d3a-a000-d4bf077201c3","created":1668693823,"eventType":"Signed","environment":"Test"}}

Once the signed payload is ready, compute the HMAC signature using the SHA256 hash function. Use the signing secret (copied from the webhook dashboard page using the "Reveal" Button) as the key and use the signed payload that just prepared above as the message.

Compare the signatures

Compare your calculated signature with the signature (or signatures) in the request header. If the signature does not match one of those sent in the request header, the request should be ignored and not treated as a legitimate BoldSign webhook event.

When there is an equality match, you can compute the difference between the current epoch timestamp and the request epoch timestamp 't,' and then decide whether or not to process the event based on your time tolerance.

To avoid timing attacks, compare the expected signature to each of the received signatures using a constant-time string comparison.

Important: To perform signature verification, BoldSign requires the raw body of the request. If you're going to use a framework, make sure it doesn't interfere with the raw body. Any changes to the request's raw body (such as JSON formatting or spaces etc.) cause the verification to fail.

namespace BoldSign.Examples
{
    using System;
    using System.IO;
    using System.Threading.Tasks;
    using BoldSign.Api;
    using BoldSign.Model.Webhook;
    using Microsoft.AspNetCore.Mvc;

    public class WebhookExampleController : Controller
    {
        [HttpPost]
        [IgnoreAntiforgeryToken]
        public async Task<IActionResult> Webhook()
        {
            var sr = new StreamReader(this.Request.Body);

            var json = await sr.ReadToEndAsync();

            if (this.Request.Headers[WebhookUtility.BoldSignEventHeader] == "Verification")
            {
                return this.Ok();
            }

            // TODO: Update your webhook secret key
            var SECRET_KEY = "<<<<SECRET_KEY>>>>";

            try
            {
                WebhookUtility.ValidateSignature(
                    json,
                    this.Request.Headers[WebhookUtility.BoldSignSignatureHeader],
                    boldSignSigningSecret);
            }
            catch (BoldSignSignatureException ex)
            {
                Console.WriteLine(ex);

                return this.Forbid();
            }

            var eventPayload = WebhookUtility.ParseEvent(json);

            switch (eventPayload.Event.EventType)
            {
                // if its a document event, cast as DocumentEvent
                case WebHookEventType.Declined:
                    var documentEvent = eventPayload.Data as DocumentEvent;

                    break;

                // if its a sender identity event, cast as SenderIdentityEvent
                case WebHookEventType.SenderIdentityUpdated:
                    var senderIdentityEvent = eventPayload.Data as SenderIdentityEvent;

                    break;
            }

            return this.Ok();
        }
    }
}
function parseHeader(header) {
    if (typeof header !== "string") {
        return null;
    }

    return header.split(",").reduce(
        (dest, item) => {
            const key = item.trim().split("=");
            if (key[0] === "t") {
                dest.timestamp = parseInt(key[1], 10);
            }
            if (["s0", "s1"].includes(key[0])) {
                dest.signatures.push(key[1]);
            }
            return dest;
        },
        {
            timestamp: -1,
            signatures: [],
        }
    );
}

function secureCompare(a, b) {
    a = Buffer.from(a);
    b = Buffer.from(b);

    if (a.length !== b.length) {
        return false;
    }

    if (crypto.timingSafeEqual) {
        return crypto.timingSafeEqual(a, b);
    }

    const len = a.length;
    let result = 0;
    for (let i = 0; i < len; ++i) {
        result |= a[i] ^ b[i];
    }

    return result === 0;
}

function isFromBoldSign(signatureHeader, payload, secretKey) {
    const parsed = parseHeader(signatureHeader);

    if (!parsed) {
        throw "BoldSign signatures doesn't exist";
    }

    const signatureMatched = parsed.signatures
        .map((x) => {
            // TODO: update signing secret here
            // https://app.boldsign.com/api-management/webhooks/
            return crypto
                .createHmac("sha256", secretKey)
                .update(parsed.timestamp + "." + payload, "utf8")
                .digest("hex");
        })
        .some((x) => {
            return parsed.signatures.some((y) => secureCompare(x, y));
        });

    if (signatureMatched == false) {
        throw "Unable to verify the signatures";
    }

    // 5 mins in seconds is safer choice, you can adjust if you prefer it
    const tolerance = 300;
    const timestampAge = Math.floor(Date.now() / 1000) - parsed.timestamp;

    // check for time tolerance to prevent replay attacks
    if (tolerance > 0 && timestampAge > tolerance) {
        throw "Exceeded allowed tolerance range";
    }

    return true;
}

const express = require("express");
...
...
const app = express();

app.post("/webhook", bodyParser.raw({ type: "application/json" }), (req, res) => {
    const eventType = req.headers["x-boldsign-event"];

    if (eventType == "Verification") {
        res.sendStatus(200);
        return;
    }

    const signature = req.headers["x-boldsign-signature"];
    const payload = req.body.toString("utf-8");

    // TODO: Update your webhook secret key
    const SECRET_KEY = "<<<<SIGNING-SECRET>>>>";
    let isValid = false;

    try {
        isValid = isFromBoldSign(signature, payload, SECRET_KEY);
    } catch (e) {
        console.error(e);

        res.sendStatus(400);

        return;
    }

    if (!isValid) {
        res.sendStatus(403);

        return;
    }

    // handle the event
    res.sendStatus(200);
});
# TODO: Update your webhook secret key
SECRET_KEY = "<<<<SECRET_KEY>>>>"

require "sinatra"
require "openssl"

module BoldSign
  class BoldSignError < StandardError
  end

  def self.is_from_boldsign(payload, header, secret, tolerance: 300)
    begin
      timestamp, signatures = parse_header(header)
    rescue StandardError
      raise BoldSignError.new(
        "Unable to parse timestamp from the header"
      )
    end

    if signatures.empty?
      raise BoldSignError.new(
        "Unable to parse signatures from the header"
      )
    end

    expected_sig = compute_signature(timestamp, payload, secret)

    unless signatures.any? { |s| secure_compare(expected_sig, s) }
      raise BoldSignError.new(
        "No signatures found matching with the computed signature for payload"
      )
    end

    if tolerance && timestamp < Time.now - tolerance
      raise BoldSignError.new(
        "Payload timestamp was outside the tolerance zone (#{Time.at(timestamp)})"
      )
    end
  end

  true

  def self.parse_header(header)
    list_items = header.split(/,\s*/).map { |i| i.split("=", 2) }
    timestamp = Integer(list_items.select { |i| i[0] == "t" }[0][1])
    signatures = list_items.select { |i| (i[0] == "s0" || i[0] == "s1") }.map { |i| i[1] }
    [Time.at(timestamp), signatures]
  end

  def self.compute_signature(timestamp, payload, secret)
    raise ArgumentError, "timestamp should be an instance of Time" unless timestamp.is_a?(Time)
    raise ArgumentError, "payload should be a string" unless payload.is_a?(String)
    raise ArgumentError, "secret should be a string" unless secret.is_a?(String)

    timestamped_payload = "#{timestamp.to_i}.#{payload}"
    OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, timestamped_payload)
  end

  def self.secure_compare(str_a, str_b)
    return false unless str_a.bytesize == str_b.bytesize

    l = str_a.unpack "C#{str_a.bytesize}"

    res = 0
    str_b.each_byte { |byte| res |= byte ^ l.shift }
    res.zero?
  end
end

# Using the Sinatra framework
set :port, 4242

post "/webhook" do
  payload = request.body.read
  event_header = request.env["HTTP_X_BOLDSIGN_EVENT"]

  if event_header == "Verification"
    return status 200
  end

  header = request.env["HTTP_X_BOLDSIGN_SIGNATURE"]

  begin
    BoldSign.is_from_boldsign(payload, header, SECRET_KEY)
  rescue BoldSign::BoldSignError => e
    # Invalid signature
    status 400
    return
  end

  # Handle the event

  status 200
end
from django.http import HttpResponse
import hashlib
import hmac
import time

# TODO: Update your webhook secret key
SECRET_KEY = "<<<<SECRET_KEY>>>>"


class BoldSignWebhookError(Exception):
    def __init__(self, message):
        self.message = message
        super().__init__(self.message)


class BoldSignHelper:

    @staticmethod
    def is_from_boldsign(signature: str, body: str, secretKey: str, tolerance=300) -> bool:
        parsed = BoldSignHelper.parse_header(signature)
        timestamp = parsed.timestamp
        signatures = parsed.signatures

        if timestamp == -1:
            raise BoldSignWebhookError("Unable to parse timestamp")

        if signature.count == 0:
            raise BoldSignWebhookError(
                "Unable to find any signature from the provided header value")

        computedSignature = hmac.new(
            key=secretKey.encode('utf-8'),
            msg=(str(timestamp) + "." + body).encode('utf-8'),
            digestmod=hashlib.sha256
        ).hexdigest()

        matched = False

        for sign in signatures:
            if hmac.compare_digest(sign, computedSignature):
                matched = True

        if matched == False:
            raise BoldSignWebhookError("HMAC SHA256 signatures doesn't match")

        age = time.time() - timestamp

        if tolerance > 0 and age > tolerance:
            raise BoldSignWebhookError(
                "Event is outside of the timestamp age tolerance")

        return True

    @staticmethod
    def parse_header(header: str):
        class ParsedHeader:
            def __init__(self, timestamp, signatures):
                self.timestamp = timestamp
                self.signatures = signatures

        if the header is None:
            raise BoldSignWebhookError(
                "Signature header value seems to be empty")

        timestamp = -1
        signatures = []
        for item in header.split(","):
            x, y = item.strip().split("=")
            if x == "t":
                timestamp = int(y)
            elif x == "s0" or x == "s1":
                signatures.append(y)

        return ParsedHeader(timestamp, signatures)


@csrf_exempt
def webhook_view(request):
    payload = request.body
    event_header = request.META['HTTP_X_BOLDSIGN_EVENT']
    if event_header == "Verification":
        return HttpResponse(status=200)

    header = request.META['HTTP_X_BOLDSIGN_SIGNATURE']

    try:
        BoldSignHelper.is_from_boldsign(header, payload, SECRET_KEY)
    except BoldSignWebhookError as e:
        return HttpResponse(status=400)

    # Handle the event
    return HttpResponse(status=200)

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

// TODO: Update your secret key
const SECRET_KEY = "<<<<SECRET_KEY>>>>"

var (
	ErrNoTimestamp      = errors.New("Unable to parse timestamp from the header")
	ErrNoSignature      = errors.New("Unable to parse signatures from the header")
	ErrNoValidSignature = errors.New("No signatures found matching with the computed signature for payload")
	ErrTolerance        = errors.New("Payload timestamp was outside the tolerance zone")
)

type signedHeader struct {
	timestamp  time.Time
	signatures [][]byte
}

func IsFromBoldSign(payload []byte, header string, secret string, tolerance time.Duration) error {
	parsed, err := parseSignatureHeader(header)

	if err != nil {
		return err
	}

	expectedSignature := ComputeSignature(parsed.timestamp, payload, secret)
	expiredTimestamp := time.Since(parsed.timestamp) > tolerance

	if expiredTimestamp {
		return ErrTolerance
	}

	for _, sig := range parsed.signatures {
		if hmac.Equal(expectedSignature, sig) {
			return nil
		}
	}

	return ErrNoValidSignature
}

func parseSignatureHeader(header string) (*signedHeader, error) {
	parsed := &signedHeader{}

	if header == "" {
		return parsed, ErrNoSignature
	}

	pairs := strings.Split(strings.Trim(header, " "), ",")
	for _, pair := range pairs {
		parts := strings.Split(strings.Trim(pair, " "), "=")
		if len(parts) != 2 {
			return parsed, ErrNoTimestamp
		}

		var key = strings.Trim(parts[0], " ")

		switch key {
		case "t":
			timestamp, err := strconv.ParseInt(strings.Trim(parts[1], " "), 10, 64)
			if err != nil {
				return parsed, ErrNoTimestamp
			}
			parsed.timestamp = time.Unix(timestamp, 0)

		case "s0", "s1":
			sig, err := hex.DecodeString(strings.Trim(parts[1], " "))
			if err != nil {
				continue
			}

			parsed.signatures = append(parsed.signatures, sig)

		default:
			continue
		}
	}

	if len(parsed.signatures) == 0 {
		return parsed, ErrNoValidSignature
	}

	return parsed, nil
}

func ComputeSignature(t time.Time, payload []byte, secret string) []byte {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(fmt.Sprintf("%d", t.Unix())))
	mac.Write([]byte("."))
	mac.Write(payload)

	return mac.Sum(nil)
}

func main() {
	http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) {
		const MaxBodyBytes = int64(65536)
		req.Body = http.MaxBytesReader(w, req.Body, MaxBodyBytes)
		payload, err := ioutil.ReadAll(req.Body)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err)
			w.WriteHeader(http.StatusServiceUnavailable)
			return
		}

		x := req.Header.Get("X-BoldSign-event")

		if x == "Verification" {
			w.WriteHeader(http.StatusOK)
			return
		}

		x = req.Header.Get("X-BoldSign-Signature")
		t, _ := time.ParseDuration("5m")

		err = IsFromBoldSign(payload, x, SECRET_KEY, t)

		if err != nil {
			// Return a 400 when verification fails
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		// handle event
		w.WriteHeader(http.StatusOK)
	})

	err := http.ListenAndServe(":3333", nil)
	if errors.Is(err, http.ErrServerClosed) {
		fmt.Printf("server closed\n")
	} else if err != nil {
		fmt.Printf("error starting server: %s\n", err)
		os.Exit(1)
	}
}
<?php


class BoldSignException extends \Exception
{
}

class BoldSignHelper
{
    // Timestamp tolerance is default to 300 seconds (5 minutes)
    public static function isFromBoldSign($payload, $header, $secret, $tolerance = 300)
    {
        $timestamp = self::getTimestamp($header);
        $signatures = self::getSignatures($header);

        if ($timestamp === -1) {
            throw new BoldSignException('Unable to parse timestamp from the header');
        }

        if (empty($signatures)) {
            throw new BoldSignException('Unable to parse signatures from the header');
        }

        $signedPayload = "{$timestamp}.{$payload}";
        $computedSignature = self::computeSignature($signedPayload, $secret);
        $isMatched = false;

        foreach ($signatures as $signature) {
            if (self::secureCompare($computedSignature, $signature)) {
                $isMatched = true;

                break;
            }
        }

        if (!$isMatched) {
            throw new BoldSignException('No signatures found matching with the computed signature for payload');
        }

        if (($tolerance > 0) && (\abs(\time() - $timestamp) > $tolerance)) {
            throw new BoldSignException('Payload timestamp was outside the tolerance zone');
        }

        return true;
    }


    private static function getTimestamp($header)
    {
        $items = \explode(',', $header);

        foreach ($items as $item) {
            $itemParts = \explode('=', $item, 2);
            if ($itemParts[0] === 't') {
                if (!\is_numeric($itemParts[1])) {
                    return -1;
                }

                return (int) ($itemParts[1]);
            }
        }

        return -1;
    }

    private static function getSignatures($header)
    {
        $signatures = [];
        $items = \explode(',', $header);

        foreach ($items as $item) {
            $itemParts = \explode('=', $item, 2);
            if (\trim($itemParts[0]) === "s0" || \trim($itemParts[0]) === "s1") {
                $signatures[] = $itemParts[1];
            }
        }

        return $signatures;
    }

    private static function computeSignature($payload, $secret)
    {
        return \hash_hmac('sha256', $payload, $secret);
    }

    private static function secureCompare($a, $b)
    {
        if (\strlen($a) !== \strlen($b)) {
            return false;
        }

        $result = 0;
        for ($i = 0; $i < \strlen($a); ++$i) {
            $result |= \ord($a[$i]) ^ \ord($b[$i]);
        }

        return $result === 0;
    }
}

$event_header = $_SERVER['HTTP_X_BOLDSIGN_EVENT'];

if ($event_header == "Verification") {
    http_response_code(200);

    return;
}

// TODO: Update your webhook secret key
$SECRET_KEY = "<<<<SECRET_KEY>>>>";

$payload = @file_get_contents('php://input');
$header = $_SERVER['HTTP_X_BOLDSIGN_SIGNATURE'];

try {
    $isFromBoldSign = BoldSignHelper::isFromBoldSign($payload, $header, $SECRET_KEY);
} catch (BoldSignException $e) {
    // Invalid signature
    http_response_code(400);
    exit();
}

// Handle the event

http_response_code(200);
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import com.sun.net.httpserver.HttpServer;

public class Server {
    // TODO: Update your secret key
    private static final String SECRET_KEY = "<<<<SECRET_KEY>>>>";

    public static void main(String[] args) {
        try {
            HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);

            server.createContext("/", httpExchange -> {
                var eventHeader = httpExchange.getRequestHeaders().get("x-boldsign-event");
                if (eventHeader.get(0).equals("Verification")) {
                    httpExchange.sendResponseHeaders(200, 0);
                    httpExchange.getResponseBody().close();

                    return;
                }

                var sigHeaders = httpExchange.getRequestHeaders().get("x-boldsign-signature");
                var requestBody = new String(httpExchange.getRequestBody().readAllBytes());

                String signatureHeader = sigHeaders.get(0);
                try {
                    BoldSignHelper.isFromBoldsign(signatureHeader, requestBody, SECRET_KEY,
                            300);

                    httpExchange.sendResponseHeaders(200, 0);
                    httpExchange.getResponseBody().close();
                } catch (BoldsignException e) {
                    httpExchange.sendResponseHeaders(403, 0);
                    httpExchange.getResponseBody().close();
                    return;
                }
            });

            server.start();
        } catch (Throwable tr) {
            tr.printStackTrace();
        }
    }
}

class BoldSignHelper {

    // Default time tolerance is 300 (5 minutes)
    public static boolean isFromBoldsign(String signatureHeader, String rawRequestBody, String secretKey)
            throws BoldsignException {
        return isFromBoldsign(signatureHeader, rawRequestBody, secretKey, 300);
    }

    // Default time tolerance is 300 (5 minutes)
    public static boolean isFromBoldsign(String signatureHeader, String rawRequestBody, String secretKey, int tolerance)
            throws BoldsignException {
        if (signatureHeader == null || signatureHeader.isEmpty()) {
            throw new BoldsignException("Signature header cannot be null or empty");
        }

        if (rawRequestBody == null || rawRequestBody.isEmpty()) {
            throw new BoldsignException("RAW request body string cannot be null or empty");
        }

        if (secretKey == null || secretKey.isEmpty()) {
            throw new BoldsignException("Secret Key cannot be null or empty");
        }

        ParseHeaders parseHeader = parseHeader(signatureHeader);

        if (parseHeader.timestamp == -1) {
            throw new BoldsignException("Unable to parse timestamp from the header");
        }

        if (parseHeader.signatures.size() == 0) {
            throw new BoldsignException("Unable to parse signatures from the header");
        }

        String computedSignature;

        try {
            computedSignature = computeHamcSignature(secretKey, parseHeader.timestamp + "." + rawRequestBody);
        } catch (Exception e) {
            throw new BoldsignException("Unable to compute signature for payload");
        }

        var isMatched = false;

        for (String signature : parseHeader.signatures) {
            if (secureCompare(computedSignature, signature)) {
                isMatched = true;
                break;
            }
        }

        if (!isMatched) {
            throw new BoldsignException("No signatures found matching with the computed signature for payload");
        }

        // Check tolerance
        if ((tolerance > 0) && (parseHeader.timestamp < ((System.currentTimeMillis() / 1000L) - tolerance))) {
            throw new BoldsignException("Timestamp outside the tolerance zone");
        }

        return true;
    }

    private static ParseHeaders parseHeader(String signatureHeader) throws BoldsignException {
        var timestamp = -1;
        List<String> parsedSignatures = new ArrayList<String>();
        for (String header : signatureHeader.split(",")) {
            List<String> pair = Arrays.asList(header.strip().split("=", -1))
                    .stream()
                    .map(x -> x.strip())
                    .dropWhile(x -> x == null || x.isEmpty())
                    .collect(Collectors.toList());

            if (pair.size() == 0 || pair.size() == 1) {
                throw new BoldsignException("Expected signature header value seems to be empty");
            }

            switch (pair.get(0)) {
                case "t":
                    timestamp = Integer.parseInt(pair.get(1));

                    break;

                case "s0":
                case "s1":
                    parsedSignatures.add(pair.get(1));

                    break;
            }
        }

        return new ParseHeaders(timestamp, parsedSignatures);
    }

    private static String computeHamcSignature(String secretKey, String payload) {
        byte[] hmacSha256 = null;
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");
            mac.init(secretKeySpec);
            hmacSha256 = mac.doFinal(payload.getBytes());
        } catch (Exception e) {
            throw new RuntimeException("Failed to calculate hmac-sha256", e);
        }

        return String.format("%064x", new BigInteger(1, hmacSha256));
    }

    private static boolean secureCompare(String a, String b) {
        byte[] digesta = a.getBytes(StandardCharsets.UTF_8);
        byte[] digestb = b.getBytes(StandardCharsets.UTF_8);

        return MessageDigest.isEqual(digesta, digestb);
    }

}

class BoldsignException extends Exception {
    public BoldsignException() {
    }

    public BoldsignException(String errorMessage) {
        super(errorMessage);
    }
}

class ParseHeaders {
    public final int timestamp;
    public final List<String> signatures;

    public ParseHeaders(int timestamp, List<String> signatures) {
        this.timestamp = timestamp;
        this.signatures = signatures;
    }
}

Roll secret key

The webhook signing secret key can be changed by using the "Roll Secret" option. Rolling the secret will also provide you option on how long you want to keep the old secret in order to provide you the optimal time to switch the old secret for the new one within your application. This will be useful when you want to roll secret in a production application to avoid downtime.

Step 1: Click the "Roll Secret" button to reveal the roll secret option.

Webhook Step 3

Step 2: Choose the expiration date for the current secret. You can select up to 24 hours, and webhook events will include signatures generated using both the old and new signing secret keys.

Webhook Step 4

Importance of event verification

Since the webhook endpoints are open and public, they can be called by anyone and compromise your application data. So, all the webhook events sent to your endpoint must be validated to ensure that they came from the BoldSign.