Let's Go! Making DocuSign API calls with Go

Go for the Go language! Right? Go (sometimes called golang) is more popular than ever before. If you're looking for a lightweight C alternative, why not give this language a try? Go has been designed to be quick and easy to use, yet powerful and able to handle dynamic structure types. Go has a rich library of tools and features built in to make most application projects easy. Let's face it: All the cool kids are using it these days, for better or worse. In fact, all it took for me to get started was to install the compiler provided. With that task completed, I restarted my computer. (As a best practice you should do this when adding or removing programs that will modify the system PATH or aliases.)  

Configure OAuth and JWT

To get started, generate access tokens using a technology known as JSON Web Tokens. JWT is a type of OAuth grant flow that enables your customers to log in and authenticate their DocuSign account automatically without a password (on the first time logging in, each user is required to grant consent). I’m using JWT here as a matter of convenience; Authorization Code Grant or implicit grant can also be utilized per your preference to initiate OAuth. With Go installed on your machine, create a file named app.go and another file named go.mod. Think of this go.mod as an instructions file, or a manifest if you're hailing from the world of Node.js. Within your app.go file, using your favorite code editor, import the following JWT module like this:

import ( "github.com/Go-jwt/jwt" )

To get started, you will also need to import the standard built-in modules time (easy manipulation of time and date operations), fmt (format), and os (similar to os from the world of Python, this lets you handle common operating system tasks using a cross-platform go module). Your entire import statement should appear similar to this:

import (
   
    "github.com/Go-jwt/jwt"
    "fmt"
    "os"
    "time"
)

Generate a DocuSign integration

Before getting into the details of the API call code itself, generate your DocuSign integration credentials. Navigate to the Developer Center, log in to your account, then visit the Apps and Keys page. From there, you'll create a new integration. Choose the integration type of "service integration" to generate (or import) an RSA private key. Copy this private key to a safe location on your local development environment, as you'll need this to generate your JSON web tokens. Copy the integration key that has now been generated for this new Go app in your developer account, as well as your account ID GUID. 

TL;DR: By the time you're done with tinkering on settings in the Developer Center, you should have an Integration Key GUID, your UserId GUID, and a long private key string (in .pem format).

Create a JWT assertion

To solve the problems regarding access tokens for DocuSign and no Go SDK support, I had to look up both the reference data for the jwt-go software package as found in the Go documentation as well as look through the DocuSign JWT Grant Authentication how-to in the Developer Center. I was able to walk through the HMAC listed example on the Go docs and was able to figure out the correlated RS256 method that DocuSign supports. Just as I've done before with NodeJS and Python, I load the RSA key into a byte buffer using the os module and load it into a compatible structure by using the ParseRSAPrivateKeyFromPEM method. Then, I create a new variable, rawJWT , and instantiate it to be the output from the jwt.NewWithClaims method. In that method, I've specified to use RS256 (RSA) and to set the claims parameter (POST variables) as per the DocuSign API specification. Finally, I retrieve a generated JSON web token by applying the RSA private key by using rawJWT.SignedString(RSAPrivateKey):

func makeDSToken() (string, error) {
 
    // Create a new JWT claim. Set the integration key, impersonated user GUID, time of issue, expiry time, account server, and required scopes
    rawJWT := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
        "iss":   "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "sub":   "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "iat":   time.Now().Unix(),
        "exp":   time.Now().Unix() + 3600,
        "aud":   "account-d.docusign.com",
        "scope": "signature impersonation",
    })
 
    RSAPrivateKey, err := os.ReadFile("./private.key")
    if err != nil {
        fmt.Printf("Error opening file: %s", err)
         return "", err
    }
 
    // load the private.key file into JWT library
    rsaPrivate, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(RSAPrivateKey))
    if err != nil {
        fmt.Printf("key update error for: %s", err)
        return "", err
    }
 
    // Generate the signed JSON Web Token assertion with an RSA private key
    tokenString, err := rawJWT.SignedString(rsaPrivate)
    //fmt.Println(tokenString, err)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
        return "", err
    }

Note: This JWT is not your access token. You will still need to submit the JWT to the DocuSign account server in order to return an access token that you'll use to retrieve an API account ID and to make subsequent requests, such as sending envelopes with documents to be signed.

An aside

My original plan was to skip dealing with struct(ure) types and manually parse the response outputs for something repeatable, such as "access_token" or "account_id", strings that I could locate within the JSON response bodies, because I'm lazy. However, after much thought, I changed my mind on the matter and structs are back on the menu! 

