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

Features

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?



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 = '<<>>';

    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;
                break;
            }
        }

        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) {
            Log::error($e->getMessage());
            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('https://api.boldsign.com/v1/document/download', [
            '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());
        }
    }
}
  
        


namespace BoldSignWebhookListener
{
    using System;
    using System.IO;
    using System.Threading.Tasks;
    using BoldSign.Api;
    using BoldSign.Model.Webhook;
    using Microsoft.AspNetCore.Mvc;
    [ApiController]
    [Route("webhook")]
    public class WebhookExampleController : ControllerBase
    {
        [HttpPost]
        public async Task 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 = "<<>>";

            try
            {
                WebhookUtility.ValidateSignature(
                    json,
                    this.Request.Headers[WebhookUtility.BoldSignSignatureHeader],
                    SECRET_KEY);
            }
            catch (BoldSignSignatureException ex)
            {
                Console.WriteLine(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)
        {
            try
            {
                var apiClient = new ApiClient("https://api.boldsign.com", "{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))
                    {
                        Directory.CreateDirectory(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.");
                }
                else
                {
                    Console.WriteLine($"Error downloading document {documentId}: Document stream is null.");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error downloading document {documentId}: {ex}");
            }
        }
    }
}
         


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 = "<<>>"

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 not signatures:
            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 = 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

    @staticmethod
    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"]:
                signatures.append(y)
        return ParsedHeader(timestamp, signatures)

@app.route('/webhook', methods=['POST'])
def webhook():
    event_type = request.headers.get('x-boldsign-event')
    if event_type == "Verification":
        return jsonify({'status': 'verified'}), 200

    payload = request.get_data(as_text=True)
    signature = request.headers.get('x-boldsign-signature')

    try:
        is_valid = BoldSignHelper.is_from_boldsign(signature, payload, SECRET_KEY)
    except BoldSignWebhookError as e:
        return jsonify({'error': str(e)}), 400

    if not is_valid:
        return jsonify({'error': 'Forbidden'}), 403

    payload_json = json.loads(payload)
    if event_type == "Completed":
        document_id = payload_json['data']['documentId']
        download_completed_document(document_id)

    return jsonify({'status': 'success'}), 200

def download_completed_document(document_id):
    api_key = "{Your API Key}"
    directory = "{Specify the directory where you want to save the file}"

    url = f"https://api.boldsign.com/v1/document/download?documentId={document_id}"
    headers = {
        "accept": "application/json",
        "X-API-KEY": api_key
    }

    response = requests.get(url, headers=headers)

    if response.status_code == 200:
        if not os.path.exists(directory):
            os.makedirs(directory)
        file_path = os.path.join(directory, f"{document_id}.pdf")
        with open(file_path, "wb") as file:
            file.write(response.content)
        print(f"File {document_id}.pdf downloaded and saved successfully.")
    else:
        print(f"Error downloading document: {response.text}")

if __name__ == '__main__':
    app.run(debug=True, port=5000)
         


const crypto = require('crypto');
const axios = require('axios');
const express = require('express');
const fs = require('fs');
const path = require('path');

const app = express();
app.use(express.json());

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

// Function to validate signature
function isFromBoldSign(signatureHeader, payload, secretKey, tolerance = 300) {
    const parsedHeader = parseHeader(signatureHeader);
    const timestamp = parsedHeader.timestamp;
    const signatures = parsedHeader.signatures;

    if (timestamp === -1) {
        throw new Error("Unable to parse timestamp");
    }

    if (!signatures.length) {
        throw new Error("Unable to find any signature from the provided header value");
    }

    const hmac = crypto.createHmac('sha256', secretKey);
    hmac.update(`${timestamp}.${payload}`);
    const computedSignature = hmac.digest('hex');

    const matched = signatures.some(sign => crypto.timingSafeEqual(Buffer.from(sign), Buffer.from(computedSignature)));

    if (!matched) {
        throw new Error("HMAC SHA256 signatures don't match");
    }

    const age = Math.floor(Date.now() / 1000) - timestamp;

    if (tolerance > 0 && age > tolerance) {
        throw new Error("Event is outside of the timestamp age tolerance");
    }

    return true;
}

// Function to parse header
function parseHeader(header) {
    const result = {
        timestamp: -1,
        signatures: []
    };

    if (!header) {
        throw new Error("Signature header value seems to be empty");
    }

    header.split(",").forEach(item => {
        const [key, value] = item.trim().split("=");
        if (key === "t") {
            result.timestamp = parseInt(value);
        } else if (key === "s0" || key === "s1") {
            result.signatures.push(value);
        }
    });

    return result;
}

app.post('/webhook', async (req, res) => {
    const eventType = req.headers['x-boldsign-event'];

    if (eventType === "Verification") {
        return res.status(200).json({ status: 'verified' });
    }

    const payload = JSON.stringify(req.body);
    const signature = req.headers['x-boldsign-signature'];

    try {
        isFromBoldSign(signature, payload, SECRET_KEY);
    } catch (error) {
        return res.status(400).json({ error: error.message });
    }

    if (eventType === "Completed") {
        const documentId = req.body.data.documentId;
        await downloadCompletedDocument(documentId);
    }

    res.status(200).json({ status: 'success' });
});

async function downloadCompletedDocument(documentId) {
    const apiKey = "{Your API Key}";
    const directory = "{Specify the directory where you want to save the file}";

    const url = `https://api.boldsign.com/v1/document/download?documentId=${documentId}`;
    const headers = {
        "accept": "application/json",
        "X-API-KEY": apiKey
    };

    try {
        const response = await axios.get(url, { headers, responseType: 'stream' });

        if (!fs.existsSync(directory)) {
            fs.mkdirSync(directory, { recursive: true });
        }

        const filePath = path.join(directory, `${documentId}.pdf`);
        const writer = fs.createWriteStream(filePath);

        response.data.pipe(writer);

        return new Promise((resolve, reject) => {
            writer.on('finish', resolve);
            writer.on('error', reject);
        });
    } catch (error) {
        console.error(`Error downloading document: ${error.message}`);
    }
}

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

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.

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. 

Conclusion

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 *