The BoldSign mobile app is now available. Visitthis link for more details and give it a try!

The BoldSign mobile app is now available. Visitthis link for more details and give it a try!

Request Demo
BoldSign Logo Small


Explore the BoldSign features that make eSigning easier.

Automatically Download eSignature Documents Using Webhook Callbacks

Automatically Download eSignature Documents Using Webhook Callbacks

BoldSign already simplifies the eSignature process, and with the integration of webhooks, you can further automate your document management. This blog post will guide you through the process of automatically downloading eSignature documents as soon as they are completed by the signers using webhook callbacks.

What are Webhooks?

Webhooks are automated messages sent from apps when specific events occur. They are a simple and effective way to receive real-time updates without constantly polling a server for data. In the context of BoldSign, webhooks notify your application when events, such as when a document is signed, occur.

Setting Up Webhooks in BoldSign

To get started, you’ll need to set up a webhook in BoldSign. Refer to the Webhooks Introduction in the BoldSign documentation to set up webhooks in BoldSign. Please select the Completed event while setting up the webhooks. This ensures you receive notification from webhooks when a document has been fully signed.

Listening for Webhook Events

Once you have set up the webhook in BoldSign, the next step is to configure your server to listen for these events and handle them appropriately.


namespace BoldSignWebhookListener
    using System;
    using System.IO;
    using System.Threading.Tasks;
    using BoldSign.Api;
    using BoldSign.Model.Webhook;
    using Microsoft.AspNetCore.Mvc;
    public class WebhookExampleController : ControllerBase
        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>>>";

            catch (BoldSignSignatureException ex)

                return this.Forbid();

            var eventPayload = WebhookUtility.ParseEvent(json);

            if (eventPayload.Event.EventType == WebHookEventType.Completed)
                var documentEvent = eventPayload.Data as DocumentEvent;
                if (documentEvent != null)
                    await DownloadCompletedDocument(documentEvent.DocumentId);

            return this.Ok();
        private async Task DownloadCompletedDocument(string documentId)
                var apiClient = new ApiClient("", "{Your API Key}");
                var documentClient = new DocumentClient(apiClient);
                var documentStream = documentClient.DownloadDocument(documentId);

                // Check if the document stream is not null.
                if (documentStream != null)
                    // Specify the path where the document should be saved.
                    var downloadsPath = @"{Specify the path where the document should be saved}";

                    // Ensure the directory exists
                    if (!Directory.Exists(downloadsPath))
                    var filePath = Path.Combine(downloadsPath, $"{documentId}.pdf");
                    await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
                    await documentStream.CopyToAsync(fileStream);   
                    Console.WriteLine($"Document {documentId} downloaded successfully.");
                    Console.WriteLine($"Error downloading document {documentId}: Document stream is null.");
            catch (Exception ex)
                Console.WriteLine($"Error downloading document {documentId}: {ex}");


