SpreadsheetWEB applications can generate complicated documents from any number of Microsoft Word templates. In this tutorial, let’s take a look at how you can embed electronic signatures into these types of documents using PandaDoc electronic signature REST API.

Here, we will be using a simple SpreadsheetWEB application through which users can fill out their details and sign a Non-Disclosure Agreement (NDA). The application consists of a set of inputs that collect the user data and the application looks like below.

Inserting PandaDoc Electronic Signature to Word Documents in SpreadsheetWeb Applications

Other details such as signer name or address are kept in the underlying workbook. This creates the opportunity of building dynamic fields using Excel formulas. For example, data can be sent to different VPs in the company based on the location entered in the form.

The data captured by the SpreadsheetWeb application’s user interface can be printed into the Word template with the use of double curly brackets {{data}}.

That this Word template also include PandaDoc electronic signature tags which identify the location of electronic signatures.

 

PandaDoc Electronic Signature REST API

Begin by obtaining a developer key from the PandaDoc developer site.

https://signup.pandadoc.com/

Then, insert the developer key information into the sample Custom Action C# code below.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Text;
using System.Threading;
using System.Web;
using Newtonsoft.Json;
using Pagos.Designer.Interfaces.External.CustomHooks;
using Pagos.Designer.Interfaces.External.DataTransfer;
using Pagos.Designer.Interfaces.External.Messaging;
using Pagos.SpreadsheetWeb.Web.Api.Objects.Calculation;


namespace PandaDoc
{
    #region TokenObject
    class TokenObject
    {
        public string access_token { get; set; }
        public int expires_in { get; set; }
        public string refresh_token { get; set; }
        public string scope { get; set; }
        public string token_type { get; set; }
        public string message { get; set; }

    }
    #endregion

    #region AccessTokenRequest
    class AccessTokenRequest
    {
        public string grant_type { get; set; }
        public string client_id { get; set; }
        public string client_secret { get; set; }
        public string code { get; set; }
        public string scope { get; set; }
        public string redirect_uri { get; set; }

    }
    #endregion

    #region Recipient
    class Recipient {
        public string email { get; set; }
        public string first_name { get; set; }
        public string last_name { get; set; }
        public string role { get; set; }
    }
    #endregion

    #region PrintDocBody
    class PrintDocBody {
        public List<Recipient> recipients { get; set; }
        public bool parse_form_fields { get; set; }
        public string name { get; set; }
    }
    #endregion

    #region PdfStatus
    class PdfStatus {
        public string id { get; set; }
        public string name  { get; set; }
        public string status { get; set; }
        public string date_created { get; set; }
        public string date_modified { get; set; }
        public string expiration_date { get; set; }
        public string uuid { get; set; }
    }
    #endregion

    public class Class1 : IAfterPrint
    {
        TokenObject currentToken = new TokenObject();
        const string TOKEN_FILE_PATH = "c:\\pandaAuthCode.json";
        const string CLIENT_ID = "YOUR CLIENT ID";
        const string CLIENT_SECRET = "YOUR CLIENT SECRET";

        public Class1()
        {
            ServicePointManager.SecurityProtocol =
                SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;

            ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback
            (
                delegate { return true; }
            );

        }

        #region GetDocumentStatus
        private PdfStatus GetDocumentStatus(string docId, string accessToken) {
            var request = (HttpWebRequest)WebRequest.Create($"https://api.pandadoc.com/public/v1/documents/{docId}");
            PdfStatus status = null;
            try
            {
                request.Method = "GET";
                request.Timeout = 180000;
                request.Headers["Authorization"] = $"Bearer {accessToken}";
                using (var response = (HttpWebResponse) request.GetResponse())
                using (var stream = response.GetResponseStream())
                using (var reader = new System.IO.StreamReader(stream, Encoding.UTF8))
                {
                    string responseText = reader.ReadToEnd();
                    status = JsonConvert.DeserializeObject<PdfStatus>(responseText);
                }
            }
            catch (Exception ex)
            {
            }
            return status;
        }
        #endregion

        #region SendDocument
        public string SendDocument(string docId, string accessToken) {
            var request = (HttpWebRequest)WebRequest.Create($"https://api.pandadoc.com/public/v1/documents/{docId}/send");
            request.Method = "POST";
            request.Timeout = 180000;
            request.Headers["Authorization"] = $"Bearer {accessToken}";
            request.ContentType = "application/json";
            
            using (var response = (HttpWebResponse)request.GetResponse())
            using (var stream = response.GetResponseStream())
            using (var reader = new System.IO.StreamReader(stream, Encoding.UTF8))
            {
                string responseText = reader.ReadToEnd();
            }
            return "";
        }
        #endregion