Briefly: Structs (inherited from C) are a programming language feature that allows you to define custom “structures” or types for your program. I was feeling hesitant at first about including structs because this is a, perhaps, 300-line demo (with extensive commenting, mind you!), and adding sample struct definitions for the entire JSON response when I was just trying to pull out a single value felt like overkill.  It seemed easier instead to find the values I needed based on substrings and indexes and to then slice out the given value (at least for fixed length constants such as the account ID). I felt this way up until I had to work with retrieving the Account ID from the raw JSON response. Sure, I could just look for the string [{"account_id": and pull the next 36 characters starting at index of 1, but that's both brittle and can be a painstaking effort on its own. What if you have special characters or non-latin characters to deal with? What if you need a different API account ID, besides the first (default) one?

It turns out, someone made an open-source tool, JSON to Go Struct, that will transform your JSON string into a Go struct automatically! Have you tried writing real-world Go structs? They take Regex levels of focus and I would describe them as akin to reading a Swagger definition spec, written in Braille. This tool takes that pain away!

Specify your (data) structs

For Go to parse the JSON responses, you'll need to specify data structures so that you can unmarshal (deserialize) the corresponding data to its given key-value pair. I did end up writing the AccessToken struct by hand because it For the AccountId struct, I used the JSON to Go Struct tool with a sample known good JSON output for the DocuSign OAuth/UserInfo call, which you need to get user account information such as your user’s API account ID and base URI. In case you're wondering how the heck to retrieve a sample known good JSON output from the OAuth/UserInfo call, here is one that you can conveniently find in our JWT implementation guide:was a simple, straightforward structure of three strings:

type AccessToken struct {
    Token  string `json:"access_token"`
    Type   string `json:"token_type"`
    Expiry int    `json:"expires_in"`
}

For the AccountId struct, I used the JSON to Go Struct tool with a sample known good JSON output for the DocuSign OAuth/UserInfo call, which you need to get user account information such as your user’s account ID API and base path URI. In case you're wondering how the heck to retrieve a sample known good JSON output from the OAuth/UserInfo call, here is one that you can conveniently find in our JWT implementation guide:

{
  "sub": "564f7988-xxxx-xxxx-xxxx-781ee556ab7a",
  "name": "Example J Smith",
  "given_name": "Example",
  "family_name": "Smith",
  "created": "2018-04-13T22:03:03.45",
  "email": "Example.Smith@exampledomain.com",
  "accounts": [
    {
      "account_id": "18b4799a-xxxx-xxxx-xxxx-b5b4b8a97604",
      "is_default": true,
      "account_name": "ExampleAccount",
      "base_uri": "https://demo.docusign.net"
    }
  ]
}

This response does change based on your account; for instance, my own developer account that I used for this demo had DocuSign admin organizations associated with it. Thus, when I ran my own UserInfo call and processed the response through the JSON to Go Struct tool, it created this sort of AccountId struct instead:

// Auto-generated using https://transform.tools/json-to-go
type AccountId struct {
    Sub        string `json:"sub"`
    Name       string `json:"name"`
    GivenName  string `json:"given_name"`
    FamilyName string `json:"family_name"`
    Created    string `json:"created"`
    Email      string `json:"email"`
    Accounts   []struct {
        AccountID    string `json:"account_id"`
        IsDefault    bool   `json:"is_default"`
        AccountName  string `json:"account_name"`
        BaseURI      string `json:"base_uri"`
        Organization struct {
            OrganizationID string `json:"organization_id"`
            Links          []struct {
                Rel  string `json:"rel"`
                Href string `json:"href"`
            } `json:"links"`
        } `json:"organization"`
    } `json:"accounts"`
}

My word of advice is to keep all your structs in the same section of the code so they're easy to review. I've kept mine at the top of my Go script, just below the import declaration, and even took a moment to translate the final output EnvelopeID struct as well, just to clean things up slightly:

type EnvelopeID struct {
    EnvelopeID     string    `json:"envelopeId"`
    URI            string    `json:"uri"`
    StatusDateTime time.Time `json:"statusDateTime"`
    Status         string    `json:"status"`
}

Grant first-time user consent

The first time you or your users attempt JWT, they'll need to manually log in and grant consent. If you don't, you'll trigger the following error:

Code: 400
Body: {"error":"consent_required"}

To grant consent, I'll use the example URL syntax from the Docusign Developer article linked above:

https://account-d.docusign.com/oauth/auth?response_type=code&
scope=signature%20impersonation&client_id=7c2b8d7e-xxxx-xxxx-xxxx-cda8a50dd73f
&redirect_uri=http://example.com/callback/

Fill in this link with your own integration key for the client_id query parameter above and log in. You'll see a permission prompt appear that you'll need to accept in order to proceed:

DocuSign authorization permission form

Get an access token

Now that you have prepared your signed JSON Web Token and have granted individual consent, send it to the API to generate an access token:

    // Submit the JWT to the account server and request an access token
    resp, err := http.PostForm("https://account-d.docusign.com/oauth/token",
        url.Values{
            "grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
            "assertion":  {tokenString},
        })
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
        return "", err
 
    }
 
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
        return "", err
    }
    // fmt.Printf("Body: %s\n", body)
 
    // Done with the request, close it; see https://stackoverflow.com/q/18598780/2226328
    resp.Body.Close()
 
    // decode the response to JSON
    var token AccessToken
    jsonErr := json.Unmarshal(body, &token)
    if jsonErr != nil {
        fmt.Printf("There was an error decoding the JSON. err = %s", jsonErr)
        return "", jsonErr
    }
 
 
    // fmt.Println(token.Token)
    return token.Token, nil
}