const express = require("express");
const bodyParser = require("body-parser");
const crypto = require("crypto");
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])) {
            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 new Error("BoldSign signatures don't exist");

    const signatureMatched = parsed.signatures
        .map((x) => {
            return crypto
                .createHmac("sha256", secretKey)
                .update(parsed.timestamp + "." + payload, "utf8")
        .some((x) => {
            return parsed.signatures.some((y) => secureCompare(x, y));

    if (!signatureMatched) {
        throw new Error("Unable to verify the signatures");

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

    // Check for time tolerance to prevent replay attacks.
    if (tolerance > 0 && timestampAge > tolerance) {
        throw new Error("Exceeded allowed tolerance range");

    return true;

const app = express();
const PORT = 3000;

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

    if (eventType === "Verification") {

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

    // TODO: Update your webhook secret key
    const SECRET_KEY = "<<<Secret Key>>>";
    let isValid = false;

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

    if (!isValid) {

    // Handle the event.
    console.log("Event received:", eventType);

    if (eventType === "Completed") {
        const axios = require('axios');
        const fs = require('fs');
        const path = require('path');

        const payloadObject = JSON.parse(payload);
        // Make the request to download the document.
        axios.get('', {
            params: {
            responseType: "stream",
            headers: {
                'accept': 'application/json',
                'X-API-KEY': '{Your API Key}'
        .then(response => {
            // Specify the directory where you want to save the file.
            const directory = '{Specify the directory where you want to save the file}';
 // If the directory does not exist, create it.
            if (!fs.existsSync(directory)) {
            // Set the file path where you want to save the file.
            const filePath = path.join(directory, `${}.pdf`);
            // Create a writable stream to save the file.
            const writer = fs.createWriteStream(filePath);
            // Pipe the response stream to the file stream.
            // Listen for the 'finish' event to know when the file is fully written.
            writer.on('finish', () => {
                console.log(`File ${}.pdf downloaded and saved successfully.`);
            // Listen for errors.
            writer.on('error', (err) => {
                console.error(`Error downloading or saving file: ${err}`);
        .catch(error => {
            console.error(`Error downloading document: ${error}`);


app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);


from flask import Flask, request, jsonify
import hashlib
import requests
import os
import hmac
import time
import json

app = Flask(__name__)

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

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

class BoldSignHelper:

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

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

        if not signatures:
            raise BoldSignWebhookError("Unable to find any signature from the provided header value")

        computedSignature =
            msg=(str(timestamp) + "." + body).encode('utf-8'),

        matched = any(hmac.compare_digest(sign, computedSignature) for sign in signatures)

        if not matched:
            raise BoldSignWebhookError("HMAC SHA256 signatures don't match")

        age = time.time() - timestamp

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

        return True

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

        if not header:
            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 in ["s0", "s1"]:

        return ParsedHeader(timestamp, signatures)

@app.route('/webhook', methods=['POST'])
def webhook():
    payload ='utf-8')
    event_header = request.headers.get('X-BoldSign-Event')
    if event_header == "Verification":
        return jsonify(success=True), 200

    header = request.headers.get('X-BoldSign-Signature')

        BoldSignHelper.is_from_boldsign(header, payload, SECRET_KEY)
    except BoldSignWebhookError as e:
        return jsonify(error=str(e)), 400

    # Handle the event.
    if event_header == "Completed":
        payload_json = json.loads(payload)

        url = "" + payload_json["data"]["documentId"]
        # Change this to your desired directory.
        output_directory = "{Change this to your desired directory}" 
        # Make sure the output directory exists, create it if not.
        if not os.path.exists(output_directory):
        filename = os.path.join(output_directory, payload_json["data"]["documentId"] + ".pdf")
        payload = {}
        headers = {
            'accept': 'application/json',
            'X-API-KEY': '{Your API Key}'
        response = requests.request("GET", url, headers=headers, data=payload)
        if response.status_code == 200:
            with open(filename, 'wb') as f:
            print("File downloaded successfully to:", filename)
            print("Error downloading file:", response.text)
    return jsonify(success=True), 200
    if __name__ == '__main__':


namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
use Exception;

class BoldSignWebhookController extends Controller
    private const SECRET_KEY = '<<<Secret Key>>>';

    private function parseHeader($header) {
        if (!is_string($header)) {
            return null;

        $result = [
            'timestamp' => -1,
            'signatures' => [],

        $items = explode(",", $header);
        foreach ($items as $item) {
            $key = explode("=", trim($item));
            if ($key[0] === 't') {
                $result['timestamp'] = intval($key[1], 10);
            if (in_array($key[0], ['s0', 's1'])) {
                $result['signatures'][] = $key[1];

        return (object) $result;

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

        return hash_equals($a, $b);

    private function isFromBoldSign($signatureHeader, $payload, $secretKey) 
        $parsed = $this->parseHeader($signatureHeader);

        if (!$parsed) {
            throw new Exception("BoldSign signatures don't exist");

        $signatureMatched = false;

        foreach ($parsed->signatures as $signature) {
            $computedSignature = hash_hmac('sha256', $parsed->timestamp . "." . $payload, $secretKey);
            if ($this->secureCompare($computedSignature, $signature)) {
                $signatureMatched = true;

        if (!$signatureMatched) {
            throw new Exception("Unable to verify the signatures");

        $tolerance = 300;
        $timestampAge = time() - $parsed->timestamp;

        if ($tolerance > 0 && $timestampAge > $tolerance) {
            throw new Exception("Exceeded allowed tolerance range");

        return true;

    public function handleWebhook(Request $request) {

        $eventType = $request->header('x-boldsign-event');

        if ($eventType == "Verification") {
            return response()->json(['status' => 'verified'], 200);

        $signature = $request->header('x-boldsign-signature');
        $payload = $request->getContent();

        try {
            $isValid = $this->isFromBoldSign($signature, $payload, self::SECRET_KEY);
        } catch (Exception $e) {
            return response()->json(['error' => 'Bad Request'], 400);

        if (!$isValid) {
            return response()->json(['error' => 'Forbidden'], 403);

        // Handle the event.
        if ($eventType === "Completed") {
            $this->handleCompletedEvent(json_decode($payload, true));

        return response()->json(['status' => 'success'], 200);

    private function handleCompletedEvent($payloadObject) {
        $documentId = $payloadObject['data']['documentId'];
        $apiKey = '{Your API Key}';
        $directory = '{Specify the directory where you want to save the file}';

        $response = Http::withHeaders([
            'accept' => 'application/json',
            'X-API-KEY' => $apiKey,
        ])->get('', [
            'documentId' => $documentId,

        if ($response->successful()) {
            if (!is_dir($directory)) {
                mkdir($directory, 0755, true);

            $filePath = $directory . '/' . $documentId . '.pdf';
            file_put_contents($filePath, $response->body());

            Log::info("File {$documentId}.pdf downloaded and saved successfully.");
        } else {
            Log::error("Error downloading document: " . $response->body());
For more on listening to callbacks from webhooks on your local machine, refer to the Setting up your endpoint to listen for callbacks API documentation.


By integrating BoldSign webhooks with your application, you can automate the process of downloading signed documents, ensuring you have immediate access to completed eSignatures. This not only saves time but also reduces the risk of human error in handling important documents. Start leveraging webhooks today to enhance your document workflow automation with BoldSign.

If you are not yet a BoldSign customer, we welcome you to start a 30-day free trial now to see how much it has to offer. Please feel free to leave a comment below. Your thoughts are much appreciated.  If you have any questions or would like more information about our services, please schedule a demo or contact our support team via our support portal.
Picture of Gopinath Kannusamy

Gopinath Kannusamy

Gopinath is a passionate software developer with 2 years of experience at BoldSign. He is an avid writer and enjoys sharing his insights on technology and development. In his free time, he enjoys exploring new technologies and learning new things.

Share this blog

Picture of Gopinath Kannusamy

Gopinath Kannusamy

Gopinath is a passionate software developer with 2 years of experience at BoldSign. He is an avid writer and enjoys sharing his insights on technology and development. In his free time, he enjoys exploring new technologies and learning new things.

Subscribe RSS feed

Leave a Reply

Your email address will not be published. Required fields are marked *