This topic describes the details and sample code to merge the document with Salesforce data using .NET Core 3.1 SDK. You might use this functionality differently to generate documents depending on your business case.

See Composer REST APIs for a comprehensive list of Composer APIs that you can use for your business use case.

Prerequisites

Important

The Composer APIs are available as part of an early adopter program that requires Conga product management review for inclusion.

  • Contact your Account Executive with your 18-digit OrgID to enable the Composer API feature.
  • Refer to the Authentication section to generate the Client ID and Client Secret that will be used in the code to connect to the Conga Authorization server.

  • Refer to Step 2 to obtain the sessionId (Salesforce Access Token) that will be used to retrieve the Composer template and Salesforce data for the merge.

Document Generation

When you open the following sample .NET Core 3.1 class in a Visual Studio Console project, you can use the IDE to directly execute the 'Main' function in this class to initiate a Conga API merge.

In this sample code, we merge the document into PDF format and save it to Google Drive (You need to set up Google Drive under the Composer Setup Menu first). All the parameters are added to the LegacyOptions section of the Conga Ingress API call. For more information. see Step 3.

Sample Code (It may differ depending on your usage)

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace CongaApiSample
{
    class CongaAPI
    {
        // Contact Conga to enable the Api Integration feature in the Composer Setup Menu.
        // Contact Conga to white-glove the org from the Conga Auth server (the 18 digits orgId is the only needed information for this process)
        
        // These values (Conga Auth ClientID and Client Secret) should be retrieved from Composer Setup Menu.
        private static string congaAuthClientId = "d0734ba0-fb06-4191-0190-7339926751bb";
        private static string congaAuthClientSecret = "xhYN?1w@V1K1!!nm6e6W7$2cX";

        // A Salesforce connected app is needed to be created within the Salesforce Org. After creating the connected app, copy the Client_Id value to salesforceConnectedAppClientId and the Client_Secret value to salesforceConnectedAppClientSecret.
        private static string salesforceConnectedAppClientId =
            "3MVG9vDPWAliPr7o2OJES4dpOVXLgqE1VBwQw8AAb.rya4MWqq178_scIV7jTmKabAn0R343BPGuyPWEhQQBV";
        private static string salesforceConnectedAppClientSecret =
            "5DDD440103744C59A923C6B9D8CE03D99D4D23E6EC4A89CD5721378AF92F46BD";

        // Salesforce infomation needed.
        private static string composerTemplateId = "a0F7e02001MzgffEAB";
        private static string salesforceMasterObjId = "0017e01001WKqLzAAL";
        private static string orgUserName = "test-0n3kb6bpomrc@example.com";
        private static string orgUserPassword = "u]qk2izcfRhnu";
        private static string orgId = "00D7e00907EeloF";


        static async Task Main(string[] args)
        {
            Console.WriteLine("Conga Api is about to start executing!");

            Console.WriteLine("Step 1: reterieve the Conga Auth Token.");
            var congaAccessToken = await RetrieveCongaAuthToken();
           
            Console.WriteLine("Step 2: reterieve the Salesforce Auth Token and InstanceUrl.");
            var salesForceTokenAndInstanceUrl = await RetrieveSalesForceAccessToken();
            
            Console.WriteLine("Step 3: initiate the Conga Ingress call for the merge.");
            var correlationId = await InvokeCongaIngressApi(congaAccessToken, salesForceTokenAndInstanceUrl[0],
                salesForceTokenAndInstanceUrl[1]);
            Console.WriteLine("Step 3 finished: the correlationId is: " + correlationId);
            
            Console.WriteLine("Step 4: Constant checking the Conga Status Service every two seconds to see if the merge has finished.");
            for (int i = 0; i < 40; i++)
            {
                string congaStatusServiceResponse = await InvokeCongaStatusServiceApi(congaAccessToken, correlationId);
                Thread.Sleep(3000);
                Console.WriteLine();
                Console.WriteLine(Regex.Replace(congaStatusServiceResponse, @"\s+", ""));
                if (Regex.Replace(congaStatusServiceResponse, @"\s+", "")?.IndexOf(@"""message"":""Completed""") != -1)
                {
                    Console.WriteLine("merge is successful. The correlationId is: " + correlationId);
                    break;
                }
                else if (Regex.Replace(congaStatusServiceResponse, @"\s+", "")?.IndexOf(@"""message"":""Error""") != -1)
                {
                    Console.WriteLine("merge is failed. The correlationId is: " + correlationId);
                    break;
                }
            }
        }
        
        // Call the Conga Auth api to get Conga-Auth-AccessToken
        private static async Task<string> RetrieveCongaAuthToken()
        {
            try
            {
                var client = new HttpClient();
                var stringContent = new FormUrlEncodedContent(new[]
                {
                    new KeyValuePair<string, string>("grant_type", "client_credentials"),
                    new KeyValuePair<string, string>("scope", "doc-gen.composer"),
                    new KeyValuePair<string, string>("client_id", congaAuthClientId),
                    new KeyValuePair<string, string>("client_secret", congaAuthClientSecret)
                });

                var response =
                    await client.PostAsync("https://services.congamerge.com/api/v1/auth/connect/token",
                        stringContent);
                if (response.Content != null)
                {
                    var responseJson = await response.Content.ReadAsStringAsync();
                    dynamic responseObj = JsonConvert.DeserializeObject(responseJson);
                    var accessToken = (string) responseObj?.access_token;
                    return accessToken;
                }

                throw new Exception("retrieveCongaAuthToken() returned null value.");
            }
            catch (Exception e)
            {
                throw new Exception(e.Message);
            }
        }

        // Call the Salesforce Auth endpoint to get Salesforce-AccessToken
        private static async Task<List<string>> RetrieveSalesForceAccessToken()
        {
            try
            {
                var result = new List<string>();
                var client = new HttpClient();
                var stringContent = new FormUrlEncodedContent(new[]
                {
                    new KeyValuePair<string, string>("grant_type", "password"),
                    new KeyValuePair<string, string>("username", orgUserName),
                    new KeyValuePair<string, string>("password", orgUserPassword),
                    new KeyValuePair<string, string>("client_id", salesforceConnectedAppClientId),
                    new KeyValuePair<string, string>("client_secret", salesforceConnectedAppClientSecret)
                });
                stringContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
                var response =
                    await client.PostAsync("https://test.salesforce.com/services/oauth2/token", stringContent);

                if (response.Content != null)
                {
                    var responseJson = await response.Content.ReadAsStringAsync();
                    dynamic responseObj = JsonConvert.DeserializeObject(responseJson);
                    var accessToken = (string) responseObj?.access_token;
                    var instanceUrl = (string) responseObj?.instance_url;
                    result.Add((accessToken));
                    result.Add(instanceUrl);

                    return result;
                }

                throw new Exception("retrieveSalesForceAccessToken() returned null value.");
            }
            catch (Exception e)
            {
                throw new Exception(e.Message);
            }
        }

        // Call the Conga Ingress endpoint to initiate the merge process
        private static async Task<string> InvokeCongaIngressApi(string congaAuthToken, string salesforceAccessToken,
            string instanceUrl)
        {
            try
            {
                var client = new HttpClient();

                string postBody = @"{{'SalesforceRequest': 
                    {{
                        'sessionId': '{0}',
                        'TemplateId': '{1}' , 
                        'MasterId': '{2}',
                        'ServerUrl': '{3}/services/Soap/u/50.0/{4}',
                    }}, 
                    'LegacyOptions': {{'DeFaultpdf':'1','Pdfa':'1a','DS7':'9','OFP':'<google-drive relative path>'}}
                }}";

                postBody = string.Format(postBody, salesforceAccessToken, composerTemplateId, salesforceMasterObjId,
                    instanceUrl, orgId);
                var stringContent = new StringContent(postBody, Encoding.UTF8, "application/json");
                stringContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
                client.DefaultRequestHeaders.Add("Authorization", "Bearer " + congaAuthToken);
                //stringContent.Headers.Add("Authorization", "Bearer " + congaAuthToken);

                var response = await client.PostAsync("https://coreapps-rlsprod.congacloud.com/api/ingress/v1/Merge",
                    stringContent);

                if (response.Content != null)
                {
                    var responseJson = await response.Content.ReadAsStringAsync();
                    dynamic responseObj = JsonConvert.DeserializeObject(responseJson);
                    return responseObj.correlationId;
                }

                throw new Exception("InvokeCongaIngressApi() failed.");
            }
            catch (Exception e)
            {
                throw new Exception(e.Message);
            }
        }

        // call the Conga status-service endpoint to check the merge progress
        private static async Task<string> InvokeCongaStatusServiceApi(String congaAuthToken, String correlationId)
        {
            try
            {
                var client = new HttpClient();
                client.DefaultRequestHeaders.Add("Authorization", "Bearer " + congaAuthToken);
                var response =
                    await client.GetAsync("https://services.congamerge.com/api/v1/status/v1/Status/" +
                                          correlationId);

                if (response.Content != null)
                {
                    var responseJson = await response.Content.ReadAsStringAsync();
                    return responseJson;
                }

                throw new Exception("InvokeCongaStatusServiceApi() failed.");
            }
            catch (Exception e)
            {
                throw new Exception(e.Message);
            }
        }
    }
}
CODE

