├── .gitignore ├── README.md ├── go.mod ├── config.example.json ├── go.sum ├── examples └── HMAC_validation.go ├── structs.go └── app.go /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | private.key 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # code-examples-go 2 | 3 | Work-in-progress Code examples launcher, powered by Go. 4 | More examples will be added over time, please open an issue if you have any requests or feedback. 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module app 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/golang-jwt/jwt v3.2.2+incompatible 7 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 8 | ) 9 | 10 | require golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71 // indirect 11 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "signer_name" : "Sally Signer", 3 | "signer_email": "sally@tally.co", 4 | "cc_name" : "Mallory Managing", 5 | "cc_email": "mallory@tally.co", 6 | "integration_key": "00000000-0000-0000-0000-000000000000", 7 | "user_impersonation_guid_jwt": "00000000-0000-0000-0000-000000000000", 8 | "secret_key_authorization_code_grant" : "00000000-0000-0000-0000-000000000000", 9 | "RSA_private_key_jwt_location" : "./private.key" 10 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 2 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 3 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= 4 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 5 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71 h1:X/2sJAybVknnUnV7AD2HdT6rm2p5BP6eH2j+igduWgk= 6 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 7 | -------------------------------------------------------------------------------- /examples/HMAC_validation.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "encoding/base64" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "os" 8 | "fmt" 9 | ) 10 | 11 | 12 | //ds-snippet-start:Connect1Step1 13 | func computeHash(secret []byte, payload []byte) string{ 14 | mac := hmac.New(sha256.New, secret) 15 | mac.Write(payload) 16 | base64Hash := base64.StdEncoding.EncodeToString(mac.Sum(nil)) 17 | return base64Hash 18 | } 19 | 20 | func checkHash(secret string, payload []byte, verify string) bool { 21 | 22 | return hmac.Equal([]byte(verify), []byte(computeHash([]byte(secret), payload))) 23 | } 24 | //ds-snippet-end:Connect1Step1 25 | func main(){ 26 | jsonResult, err := os.ReadFile("./payload.txt") 27 | if err != nil { 28 | fmt.Printf("Error opening file: %s", err) 29 | os.Exit(1) 30 | 31 | } 32 | 33 | fmt.Printf("Is this HMAC valid? %t", checkHash("{DocuSign HMAC private key}", jsonResult, "{JSON response signature}")) 34 | 35 | 36 | } -------------------------------------------------------------------------------- /structs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Config struct { 8 | SignerName string `json:"signer_name"` 9 | SignerEmail string `json:"signer_email"` 10 | CcName string `json:"cc_name"` 11 | CcEmail string `json:"cc_email"` 12 | IntegrationKey string `json:"integration_key"` 13 | UserImpersonationGUIDJwt string `json:"user_impersonation_guid_jwt"` 14 | SecretKeyAuthorizationCodeGrant string `json:"secret_key_authorization_code_grant"` 15 | RSAPrivateKeyJwtLocation string `json:"RSA_private_key_jwt_location"` 16 | } 17 | 18 | type AccessToken struct { 19 | Token string `json:"access_token"` 20 | Type string `json:"token_type"` 21 | Expiry int `json:"expires_in"` 22 | } 23 | 24 | // Auto-generated using https://transform.tools/json-to-go 25 | type AccountId struct { 26 | Sub string `json:"sub"` 27 | Name string `json:"name"` 28 | GivenName string `json:"given_name"` 29 | FamilyName string `json:"family_name"` 30 | Created string `json:"created"` 31 | Email string `json:"email"` 32 | Accounts []struct { 33 | AccountID string `json:"account_id"` 34 | IsDefault bool `json:"is_default"` 35 | AccountName string `json:"account_name"` 36 | BaseURI string `json:"base_uri"` 37 | Organization struct { 38 | OrganizationID string `json:"organization_id"` 39 | Links []struct { 40 | Rel string `json:"rel"` 41 | Href string `json:"href"` 42 | } `json:"links"` 43 | } `json:"organization"` 44 | } `json:"accounts"` 45 | } 46 | 47 | // Auto-generated using https://transform.tools/json-to-go 48 | type EnvelopeID struct { 49 | EnvelopeID string `json:"envelopeId"` 50 | URI string `json:"uri"` 51 | StatusDateTime time.Time `json:"statusDateTime"` 52 | Status string `json:"status"` 53 | } 54 | 55 | // Auto-generated using https://transform.tools/json-to-go 56 | type CustomFields struct { 57 | TextCustomFields []struct { 58 | FieldID string `json:"fieldId"` 59 | Name string `json:"name"` 60 | Show string `json:"show"` 61 | Required string `json:"required"` 62 | Value string `json:"value"` 63 | } `json:"textCustomFields"` 64 | ListCustomFields []interface{} `json:"listCustomFields"` 65 | } 66 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "math/rand" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "strings" 13 | "time" 14 | "examples" 15 | "github.com/golang-jwt/jwt" 16 | "github.com/pkg/browser" 17 | ) 18 | 19 | // Random String generator made from: https://github.com/Onelinerhub/onelinerhub/blob/main/golang/how-to-generate-random-string.md 20 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 21 | 22 | func rand_str(n int) string { 23 | b := make([]rune, n) 24 | for i := range b { 25 | b[i] = letters[rand.Intn(len(letters))] 26 | } 27 | return string(b) 28 | } 29 | 30 | var DSAccessToken string 31 | var DSAccountId string 32 | var EnvelopeId string 33 | var EnvelopeDefinition string 34 | var config Config 35 | 36 | // For RSA signing method, the key can be any []byte. It is recommended to generate 37 | // a key using crypto/rand or something equivalent. You need the same key for signing 38 | // and validating. 39 | var RSAPrivateKey []byte 40 | 41 | func makeDSToken(config Config) (string, error) { 42 | 43 | // Create a new JWT claim. Set your integration key, impersonated user GUID, time of issue, expiry time, account server, and required scopes 44 | rawJWT := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ 45 | "iss": config.IntegrationKey, 46 | "sub": config.UserImpersonationGUIDJwt, 47 | "iat": time.Now().Unix(), 48 | "exp": time.Now().Unix() + 3600, 49 | "aud": "account-d.docusign.com", 50 | "scope": "signature impersonation", 51 | }) 52 | 53 | RSAPrivateKey, err := os.ReadFile(config.RSAPrivateKeyJwtLocation) 54 | if err != nil { 55 | log.Fatalf("Error opening file: %s", err) 56 | return "", err 57 | } 58 | 59 | // Load the private.key file into JWT library 60 | rsaPrivate, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(RSAPrivateKey)) 61 | if err != nil { 62 | log.Fatalf("key update error for: %s", err) 63 | return "", err 64 | } 65 | 66 | // Generate the signed JSON Web Token assertion with an RSA private key 67 | tokenString, err := rawJWT.SignedString(rsaPrivate) 68 | //fmt.Println(tokenString, err) 69 | if err != nil { 70 | log.Fatalf("Request Failed: %s", err) 71 | return "", err 72 | } 73 | 74 | // Submit the JWT to the account server and request and access token 75 | resp, err := http.PostForm("https://account-d.docusign.com/oauth/token", 76 | url.Values{ 77 | "grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"}, 78 | "assertion": {tokenString}, 79 | }) 80 | 81 | if err != nil { 82 | log.Fatalf("Request Failed: %s", err) 83 | return "", err 84 | } 85 | 86 | body, err := io.ReadAll(resp.Body) 87 | if err != nil { 88 | log.Fatalf("Request Failed: %s", err) 89 | return "", err 90 | } 91 | fmt.Printf("Body: %s\n", body) 92 | 93 | if strings.Contains(string(body), "consent_required") { 94 | fmt.Println("consent has not been granted to use this account") 95 | var loginUrl = fmt.Sprintf("https://account-d.docusign.com/oauth/auth?response_type=code&scope=signature+impersonation&client_id=%s&redirect_uri=https://developers.docusign.com/platform/auth/consent", config.IntegrationKey) 96 | browser.OpenURL(loginUrl) 97 | fmt.Println("A new browser window has been opened to: %s", loginUrl) 98 | fmt.Println("Waiting for 90 seconds then I'll try to login using JWT again") 99 | time.Sleep(90 * time.Second) 100 | makeDSToken(config) 101 | } 102 | 103 | // Done with the request, close it: https://stackoverflow.com/q/18598780/2226328 104 | resp.Body.Close() 105 | 106 | // Decode the response to JSON 107 | var token AccessToken 108 | jsonErr := json.Unmarshal(body, &token) 109 | if jsonErr != nil { 110 | log.Fatalf("There was an error decoding the json. err = %s", jsonErr) 111 | return "", jsonErr 112 | } 113 | //fmt.Println(token.Token) 114 | return token.Token, nil 115 | } 116 | 117 | // Internal API call to pull in the API account ID GUID used to make all subsequent API calls 118 | func getAPIAccId(DSAccessToken string) (string, error) { 119 | client := &http.Client{} 120 | // Use http.NewRequest in order to set custom headers 121 | req, err := http.NewRequest("GET", "https://account-d.docusign.com/oauth/userinfo", nil) 122 | req.Header.Set("Authorization", "Bearer "+DSAccessToken) 123 | if err != nil { 124 | log.Fatalf("Request Failed: %s", err) 125 | return "", err 126 | } 127 | 128 | // Since http.NewRequest is being used, client.Do is needed to execute the request 129 | res, err := client.Do(req) 130 | if err != nil { 131 | log.Fatalf("Request Failed: %s", err) 132 | return "", err 133 | 134 | } 135 | 136 | body, err := io.ReadAll(res.Body) 137 | if err != nil { 138 | log.Fatalf("Request Failed: %s", err) 139 | return "", err 140 | } 141 | // fmt.Printf("Body: %s\n", body) 142 | res.Body.Close() 143 | 144 | // Decode the response to JSON 145 | var accountId AccountId 146 | jsonErr := json.Unmarshal(body, &accountId) 147 | if jsonErr != nil { 148 | log.Fatalf("There was an error decoding the json. err = %s", jsonErr) 149 | return "", jsonErr 150 | } 151 | 152 | //fmt.Println(accountId.Accounts[0].AccountID) 153 | return accountId.Accounts[0].AccountID, nil 154 | 155 | } 156 | 157 | // Make the envelope definition 158 | func makeEnvelope(signerName string, signerEmail string, ccName string, ccEmail string) string { 159 | 160 | envelope := fmt.Sprintf(`{ 161 | "emailSubject": "Please sign this document set", 162 | "documents": [{ 163 | "documentBase64": "DQoNCg0KCQkJCXRleHQgZG9jDQoNCg0KDQoNCg0KUk0gIwlSTSAjCVJNICMNCg0KDQoNClxzMVwNCg0KLy9hbmNoMSANCgkvL2FuY2gyDQoJCS8vYW5jaDM=", 164 | "documentId": "1", 165 | "fileExtension": "txt", 166 | "name": "NDA" 167 | }], 168 | "recipients": { 169 | "carbonCopies": [ 170 | { 171 | "email": "%s", 172 | "name": "%s", 173 | "recipientId": "2", 174 | "routingOrder": "2" 175 | } 176 | ], 177 | "signers": [ 178 | { 179 | "email": "%s", 180 | "name": "%s", 181 | "recipientId": "1", 182 | "routingOrder": "1", 183 | "tabs": { 184 | "signHereTabs": [{ 185 | "documentId": "1", 186 | "name": "SignHereTab", 187 | "pageNumber": "1", 188 | "recipientId": "1", 189 | "tabLabel": "SignHereTab", 190 | "xPosition": "75", 191 | "yPosition": "572" 192 | }] 193 | }, 194 | } 195 | ] 196 | }, 197 | "status": "sent" 198 | }`, ccEmail, ccName, signerEmail, signerName) 199 | 200 | return envelope 201 | } 202 | 203 | func makeCustomFieldsEnvelope(signerEmail string, signerName string, customerId string) string { 204 | 205 | dateTimeString := time.Now().Format("01-02-2006") 206 | 207 | envelope := fmt.Sprintf(`{ 208 | "emailSubject": "Please review this recent order:", 209 | "documents": [{ 210 | "documentBase64": "DQoNCg0KCQkJCXRleHQgZG9jDQoNCg0KDQoNCg0KUk0gIwlSTSAjCVJNICMNCg0KDQoNClxzMVwNCg0KLy9hbmNoMSANCgkvL2FuY2gyDQoJCS8vYW5jaDM=", 211 | "documentId": "1", 212 | "fileExtension": "txt", 213 | "name": "Order Summary %s" 214 | }], 215 | "customFields": { 216 | "textCustomFields": [ 217 | { 218 | "name": "trackingNumber", 219 | "required": "false", 220 | "show": "true", 221 | }, 222 | { 223 | "name": "shippingDate", 224 | "required": "false", 225 | "show": "true", 226 | }, 227 | { 228 | "name": "customerID", 229 | "required": "true", 230 | "show": "true", 231 | "value": "%s" 232 | } 233 | ] 234 | }, 235 | "recipients": { 236 | "signers": [ 237 | { 238 | "email": "%s", 239 | "name": "%s", 240 | "recipientId": "1", 241 | "routingOrder": "1", 242 | "tabs": { 243 | "signHereTabs": [{ 244 | "documentId": "1", 245 | "name": "SignHereTab", 246 | "pageNumber": "1", 247 | "recipientId": "1", 248 | "tabLabel": "SignHereTab", 249 | "xPosition": "75", 250 | "yPosition": "572" 251 | }] 252 | }, 253 | } 254 | ] 255 | }, 256 | "status": "sent" 257 | }`, dateTimeString, customerId, signerEmail, signerName) 258 | 259 | return envelope 260 | } 261 | 262 | // Send an envelope 263 | func sendEnvelope(DSAccessToken string, DSAccountId string, envelopeDefinition string) (string, error) { 264 | client := &http.Client{} 265 | // Use http.NewRequest in order to set custom headers 266 | req, err := http.NewRequest("POST", "https://demo.docusign.net/restapi/v2.1/accounts/"+DSAccountId+"/envelopes", strings.NewReader(envelopeDefinition)) 267 | req.Header.Set("Authorization", "Bearer "+DSAccessToken) 268 | if err != nil { 269 | log.Fatalf("Request Failed: %s", err) 270 | return "", err 271 | } 272 | // Since http.NewRequest is being used, client.Do is needed to execute the request 273 | res, err := client.Do(req) 274 | if err != nil { 275 | log.Fatalf("Request Failed: %s", err) 276 | return "", err 277 | } 278 | 279 | body, err := io.ReadAll(res.Body) 280 | if err != nil { 281 | log.Fatalf("Request Failed: %s", err) 282 | return "", err 283 | } 284 | 285 | // Decode the response to JSON 286 | var envelope EnvelopeID 287 | jsonErr := json.Unmarshal(body, &envelope) 288 | if jsonErr != nil { 289 | log.Fatalf("Request Failed: %s", jsonErr) 290 | return "", jsonErr 291 | } 292 | return envelope.EnvelopeID, nil 293 | } 294 | 295 | // Gets envelope custom fields 296 | func getCustomFields(DSAccessToken string, DSAccountId string, envelopeId string) ([]string, error) { 297 | client := &http.Client{} 298 | // Use http.NewRequest in order to set custom headers 299 | req, err := http.NewRequest("GET", "https://demo.docusign.net/restapi/v2.1/accounts/"+DSAccountId+"/envelopes/"+envelopeId+"/custom_fields", nil) 300 | req.Header.Set("Authorization", "Bearer "+DSAccessToken) 301 | if err != nil { 302 | log.Fatalf("Request Failed: %s", err) 303 | return []string{}, err 304 | } 305 | // Since http.NewRequest is being used, client.Do is needed to execute the request 306 | res, err := client.Do(req) 307 | if err != nil { 308 | log.Fatalf("Request Failed: %s", err) 309 | return []string{}, err 310 | } 311 | // fmt.Printf("response: %s\n", res) 312 | 313 | body, err := io.ReadAll(res.Body) 314 | if err != nil { 315 | log.Fatalf("Request Failed: %s", err) 316 | return []string{}, err 317 | } 318 | 319 | // Decode the response to JSON 320 | var customFields CustomFields 321 | jsonErr := json.Unmarshal(body, &customFields) 322 | if jsonErr != nil { 323 | log.Fatalf("Request Failed: %s", jsonErr) 324 | return []string{}, jsonErr 325 | } 326 | fmt.Print(customFields.TextCustomFields[0].FieldID + " -> tracking id\n") 327 | fmt.Print(customFields.TextCustomFields[1].FieldID + " -> ship date\n") 328 | responseArray := []string{customFields.TextCustomFields[0].FieldID, customFields.TextCustomFields[1].FieldID} 329 | return responseArray, nil 330 | } 331 | 332 | // Sets custom field values for an envelope 333 | func setCustomfieldValues(DSAccessToken string, DSAccountId string, envelopeId string, trackingNumberFieldId string, shippingDateFieldId string) (string, error) { 334 | client := &http.Client{} 335 | 336 | // For the sake of the example these are arbitrary values, 337 | // on a real application these values should be retreived 338 | // from an orders table on a database 339 | shippingDate := time.Now().Add(time.Hour * 24 * 3).Format("01-02-2006") 340 | trackingNumber := "1Z" + rand_str(16) 341 | 342 | requestBody := fmt.Sprintf(`{ textCustomFields: [ 343 | { "fieldId" : "%s", 344 | "value" : "%s" }, 345 | { "fieldId" : "%s", 346 | "value" : "%s" } 347 | 348 | ]}`, trackingNumberFieldId, trackingNumber, shippingDateFieldId, shippingDate) 349 | 350 | // Use http.NewRequest in order to set custom headers 351 | req, err := http.NewRequest("PUT", "https://demo.docusign.net/restapi/v2.1/accounts/"+DSAccountId+"/envelopes/"+envelopeId+"/custom_fields", strings.NewReader(requestBody)) 352 | req.Header.Set("Authorization", "Bearer "+DSAccessToken) 353 | if err != nil { 354 | log.Fatalf("Request Failed: %s", err) 355 | return "", err 356 | } 357 | // Since http.NewRequest is being used, client.Do is needed to execute the request 358 | res, err := client.Do(req) 359 | if err != nil { 360 | log.Fatalf("Request Failed: %s", err) 361 | return "", err 362 | } 363 | 364 | body, err := io.ReadAll(res.Body) 365 | if err != nil { 366 | log.Fatalf("Request Failed: %s", err) 367 | return "", err 368 | } 369 | fmt.Printf("Body: %s\n", body) 370 | 371 | return string(body), nil 372 | } 373 | 374 | // Voids an envelope 375 | func voidEnvelope(DSAccessToken string, DSAccountId string, EnvelopeID string) (string, error) { 376 | client := &http.Client{} 377 | 378 | voidBody := `{ 379 | "status": "voided", 380 | "voidedReason": "The reason for voiding the envelope" 381 | }` 382 | 383 | // Use http.NewRequest in order to set custom headers 384 | req, err := http.NewRequest("PUT", "https://demo.docusign.net/restapi/v2.1/accounts/"+DSAccountId+"/envelopes/"+EnvelopeID, strings.NewReader(voidBody)) 385 | req.Header.Set("Authorization", "Bearer "+DSAccessToken) 386 | if err != nil { 387 | log.Fatalf("Request Failed: %s", err) 388 | return "", err 389 | } 390 | // Since http.NewRequest is being used, client.Do is needed to execute the request 391 | res, err := client.Do(req) 392 | if err != nil { 393 | log.Fatalf("Request Failed: %s", err) 394 | return "", err 395 | } 396 | fmt.Print("Void Envelope Response: " + res.Status + "\n") 397 | return res.Status, err 398 | 399 | } 400 | 401 | func main() { 402 | 403 | fmt.Println("\n\nWelcome to the DocuSign Go Launcher using Authorization Code grant or JWT grant authentication.") 404 | 405 | data, err := os.ReadFile("config.json") 406 | if err != nil { 407 | log.Fatalf("Error when opening file: %v", err) 408 | } 409 | 410 | err = json.Unmarshal(data, &config) 411 | if err != nil { 412 | log.Fatalf("Error when unmarshaling JSON: %v", err) 413 | } 414 | 415 | //fmt.Printf("Config: %+v\n", config) 416 | 417 | // we delcared the config variable around line 92 418 | 419 | fmt.Println("We're making an Access token using JWT grant authentication.\n") 420 | dSAccessToken, err := makeDSToken(config) 421 | if err != nil { 422 | log.Fatalf("Failed to retrieve token: %s", err) 423 | } 424 | fmt.Printf("access token: %s\n", dSAccessToken) 425 | 426 | fmt.Println("\n\nNow we're retrieving your API account ID to make further API calls") 427 | dSAccountId, err := getAPIAccId(dSAccessToken) 428 | if err != nil { 429 | log.Fatalf("Failed to API Account ID token: %s", err) 430 | } 431 | 432 | // section 1: send an envelope (and VOID IT) 433 | 434 | envelopeDefinition := makeEnvelope(config.SignerName, config.SignerEmail, config.CcName, config.CcEmail) 435 | envelopeId, err := sendEnvelope(dSAccessToken, dSAccountId, envelopeDefinition) 436 | if err != nil { 437 | log.Fatalf("Failed to retrieve token: %s", err) 438 | } 439 | 440 | voidEnvelope(dSAccessToken, dSAccountId, envelopeId) 441 | 442 | // section 2: BONUS: Let's create an envelope with Custom Fields then update them programmatically 443 | envelopeDefinitionCustomTabs := makeCustomFieldsEnvelope(config.SignerEmail, config.SignerName, "CUSTOMER-"+rand_str(14)) 444 | // fmt.Printf(envelopeDefinitionCustomTabs) 445 | 446 | envelopeId2, err := sendEnvelope(dSAccessToken, dSAccountId, envelopeDefinitionCustomTabs) 447 | if err != nil { 448 | log.Fatalf("update custom fields request failed: %s", err) 449 | } 450 | // fmt.Print("envelope ID is: " + envelopeId2 + "\n") 451 | 452 | // Lets retrieve the Envelope custom Fields ids 453 | customFieldIds, err := getCustomFields(dSAccessToken, dSAccountId, envelopeId2) 454 | 455 | // Set the shipping date and tracking number on the envelope 456 | finalRes, err := setCustomfieldValues(dSAccessToken, dSAccountId, envelopeId2, customFieldIds[0], customFieldIds[1]) 457 | fmt.Printf("Envelope Custom fields updated successfully: \n%s", finalRes) 458 | 459 | } 460 | --------------------------------------------------------------------------------