Send the JWT assertion and grant type as a post request using http.PostForm as shown above. From there, use io.ReadAll to parse your response body, which is a buffer or byte array by default. Next, create a new variable to store your access token JSON data using the AccessToken type struct defined earlier. Then, use the json.Unmarshal method on the body output from the io.ReadAll operation and store it to the token pointer

Retrieve your API account ID

Now that you have an access token in the stack, you'll need to run the UserInfo API call to retrieve your API account ID. Practically speaking, all API calls need an API account ID to work for a given account. Just as before, create a new function that accepts your access token with a return type of string. With your access token in hand, you can send an HTTP GET request with a custom header, with a key "Authorization" and a value of "Bearer {YOURLONGACCESSTOKEN}":

// Internal API call to pull in the API Account ID GUID used to make all subsequent API calls
func getAPIAccId(DSAccessToken string) string {
    client := &http.Client{}
    // Use http.NewRequest to set custom headers
    req, err := http.NewRequest("GET", "https://account-d.docusign.com/oauth/userinfo", nil)
    req.Header.Set("Authorization", "Bearer " +DSAccessToken)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
     	  return "", err
    }
 
    // Since http.NewRequest is being used, client.Do is needed to execute the request
    res, err := client.Do(req)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
        return "", err
    }

In this request, since you're modifying the headers, you'll need to execute this HTTP request a bit differently from above. Note that it uses http.NewRequest instead of http.PostForm. You also need to initiate a new &http.Client{} and send the request using client.Do(req). Just as before, use the io.ReadAll method on the response to parse the response body and store that into a new accountId variable that you’ve created based on the JSON response output and the JSON to Go Struct tool:

    body, err := io.ReadAll(res.Body)
    if err != nil {
        fmt.Printf("Request failed: %s", err)
	  return "", err
    }
    // fmt.Printf("Body: %s\n", body)
    res.Body.Close()
 
    // decode the response to JSON
    var accountId AccountId
    jsonErr := json.Unmarshal(body, &accountId)
    if jsonErr != nil {
        fmt.Printf("There was an error decoding the JSON. err = %s", jsonErr)
        return "", jsonErr
    }    
 
    // fmt.Println(accountId.Accounts[0].AccountID)
    return accountId.Accounts[0].AccountID
 
}

Create and send an envelope

If I didn't know what to do, and/or I was making a fully fleshed-out Go application, I'd probably go the route of creating structs and marshaling the relevant JSON structures as necessary into my HTTP requests. However, this is the land of demos and happy paths. For our cursory research purposes I've borrowed the lovely envelope definition snippet from example 29 in our code-examples-bash repo. We use this to quickly populate envelope information. To make it a bit less brittle, you could encode the various envelope definition objects as structs, like I did in this small playground snippet here, but it can become a beast of its own undertaking if you must go that route by hand. When I began writing this post, for brevity, I changed around the bash launcher's envelope definition to yield this snippet:

// Make the envelope definition
func makeEnvelope(ccName string, ccEmail string, signerEmail string, signerName string) string {
 
    envelope := fmt.Sprintf(`{
    "emailSubject": "Please sign this document set",
    "documents": [{
        "documentBase64": "DQoNCg0KCQkJCXRleHQgZG9jDQoNCg0KDQoNCg0KUk0gIwlSTSAjCVJNICMNCg0KDQoNClxzMVwNCg0KLy9hbmNoMSANCgkvL2FuY2gyDQoJCS8vYW5jaDM=",
        "documentId": "1",
        "fileExtension": "txt",
        "name": "NDA"
    }],
    "recipients": {
        "carbonCopies": [
            {
                "email": "%s",
                "name": "%s",
                "recipientId": "2",
                "routingOrder": "2"
            }
        ],
        "signers": [
            {
                "email": "%s",
                "name": "%s",
                "recipientId": "1",
                "routingOrder": "1",
                "tabs": {
                    "signHereTabs": [{
                        "documentId": "1",
                        "name": "SignHereTab",
                        "pageNumber": "1",
                        "recipientId": "1",
                        "tabLabel": "SignHereTab",
                        "xPosition": "75",
                        "yPosition": "572"
                    }]
                },
            }
        ]
    },
    "status": "sent"
}`, ccName, ccEmail, signerEmail, signerName)
 
    return envelope
}

This envelope definition should include an email subject line, an envelope status, the document itself and some SignHere tabs data. There isn't too much to unpack here: I’m setting an envelope tab position and using a small-sized Base64 string to hold the document contents. Envelope definitions also require recipients that themselves usually require SignHere fields, initial fields and formula fields (which serve to hold calculated values). Those recipient and tab objects are stored in an array list.

To send the envelope via the API, use the http.NewRequest() method just as before. Since you're specifying custom headers, however, be sure to change the method at the beginning to "POST" and, instead of nil for the data, add in your envelope definition. Since that envelope definition is a raw string that was interpolated from substitute values, convert it to a string buffer by using strings.NewReader(envelopeDefinition) and send the request on its way! Again, just as before, capture the response output into io.ReadAll(rest.Body) as a string buffer and map that to the EnvelopeID struct with another pass of json.Unmarshal:

// Send an envelope
func sendEnvelope(DSAccessToken string, DSAccountId string, envelopeDefinition string) string {
    client := &http.Client{}
    // Use http.NewRequest in order to set custom headers
    req, err := http.NewRequest("POST", "https://demo.docusign.net/restapi/v2.1/accounts/"+DSAccountId+"/envelopes", strings.NewReader(envelopeDefinition))
    req.Header.Set("Authorization", "Bearer " +DSAccessToken)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
    }
    // Since http.NewRequest is being used, client.Do is needed to execute the request
    res, err := client.Do(req)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
    }
 
    body, err := io.ReadAll(res.Body)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
    }
    // fmt.Printf("Body: %s\n", body)
    // Decode the response to JSON
    var envelope EnvelopeID
    json.Unmarshal(body, &envelope)
 
    return envelope.EnvelopeID
}

Put everything together

 

So far I've provided a few snippets of functions. Let's take a moment to go over how they come together. Go files have a package declaration at the beginning of the file. I've found that the compiler complains when you take that away. I don't have a very concise answer, but I've come to accept that Go packages are the equivalent to what namespaces are in PHP, Java, C#, and other languages. Essentially, you'll need to include a package name at the top and that serves to tell Go where your script is in conjunction with the GoRoot directory. This package name also needs to have a matching named constructor function that will call the rest of the application logic. For my code, I set up the main function to generate the access token, generate the API account ID, generate an envelope definition with string interpolation, and to finally send the envelope and print the resulting envelope ID to screen:

func main() {
    dSAccessToken, err := makeDSToken()
    if err != nil {
        fmt.Printf("Failed to retrieve token: %s", err)
    }
    dSAccountId, err := getAPIAccId(dSAccessToken)
    if err != nil {
        fmt.Printf("Failed to API Account ID token: %s", err)
    }
    envelopeDefinition := makeEnvelope("sonic the hedgefrog", "sonic@example.com", "sally@example.com", "sally signer")
    envelopeId, err := sendEnvelope(dSAccessToken, dSAccountId, envelopeDefinition)
    if err != nil {
        fmt.Printf("Failed to retrieve token: %s", err)
    }
    fmt.Println(envelopeId)
}

Another point worth mentioning is the error handling mechanisms of Go. It uses strict error handling, which means no try/catch blocks: instead, you can defer and recover. In my case, I set the various functions to return an error and a string. My setup halted execution when things went wrong so well that I found a hidden bug in my original handwritten struct involving the access token expiry: I claimed it was a string, but it's actually an integer. This error was determined once I set json.Unmarshal behind error handling. I was silently ignoring the JSON structure before and still passing the access token along, masking potential errors.