        #region PostFormData
        public T PostFormData<T>(string url, string controller, string action, Dictionary<string,string> data, string bearerToken = null)
        {
            var request = (HttpWebRequest)WebRequest.Create($"{url}/{controller}/{action}");
            request.Method = "POST";
            request.Timeout = 180000;

            request.Headers["cache-control"] = "no-cache";

            if (!string.IsNullOrEmpty(bearerToken))
            {
                request.Headers["Authorization"] = $"Bearer {bearerToken}";
            }
            StringBuilder sb = new StringBuilder();
            List<string> allKeys = data.Keys.ToList();

            for (int n=0;n<allKeys.Count;n++)
            {
                var key = allKeys[n];
                sb.Append(key + "=" + HttpUtility.UrlEncode(data[key]));

                if (n < (allKeys.Count - 1))
                    sb.Append("&");
            }

            var encoding = new UTF8Encoding();
            var content = encoding.GetBytes(sb.ToString());
            request.ContentLength = content.Length;
            request.ContentType = "application/x-www-form-urlencoded";
            using (var dataStream = request.GetRequestStream())
            {
                dataStream.Write(content, 0, content.Length);
            }
            string responseText;

            using (var response = (HttpWebResponse)request.GetResponse())
            using (var stream = response.GetResponseStream())
            using (var reader = new System.IO.StreamReader(stream, Encoding.UTF8))
            {
                responseText = reader.ReadToEnd();
            }
            return JsonConvert.DeserializeObject<T>(responseText);
            
        }
        #endregion

        #region Upload File
        private PdfStatus HttpUploadFile(string url, string file, string paramName, string contentType, NameValueCollection nvc, string bearerToken)
        {
            PdfStatus pdfStatus = null;
            string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
            byte[] boundarybytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n");

            HttpWebRequest wr = (HttpWebRequest)WebRequest.Create(url);
            wr.Headers["Authorization"] = $"Bearer {bearerToken}";
            wr.ContentType = "multipart/form-data; boundary=" + boundary;
            wr.Method = "POST";
            wr.KeepAlive = true;
            wr.Credentials = System.Net.CredentialCache.DefaultCredentials;

            Stream rs = wr.GetRequestStream();

            string formdataTemplate = "Content-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}";
            foreach (string key in nvc.Keys)
            {
                rs.Write(boundarybytes, 0, boundarybytes.Length);
                string formitem = string.Format(formdataTemplate, key, nvc[key]);
                byte[] formitembytes = System.Text.Encoding.UTF8.GetBytes(formitem);
                rs.Write(formitembytes, 0, formitembytes.Length);
            }
            rs.Write(boundarybytes, 0, boundarybytes.Length);

            string headerTemplate = "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: {2}\r\n\r\n";
            string header = string.Format(headerTemplate, paramName, file, contentType);
            byte[] headerbytes = System.Text.Encoding.UTF8.GetBytes(header);
            rs.Write(headerbytes, 0, headerbytes.Length);

            FileStream fileStream = new FileStream(file, FileMode.Open, FileAccess.Read);
            byte[] buffer = new byte[4096];
            int bytesRead = 0;
            while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
            {
                rs.Write(buffer, 0, bytesRead);
            }
            fileStream.Close();

            byte[] trailer = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "--\r\n");
            rs.Write(trailer, 0, trailer.Length);
            rs.Close();

            WebResponse wresp = null;
            try
            {
                wresp = wr.GetResponse();
                Stream stream2 = wresp.GetResponseStream();
                StreamReader reader2 = new StreamReader(stream2);
                string response = reader2.ReadToEnd();
                pdfStatus= JsonConvert.DeserializeObject<PdfStatus>(response);
            }
            catch (Exception ex)
            {
                if (wresp != null)
                {
                    wresp.Close();
                    wresp = null;
                }
            }
            finally
            {
                wr = null;
            }
            return pdfStatus;
        }
        #endregion

