1

I'm fairly new to the DocuSign API, and have been messing with the samples with success - for single documents. My issue is that I need to send Multiple document sin a single package, and I do not want to use JSON formatting at this particular moment (maybe as I become more familiar with it).

I've read the post here, but it doesn't solve my issue.: Trying to create DocuSign envelope with multiple documents

I have modified the example found here (for C#) and sending single documents works fine. http://iodocs.docusign.com/APIWalkthrough/requestSignatureFromDocument

NOTE: The code below works for a SINGLE document, just as the example. However, I need to be able to send multiple documents, and have been banging my head for a more than a few hours now. My issue is that I think I've got the format down for the multiple documents (as described in the stackoverflow post above), however, I cannot seem to get it to work properly.

So, modifying the example for C#, I've come up with the following:

    public static void configureMultiPartFormDataRequest(HttpWebRequest request, string xmlBody, string docName, string contentType, string[] documents)
    {
        string boundaryName = string.Empty;
        string boundaryDocsName = string.Empty;
        boundaryName = "AAA";
        boundaryDocsName = "BBB";

        bool multipleDocs = documents.Length > 1;

        // start building the multipart request body
        string requestBodyStart = "\r\n\r\n--" + boundaryName + "\r\n" + "Content-Type: application/xml\r\n" +
                "Content-Disposition: form-data\r\n\r\n" +
                xmlBody;

        string requestBodyEnd = "\r\n--" + boundaryName + "--\r\n\r\n";
        string requestBodyDocsEnd = "\r\n--" + boundaryDocsName + "--";

        if (multipleDocs)
        {
            requestBodyStart = requestBodyStart + "\r\n--" + boundaryName + "\r\n" + "Content-Disposition: form-data\r\n" + "Content-Type: multipart/mixed; boundary=" + boundaryDocsName + "\r\n";
        }
        else
        {
            // overwrite the default content-type header and set a boundary marker
            request.ContentType = string.Format("multipart/form-data; boundary={0}", boundaryName);
            boundaryDocsName = boundaryName;
            requestBodyStart = requestBodyStart;
        }

        byte[] bodyStart = System.Text.Encoding.UTF8.GetBytes(requestBodyStart.ToString());
        byte[] bodyEnd = System.Text.Encoding.UTF8.GetBytes(requestBodyEnd.ToString());
        byte[] bodyDocsEnd = System.Text.Encoding.UTF8.GetBytes(requestBodyDocsEnd.ToString());

        Stream dataStream = request.GetRequestStream();
        dataStream.Write(bodyStart, 0, requestBodyStart.ToString().Length);

        // Do documents
        addRequestDocs(dataStream, contentType, documents, boundaryDocsName);

        if (multipleDocs)
        {
            // Write Document End
            dataStream.Write(bodyDocsEnd, 0, requestBodyDocsEnd.ToString().Length);
        }
        // Write Stream end
        dataStream.Write(bodyEnd, 0, requestBodyEnd.ToString().Length);

        string textDataStream = dataStream.ToString();

        dataStream.Close();
    }
    /////////////////////////////////////////////////////////////////////////////////////////////////////////
    public static void addRequestDocs(Stream dataStream, string contentType, string[] documents, string boundaryName)
    {
        int counter = 0;
        foreach (string x in documents)
        {
            counter = counter + 1;
            string docBodyStart = string.Empty;

            string myDocName = GetPhysicalFileName(x);

            string requestDocumentBodyStart = "\r\n--" + boundaryName + "\r\n" + "Content-Type: " + contentType + "\r\n" +
                "Content-Disposition: file; filename=\"" + x + "\"; documentId=" + counter + "\r\n\r\n";

            byte[] bodyStart = System.Text.Encoding.UTF8.GetBytes(requestDocumentBodyStart.ToString());

            dataStream.Write(bodyStart, 0, requestDocumentBodyStart.ToString().Length);

            byte[] docContents = File.ReadAllBytes(myDocName);

            // read contents of provided document(s) into the stream
            dataStream.Write(docContents, 0, docContents.Length);
        }
    }
    /////////////////////////////////////////////////////////////////////////////////////////////////////////
    public static string GetPhysicalFileName(string relativeFileName)
    {
        string virtualFileName = string.Format("{0}{1}", HttpRuntime.AppDomainAppVirtualPath, relativeFileName);
        string physicalFileName = HttpContext.Current.Request.MapPath(virtualFileName);

        return physicalFileName;
    }

Calling code looks similar to the sample, but I've added a second document, and a location for signature in each document.

    public string RequestEmailSignature()
    {
        //---------------------------------------------------------------------------------------------------
        // ENTER VALUES FOR THE FOLLOWING 6 VARIABLES:
        //---------------------------------------------------------------------------------------------------
        string username = "***";
        string password = "***";
        string integratorKey = "***";
        bool returnPrettyXML = false;

        string recipientName = "John Smith";                                                // recipient (signer) name
        string recipientEmail = "john.smith@place.com";                                             // recipient (signer) email
        string recipientName1 = "John Smith Jr.";                                               // recipient (signer) name
        string recipientEmail1 = "john.smith.jr@place.com";                                             // recipient (signer) email
        string documentName = "IRS_4506_T.pdf";                                             // copy document with same name and extension into project directory (i.e. "test.pdf")
        string documentName1 = "Doc2";                                              // copy document with same name and extension into project directory (i.e. "test.pdf")
        string contentType = "application/pdf";                                     // default content type is PDF
        //---------------------------------------------------------------------------------------------------

        // additional variable declarations
        //string response = string.Empty;
        //string retResult = string.Empty;

        string baseURL = string.Empty;          // - we will retrieve this through the Login API call
        string response = string.Empty;
        string retResult = string.Empty;

        // Endpoint for Login api call (in demo environment):
        string url = "https://demo.docusign.net/restapi/v2/login_information";

        try
        {
            //============================================================================
            //  STEP 1 - Login API Call (used to retrieve your baseUrl)
            //============================================================================

            // set request url, method, and headers.  No body needed for login api call
            HttpWebRequest request = initializeRequest(url, "GET", null, username, password, integratorKey);

            // read the http response
            response = getResponseBody(request);

            // parse baseUrl from response body
            baseURL = parseDataFromResponse(response, "baseUrl");

            if (!returnPrettyXML)
            {
                retResult = baseURL;
            }
            else
            {
                retResult = prettyPrintXml(response);
            }

            //--- display results
            //Console.WriteLine("\nAPI Call Result: \n\n" + prettyPrintXml(response));
        //}

        //try
        //{
            // Endpoint for Login api call (in demo environment):
            //string url = "https://demo.docusign.net/restapi/v2/login_information";

            //============================================================================
            //  STEP 2 - Send Signature Request from Template
            //============================================================================

            /*
                This is the only DocuSign API call that requires a "multipart/form-data" content type.  We will be 
                constructing a request body in the following format (each newline is a CRLF):

                --AAA
                Content-Type: application/xml
                Content-Disposition: form-data

                <XML BODY GOES HERE>
                --AAA
                Content-Type:application/pdf
                Content-Disposition: file; filename="document.pdf"; documentid=1 

                <DOCUMENT BYTES GO HERE>
                --AAA--
             */

            // append "/envelopes" to baseURL and use for signature request api call
            url = baseURL + "/envelopes";

            // construct an outgoing XML formatted request body (JSON also accepted)
            // .. following body adds one signer and places a signature tab 100 pixels to the right
            // and 100 pixels down from the top left corner of the document you supply
            string xmlBody =
                "<envelopeDefinition xmlns=\"http://www.docusign.com/restapi\">" +
                "<emailSubject>DocuSign API - Signature Request on Document</emailSubject>" +
                "<status>sent</status>" +   // "sent" to send immediately, "created" to save as draft in your account
                // add document(s)
                "<documents>" +

                // Doc 1
                "<document>" +
                "<documentId>1</documentId>" +
                "<name>" + documentName + "</name>" +
                "</document>" +

                // Doc 2
                "<document>" +
                "<documentId>2</documentId>" +
                "<name>" + documentName1 + "</name>" +
                "</document>" +

                "</documents>" +

                // add recipient(s)
                "<recipients>" +
                "<signers>" +
                // Signer 1
                "<signer>" +
                "<recipientId>1</recipientId>" +
                "<email>" + recipientEmail + "</email>" +
                "<name>" + recipientName + "</name>" +

                "<tabs>" +
                "<signHereTabs>" +

                // Doc 1
                "<signHere>" +
                "<xPosition>80</xPosition>" + // default unit is pixels
                "<yPosition>620</yPosition>" + // default unit is pixels
                "<documentId>1</documentId>" +
                "<pageNumber>1</pageNumber>" +
                "</signHere>" +

                // Doc 2
                "<signHere>" +
                "<xPosition>20</xPosition>" + // default unit is pixels
                "<yPosition>674</yPosition>" + // default unit is pixels
                "<documentId>2</documentId>" +
                "<pageNumber>4</pageNumber>" +
                "</signHere>" +

                "</signHereTabs>" +
                "</tabs>" +

                "</signer>" +
                "</signers>" +

                "</recipients>" +
                "</envelopeDefinition>";

            // set request url, method, headers.  Don't set the body yet, we'll set that separelty after
            // we read the document bytes and configure the rest of the multipart/form-data request
            request = initializeRequest(url, "POST", null, username, password, integratorKey);

            // DOC 1 and 2
            string[] docArray = new string[] { documentName, documentName1 };

            // DOC 1 only <-- WORKS!!!
            //string[] docArray = new string[] { documentName};

            configureMultiPartFormDataRequest(request, xmlBody, documentName, contentType, docArray);

            // read the http response
            response = getResponseBody(request);

            if (!returnPrettyXML)
            {
                retResult = response;
            }
            else
            {
                retResult = prettyPrintXml(response);
            }

            //--- display results
            //Console.WriteLine("\nAPI Call Result: \n\n" + prettyPrintXml(response));
        }
        catch (WebException e)
        {
            using (WebResponse callResponse = e.Response)
            {
                HttpWebResponse httpResponse = (HttpWebResponse)callResponse;
                //Console.WriteLine("Error code: {0}", httpResponse.StatusCode);
                using (Stream data = callResponse.GetResponseStream())
                {
                    string text = new StreamReader(data).ReadToEnd();
                    retResult = text;
                    // Console.WriteLine(prettyPrintXml(text));
                }
            }
        }

        return retResult;
    }

This results in the following result for 1 document:

Accept: application/xml
Content-Type: multipart/form-data; boundary=AAA
Host: demo.docusign.net
Content-Length: 92530
Expect: 100-continue



--AAA
Content-Type: application/xml
Content-Disposition: form-data

<envelopeDefinition xmlns="http://www.docusign.com/restapi"><emailSubject>DocuSign API - Signature Request on Document</emailSubject><status>sent</status><documents><document><documentId>1</documentId><name>IRS_4506_T.pdf</name></document></documents><recipients><signers><signer><recipientId>1</recipientId><email>troy.munford@sba.gov</email><name>Troy Munford</name><routingOrder>1</routingOrder><tabs><signHereTabs><signHere><xPosition>80</xPosition><yPosition>620</yPosition><documentId>1</documentId><pageNumber>1</pageNumber></signHere></signHereTabs></tabs></signer></signers></recipients></envelopeDefinition>
--AAA
Content-Type: application/pdf
Content-Disposition: file; filename="IRS_4506_T.pdf"; documentId=1

<PDF REMOVED>

--AAA--

This results in the following for 2 documents:

Accept: application/xml
Content-Type: application/xml
Host: demo.docusign.net
Content-Length: 694790
Expect: 100-continue



--AAA
Content-Type: application/xml
Content-Disposition: form-data

<envelopeDefinition xmlns="http://www.docusign.com/restapi"><emailSubject>DocuSign API - Signature Request on Document</emailSubject><status>sent</status><documents><document><documentId>1</documentId><name>IRS_4506_T.pdf</name></document><document><documentId>2</documentId><name>Loan Application Home (Form 5C).pdf</name></document></documents><recipients><signers><signer><recipientId>1</recipientId><email>troy.munford@sba.gov</email><name>Troy Munford</name><routingOrder>1</routingOrder><tabs><signHereTabs><signHere><xPosition>80</xPosition><yPosition>620</yPosition><documentId>1</documentId><pageNumber>1</pageNumber></signHere><signHere><xPosition>30</xPosition><yPosition>685</yPosition><documentId>2</documentId><pageNumber>4</pageNumber></signHere></signHereTabs></tabs></signer></signers></recipients></envelopeDefinition>

--AAA
Content-Disposition: form-data
Content-Type: multipart/mixed; boundary=BBB

--BBB
Content-Type: application/pdf
Content-Disposition: file; filename="IRS_4506_T.pdf"; documentId=1

<PDF REMOVED>

--BBB
Content-Type: application/pdf
Content-Disposition: file; filename="Loan Application Home (Form 5C).pdf"; documentId=2

<PDF REMOVED>

--BBB--
--AAA--

FYI: I've tried a multitude of things (including moving the --AAA and --BBB directly under the PDF, or header data), but nothing seems to allow a second document to be added.

  • Please post the raw http request you are sending out (go ahead and remove the actual document bytes). See the raw request format and your outgoing XML body should tell us what you're doing wrong. If you're not sure how to capture the raw request take a look at free tools like [Fiddler](http://www.telerik.com/fiddler) for instance – Ergin Dec 25 '14 at 05:01
  • Add Raw HTTP request as suggested. I don't think it's the spacing of the Boundaries - I've tried a multitude of things (including making the spacing look EXACTLY like the post that suggests adding multiple docs). There is one thing that may be in the "https://stackoverflow.com/questions/20318532/trying-to-create-docusign-envelope-with-multiple-documents" though that is either a typo, or requires me to escape a character: filename=\”document1.pdf" Could that be it? – Troy Munford Dec 30 '14 at 19:53

1 Answers1

2

I found the answer! Turns out that the format should look like this:

Accept: application/xml
Content-Type: multipart/form-data; boundary=AAA
Host: demo.docusign.net
Content-Length: 92530
Expect: 100-continue



--AAA
Content-Type: application/xml
Content-Disposition: form-data

<envelopeDefinition xmlns="http://www.docusign.com/restapi"><emailSubject>DocuSign API - Signature Request on Document</emailSubject><status>sent</status><documents><document><documentId>1</documentId><name>IRS_4506_T.pdf</name></document></documents><recipients><signers><signer><recipientId>1</recipientId><email>troy.munford@sba.gov</email><name>Troy Munford</name><routingOrder>1</routingOrder><tabs><signHereTabs><signHere><xPosition>80</xPosition><yPosition>620</yPosition><documentId>1</documentId><pageNumber>1</pageNumber></signHere></signHereTabs></tabs></signer></signers></recipients></envelopeDefinition>
--AAA
Content-Type: application/pdf
Content-Disposition: file; filename="IRS_4506_T.pdf"; documentId=1

<PDF REMOVED>

--AAA
Content-Type: application/pdf
Content-Disposition: file; filename="Loan Application Home (Form 5C).pdf"; documentId=2

<PDF REMOVED>

--AAA--

The bits of code to get this done are:

    public static void configureMultiPartFormDataRequest(HttpWebRequest request, string xmlBody, string contentType, DocumentList documents)
    {
        string boundaryName = string.Empty;
        boundaryName = "00000000-0000-0000-0000-000000000000";

        request.ContentType = string.Format("multipart/form-data; boundary={0}", boundaryName); 

        bool multipleDocs = documents.Count > 1;

        // start building the multipart request body
        string requestBodyStart = "\r\n\r\n--" + boundaryName + "\r\n" + "Content-Type: application/xml\r\n" +
                "Content-Disposition: form-data\r\n\r\n" +
                xmlBody;

        string requestBodyEnd = "\r\n--" + boundaryName + "--\r\n\r\n";

        byte[] bodyStart = System.Text.Encoding.UTF8.GetBytes(requestBodyStart.ToString());
        byte[] bodyEnd = System.Text.Encoding.UTF8.GetBytes(requestBodyEnd.ToString());

        Stream dataStream = request.GetRequestStream();
        dataStream.Write(bodyStart, 0, requestBodyStart.ToString().Length);

        // Do documents
        addRequestDocs(dataStream, contentType, documents, boundaryName);

        // Write Stream end
        dataStream.Write(bodyEnd, 0, requestBodyEnd.ToString().Length);

        dataStream.Close();
    }

    public static void addRequestDocs(Stream dataStream, string contentType, DocumentList documents, string boundaryName)
    {
        int counter = 0;
        foreach (Document x in documents)
        {
            counter = counter + 1;
            string docBodyStart = string.Empty;

            string myDocName = GetPhysicalFileName(x.FileName);

            string requestDocumentBodyStart = "\r\n--" + boundaryName + "\r\n" + "Content-Type: " + contentType + "\r\n" +
                "Content-Disposition: file; filename=\"" + x.FileName + "\"; documentId=" + counter + "\r\n\r\n";

            byte[] bodyStart = System.Text.Encoding.UTF8.GetBytes(requestDocumentBodyStart.ToString());

            dataStream.Write(bodyStart, 0, requestDocumentBodyStart.ToString().Length);

            byte[] docContents = File.ReadAllBytes(myDocName);

            // read contents of provided document(s) into the stream
            dataStream.Write(docContents, 0, docContents.Length);
        }
    }

    public static string GetPhysicalFileName(string relativeFileName)
    {
        string virtualFileName = string.Format("{0}\\{1}", HttpRuntime.AppDomainAppVirtualPath, relativeFileName);
        string physicalFileName = HttpContext.Current.Request.MapPath(virtualFileName);

        return physicalFileName;
    }

Plus the Document and DocumentList classes:

    public class Document
    {
        #region Properties
        public int ID { get; set; }
        public string Name { get; set; }
        public string FileName { get; set; }
        #endregion Properties
    }


    public class DocumentList : List<Document>
    {
    }

Hope this helps someone else!