That about covers it, so, hats off for getting this far in the post! Feel free to take a break; you can let your boss know that I, Aaron JacksonWilde, said it was okay to do so (unless you scrolled hastily through the post above;, then no celebration for you!) Alright, so with the various snippets of code above copied and pasted into your own app.go file, open the command line and get to that shiny new Go script by cd'ing your way towards it (or press Ctrl + ` if you're in the land of Visual Studio code). Run go mod tidy to install the jwt dependency from GitHub. Then try the script out for yourself by using go run app.go. The script will return an envelope ID and send it to your recipient and carbon copy as specified. As mentioned, I didn't quite go as far as to convert the entire envelope definition to a struct. Had I encountered situations involving composite templates or having to deal with dynamic document generation, I would suggest determining the raw JSON structure for those assorted envelope definitions and using a tool to help assist in converting them into their subsequent type definitions. For ease of reference, here is the entire completed script:

package main
 
import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strings"
    "time"
 
    "github.com/golang-jwt/jwt"
)
 
type AccessToken struct {
    Token  string `json:"access_token"`
    Type   string `json:"token_type"`
    Expiry int    `json:"expires_in"`
}
 
// Auto-generated using https://transform.tools/json-to-go
type AccountId struct {
    Sub        string `json:"sub"`
    Name       string `json:"name"`
    GivenName  string `json:"given_name"`
    FamilyName string `json:"family_name"`
    Created    string `json:"created"`
    Email      string `json:"email"`
    Accounts   []struct {
        AccountID    string `json:"account_id"`
        IsDefault    bool   `json:"is_default"`
        AccountName  string `json:"account_name"`
        BaseURI      string `json:"base_uri"`
        Organization struct {
            OrganizationID string `json:"organization_id"`
            Links          []struct {
                Rel  string `json:"rel"`
                Href string `json:"href"`
            } `json:"links"`
        } `json:"organization"`
    } `json:"accounts"`
}
 
// Auto-generated using https://transform.tools/json-to-go
type EnvelopeID struct {
    EnvelopeID     string    `json:"envelopeId"`
    URI            string    `json:"uri"`
    StatusDateTime time.Time `json:"statusDateTime"`
    Status         string    `json:"status"`
}
 
var DSAccessToken string
var DSAccountId string
var EnvelopeId string
var EnvelopeDefinition string
 
// For RSA signing method, the key can be any []byte. It is recommended to generate
// a key using crypto/rand or something equivalent. You need the same key for signing
// and validating.
var RSAPrivateKey []byte
 
func makeDSToken() (string, error) {
 
    // Create a new JWT claim. Set your integration key, impersonated user GUID, time of issue, expiry time, account server, and required scopes
    rawJWT := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
        "iss":   "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "sub":   "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "iat":   time.Now().Unix(),
        "exp":   time.Now().Unix() + 3600,
        "aud":   "account-d.docusign.com",
        "scope": "signature impersonation",
    })
 
    RSAPrivateKey, err := os.ReadFile("./private.key")
    if err != nil {
        fmt.Printf("Error opening file: %s", err)
        return "", err
    }
 
    // Load the private.key file into JWT library
    rsaPrivate, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(RSAPrivateKey))
    if err != nil {
        fmt.Printf("key update error for: %s", err)
        return "", err
    }
 
    // Generate the signed JSON Web Token assertion with an RSA private key
    tokenString, err := rawJWT.SignedString(rsaPrivate)
    //fmt.Println(tokenString, err)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
        return "", err
    }
 
    // Submit the JWT to the account server and request and access token
    resp, err := http.PostForm("https://account-d.docusign.com/oauth/token",
        url.Values{
            "grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
            "assertion":  {tokenString},
        })
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
        return "", err
    }
 
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
        return "", err
    }
    //fmt.Printf("Body: %s\n", body)
 
    // Done with the request, close it: https://stackoverflow.com/q/18598780/2226328
    resp.Body.Close()
 
    // Decode the response to JSON
    var token AccessToken
    jsonErr := json.Unmarshal(body, &token)
    if jsonErr != nil {
        fmt.Printf("There was an error decoding the json. err = %s", jsonErr)
        return "", jsonErr
    }
    // fmt.Println(token.Token)
    return token.Token, nil
}
 