The Main function above assembles all four required API calls. The following is the description of each of these four API calls.

  1. Conga Auth Access Token API call: In the sample code, the function that completed this task is called retrieveCongaAuthToken(). This API call returns the Conga-access-token, which will be used to authorize the Conga Ingress API call and the Conga Status-Service API call.

    • HTTP Method: POST

    • Server URL: https://services.congamerge.com/api/v1/auth/connect/token
    • Required Headers: Content-Type: application/x-www-form-urlencoded
    • Post Body:

      grant_type ‘client_credentials’
      Scope‘doc-gen.composer’
      client_id Retrieved from the Composer Setup Menu, and set in the code variable congaAuthClientId.
      client_secretRetrieved from the Composer Setup Menu, and set in the code variable congaAuthClientSecret.
    • Sample Response:

      {
          'access_token': 'eyJh………… ',
          'expires_in': 3600,
          'token_type': 'Bearer',
          'scope': 'doc-gen.composer'
      }
      CODE
  2. Salesforce Auth Access Token API call: In the sample code, the function that completed this task is called retrieveSalesforceAccessToken(). This API call returns the Salesforce-access-token, which will be used to authorize access to all the Salesforce data and Composer template stored in the Salesforce org.

    You can also retrieve the Salesforce Auth Access Token using any other way. More information can be found in the Salesforce documentation

    • HTTP Method: POST

    • Server URL: 
      - Sandbox: https://test.salesforce.com/services/oauth2/token
      - Production: https://login.salesforce.com/services/oauth2/token
    • Required Headers:
      - Content-Type: application/x-www-form-urlencoded
      - Content-Length: String.valueOf(postBody.Length())
    • Post Body:

      grant_type‘password’
      scope‘doc-gen.composer’
      client_id & client_secretCreate your own Salesforce connected app, get the Consumer Key and Consumer Secret, and set in the code variables salesforceConnectedAppClientId and salesforceConnectedAppClientSecret respectively.
      username‘{org user name}’
      password‘{org user password}’
    • Sample Response:

      {
          'access_token': '………………',
          'instance_url': 'https://speed-inspiration-509-dev-es.cs49.my.salesforce.com',
          'id': 'https://test.salesforce.com/id/00D9A000000NlEbUAK/0058A000009SUwTQAW',
          'token_type': 'Bearer',
          'issued_at': '1649798597751',
          'signature': '1p0VXuOaqgppeP8oMBOnQqhgwt6Fr6plKi8SaYkfMYo='
      }
      CODE
  3. Composer merge process Ingress API call: In the sample code, the function that completed this task is called invokeCongaIngressApi(). This API call initiates the merge process and returns a correlationId that will be used in the next status-service call (see step 4).
    • HTTP Method: POST

    • Server URL: https://coreapps-rlsprod.congacloud.com/api/ingress/v1/Merge

    • Required Headers:
      - Content-Type: application/json
      - Content-Length: String.valueOf(postBody.Length())
      - Authorization: Bearer + Conga Auth Access Token from the first Api call
    • Post Body:

      {
        'SalesforceRequest': 
           {
             'sessionId': <Salesforce AccessToken from the second API call>,
             'TemplateId':<set in the code variable composerTemplateId>,
             'MasterId': <set in the code variable salesforceMasterObjId>,
             'ServerUrl': <Salesforce serverUrl from the second API call> + ‘/services/Soap/u/50.0/’ + set in the code variable orgId }
           }
             'LegacyOptions': {'DeFaultpdf':'1','Pdfa':'1a','DS7':'19','OFP':'<google-drive relative path>'}
       }
      CODE

      For more information on supported parameters in the legacyOptions section for the Composer API request, click here.

    • Sample Response:

      {
          'correlationId': '85c462d8ee494a96bdc9be9b5a87dd45_DatasetJson',
          'status': 'Accepted',
          'result': {
              'statusCode': 'Success',
              'statusMessage': [
                  {
                      'code': 'SUCC200',
                      'description': 'Success'
                  }
              ]
          }
      }
      CODE
  4. Composer merge-status API call: In the sample code, the function that completed this task is called invokeCongaStatusServiceApi(). This API call checks the current merge's status and returns the status of all completed and ongoing merge steps. Call this API repeatedly until the response returns a message with the string "message":"Completed."
    • HTTP Method: GET

    • Server URL: https://services.congamerge.com/api/v1/status/v1/Status/ + { correlationId from the step 3 API call response}

    • Required Headers:
      - Authorization: Bearer + Conga Auth Access Token from the first API call

    • Sample Response:

        [
          'status': {
                          'version': '1.0.0',
                          'message': 'Pending',
                          'detail': 'Event publish request from Ingress',
                          'startDateTime': '2022-04-11T16:00:56.0204229Z',
                          'endDateTime': null,
                          'statusCode': 201
                    }
      ]
      CODE