        #region RefreshAccessToken
        void RefreshAccessToken()
        {
            TokenObject to = null;
            try
            {
                to = JsonConvert.DeserializeObject<TokenObject>(File.ReadAllText(TOKEN_FILE_PATH));

            }
            catch (Exception ex)
            {
            }


            AccessTokenRequest req = new AccessTokenRequest();
            Dictionary<string, string> dValues = new Dictionary<string, string>();
            dValues.Add("grant_type", "refresh_token");
            dValues.Add("client_id", CLIENT_ID);
            dValues.Add("client_secret", CLIENT_SECRET);
            dValues.Add("scope", "read+write");
            dValues.Add("refresh_token", to.refresh_token);

            try
            {
                var result = PostFormData<TokenObject>("https://api.pandadoc.com", "oauth2", "access_token", dValues);
                File.WriteAllText(TOKEN_FILE_PATH, JsonConvert.SerializeObject(result));


            }
            catch (Exception ex)
            {
            }
        }
        #endregion

        #region Wait Until Draft
        void WaitUntilDraft(PdfStatus status, TokenObject to)
        {
            PdfStatus newStatus = GetDocumentStatus(status.id, to.access_token);

            if (newStatus != null)
            {
                if (newStatus.status != "document.draft")
                {
                    Thread.Sleep(1000);
                    WaitUntilDraft(status, to);
                }
                else
                {
                    SendDocument(status.id, to.access_token);
                }
            }
            else
            {
            }
        }
        #endregion

        public StandardResponse AfterPrint(CalculationRequest request, CalculationResponse response, PrintProcessOutput printProcessOutput)
        {
            TokenObject to = null;
            try
            {
                RefreshAccessToken();
                
                var s1FirstName = request.Inputs.FirstOrDefault(x => x.Ref == "iSignature1Name").Value[0][0].Value;
                var s1LastName = request.Inputs.FirstOrDefault(x => x.Ref == "iSignature1Title").Value[0][0].Value;
                var s1Email = request.Inputs.FirstOrDefault(x => x.Ref == "iSignature1Email").Value[0][0].Value;

                var s2FirstName = request.Inputs.FirstOrDefault(x => x.Ref == "iSignature2Name").Value[0][0].Value;
                var s2LastName = request.Inputs.FirstOrDefault(x => x.Ref == "iSignature2Title").Value[0][0].Value;
                var s2Email = request.Inputs.FirstOrDefault(x => x.Ref == "iSignature2Email").Value[0][0].Value;

                to = JsonConvert.DeserializeObject<TokenObject>(File.ReadAllText(TOKEN_FILE_PATH));
                string fileName = "c:\\pswTemp\\" + printProcessOutput.FileName;
                File.WriteAllBytes(fileName, printProcessOutput.FileContents);
                PrintDocBody body = new PrintDocBody();
                body.name = "PDF File Test";
                body.parse_form_fields = false;
                body.recipients = new List<Recipient>();
                body.recipients.Add(new Recipient()
                {
                    email = s1Email,
                    first_name = s1FirstName,
                    last_name = s1FirstName,
                    role = "user"
                });
                body.recipients.Add(new Recipient()
                {
                    email = s2Email,
                    first_name = s2FirstName,
                    last_name = s2FirstName,
                    role = "owner"
                });
                NameValueCollection nvc = new NameValueCollection();
                nvc.Add("data", JsonConvert.SerializeObject(body));
                PdfStatus status = HttpUploadFile("https://api.pandadoc.com/public/v1/documents", fileName, "file", "application/pdf", nvc, to.access_token);
                if (status!=null)
                {
                    File.Delete(fileName);
                    if (status.status != "document.draft")
                    {
                        WaitUntilDraft(status, to);
                    }
                    else
                    {
                        SendDocument(status.id, to.access_token);
                    }
                }
            }
            catch (Exception ex)
            {
            }

            return new StandardResponse()
            {
                Success = true
            };
        }


    }
}

Following PandaDoc Electronic Signature parameters are used in this application.

PrintDocBody.name

PrintDocBody.parse_form_fields

PrintDocBody.recipients.Recipient.email

PrintDocBody.recipients.Recipient.first_name

PrintDocBody.recipients.Recipient.last_name

PrintDocBody.recipients.Recipient.role

For more details about Custom Code class implementation or Custom Actions in designer applications, please see our other tutorials like Integrate SpreadsheetWeb with Sales Force.

Note: Integration of PandaDoc electronic signature is done using SpreadsheetWeb Custom Actions. Hence it requires either a server or private cloud license.

Note: This tutorial is created using SpreadsheetWeb version 6.5.