// Internal API call to pull in the API account ID GUID used to make all subsequent API calls
func getAPIAccId(DSAccessToken string) (string, error) {
    client := &http.Client{}
    // Use http.NewRequest in order to set custom headers
    req, err := http.NewRequest("GET", "https://account-d.docusign.com/oauth/userinfo", nil)
    req.Header.Set("Authorization", "Bearer "+DSAccessToken)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
        return "", err
    }
 
    // Since http.NewRequest is being used, client.Do is needed to execute the request
    res, err := client.Do(req)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
        return "", err
 
    }
 
    body, err := io.ReadAll(res.Body)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
        return "", err
    }
    // fmt.Printf("Body: %s\n", body)
    res.Body.Close()
 
    // Decode the response to JSON
    var accountId AccountId
    jsonErr := json.Unmarshal(body, &accountId)
    if jsonErr != nil {
        fmt.Printf("There was an error decoding the json. err = %s", jsonErr)
        return "", jsonErr
    }
 
    // fmt.Println(accountId.Accounts[0].AccountID)
    return accountId.Accounts[0].AccountID, nil
 
}
 
// Make the envelope definition
func makeEnvelope(ccName string, ccEmail string, signerEmail string, signerName string) string {
 
    // You may perhaps like this style instead?
    // https://go.dev/play/p/Ik8-Mh3BBH4
 
    envelope := fmt.Sprintf(`{
    "emailSubject": "Please sign this document set",
    "documents": [{
        "documentBase64": "DQoNCg0KCQkJCXRleHQgZG9jDQoNCg0KDQoNCg0KUk0gIwlSTSAjCVJNICMNCg0KDQoNClxzMVwNCg0KLy9hbmNoMSANCgkvL2FuY2gyDQoJCS8vYW5jaDM=",
        "documentId": "1",
        "fileExtension": "txt",
        "name": "NDA"
    }],
    "recipients": {
        "carbonCopies": [
            {
                "email": "%s",
                "name": "%s",
                "recipientId": "2",
                "routingOrder": "2"
            }
        ],
        "signers": [
            {
                "email": "%s",
                "name": "%s",
                "recipientId": "1",
                "routingOrder": "1",
                "tabs": {
                    "signHereTabs": [{
                        "documentId": "1",
                        "name": "SignHereTab",
                        "pageNumber": "1",
                        "recipientId": "1",
                        "tabLabel": "SignHereTab",
                        "xPosition": "75",
                        "yPosition": "572"
                    }]
                },
            }
        ]
    },
    "status": "sent"
}`, ccEmail, ccName, signerEmail, signerName)
 
    return envelope
}
 
// Send an envelope
func sendEnvelope(DSAccessToken string, DSAccountId string, envelopeDefinition string) (string, error) {
    client := &http.Client{}
    // Use http.NewRequest in order to set custom headers
    req, err := http.NewRequest("POST", "https://demo.docusign.net/restapi/v2.1/accounts/"+DSAccountId+"/envelopes", strings.NewReader(envelopeDefinition))
    req.Header.Set("Authorization", "Bearer "+DSAccessToken)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
        return "", err
    }
    // Since http.NewRequest is being used, client.Do is needed to execute the request
    res, err := client.Do(req)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
        return "", err
    }
 
    body, err := io.ReadAll(res.Body)
    if err != nil {
        fmt.Printf("Request Failed: %s", err)
        return "", err
    }
    // fmt.Printf("Body: %s\n", body)
    // Decode the response to JSON
    var envelope EnvelopeID
    jsonErr := json.Unmarshal(body, &envelope)
    if jsonErr != nil {
        fmt.Printf("Request Failed: %s", jsonErr)
        return "", jsonErr
    }
    return envelope.EnvelopeID, nil
}
 
func main() {
    dSAccessToken, err := makeDSToken()
    if err != nil {
        fmt.Printf("Failed to retrieve token: %s", err)
    }
    dSAccountId, err := getAPIAccId(dSAccessToken)
    if err != nil {
        fmt.Printf("Failed to API Account ID token: %s", err)
    }
    envelopeDefinition := makeEnvelope("sonic the hedgefrog", "sonic@example.com", "sally@example.com", "sally signer")
    envelopeId, err := sendEnvelope(dSAccessToken, dSAccountId, envelopeDefinition)
    if err != nil {
        fmt.Printf("Failed to retrieve token: %s", err)
    }
    fmt.Println(envelopeId)
}

Additional resources

Aaron Wilde
Author
Aaron Jackson-Wilde
Programmer Writer
Published