This topic describes the details and sample code to merge the document with Salesforce data using Apex call. 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 save the following sample Apex code class into the Salesforce Org, you can directly invoke the public function initiateCongaAPI() from the Salesforce Developer Console (i.e. CongaAPI.initiateCongaAPI();) to initiate a Conga API merge.

In this sample code, we merge the document into PDF format and then email it to a Salesforce Contact with one Salesforce attachment. All of these composer 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)

public with sharing class CongaApi {

  // Contact your Account Executive to enable the Composer API Integration feature in the Composer Setup Menu.
  // Contact your Account Executive to white-glove the org from the Conga Auth server (the 18 digits orgId is the only needed information for this process)

  // These values should be retrieved from Composer Setup Menu.
    private static String congaAuthClientId = 'CONGA_AUTH_CLIENT_ID';         // Consider Named Credentials
    private static String congaAuthClientSecret = 'CONGA_AUTH_CLIENT_SECRET'; // Consider Named Credentials

// the Salesforce info needed.

    private static String orgId = '00D8B000000NlDc'; // Consider UserInfo.getOrganizationId();
    private static String composerTemplateId = 'a0F8A000002yVNXUA2';
    private static String salesforceMasterObjId = '0018A00000kXOgcQAG';

// call the Conga Auth api to get Conga-Auth-AccessToken

    private static String retrieveCongaAuthToken() {
        try {
            HttpRequest req = new HttpRequest();

            String postBody = 'grant_type=client_credentials&scope=doc-gen.composer&client_id=' + congaAuthClientId + '&client_secret=' + congaAuthClientSecret;
            req.setBody(postBody);

            req.setHeader('Content-Type', 'application/x-www-form-urlencoded');

            req.setEndpoint('https://services.congamerge.com/api/v1/auth/connect/token');
            req.setMethod('POST');
            req.setTimeout(30000);
            HttpResponse res = new Http().send(req);
            return res.getBody();
        } catch(Exception e) {
            throw new CongaHandledException(Label.Toast_General_Merge_Error).withMessage(e.getMessage());
        }
    }

// call the Salesforce Auth endpoint to get Salesforce-AccessToken

    private static String retrieveSalesforceAccessToken() {
        try {
            return UserInfo.getsessionId();
        } catch(Exception e) {
            throw new CongaHandledException(Label.Toast_General_Merge_Error).withMessage(e.getMessage());
        }
    }

    private static String invokeCongaIngressApi(string congaAuthToken, string salesforceAccessToken, string instanceUrl) {
        try {
            HttpRequest req = new HttpRequest();

            String postBody = '{' +
                '"SalesforceRequest": {' +
                    '"sessionId": "'+ salesforceAccessToken + '",' +
                    '"TemplateId": "' + composerTemplateId + '",' +
                    '"MasterId": "' + salesforceMasterObjId + '",' +
                    '"ServerUrl": "'+ instanceUrl + '/services/Soap/u/50.0/' + orgId +'",' +
                '}, "LegacyOptions": {"sc0":"1","sc1":"attachments","DeFaultpdf":"1","Pdfa":"1a","DS7":"12","EmailToId":"0037e00001WzHFQAA3",
                                      "CongaEmailTemplateId":"a067e00000BiBriAAF","attachmentid":"0697e0000018RTQ"}' +
            '}';
            req.setBody(postBody);

            req.setHeader('Content-Length', String.valueOf(postBody.Length()));
            req.setHeader('Content-Type', 'application/json');
            req.setHeader('Authorization', 'Bearer ' + congaAuthToken);

            req.setEndpoint('https://coreapps-rlsprod.congacloud.com/api/ingress/v1/Merge');
            req.setMethod('POST');
            req.setTimeout(30000);
            HttpResponse res = new Http().send(req);
            return res.getBody();
        } catch(Exception e) {
            throw new CongaHandledException(Label.Toast_General_Merge_Error).withMessage(e.getMessage());
        }
    }

    private static String invokeCongaStatusServiceApi(String congaAuthToken, String correlationId) {
        try {
            HttpRequest req = new HttpRequest();
            req.setHeader('Authorization', 'Bearer ' + congaAuthToken);
            req.setEndpoint('https://services.congamerge.com/api/v1/status/v1/Status/' + correlationId);
            req.setMethod('GET');
            req.setTimeout(30000);
            HttpResponse res = new Http().send(req);
            return res.getBody();
        } catch(Exception e) {
            throw new CongaHandledException(Label.Toast_General_Merge_Error).withMessage(e.getMessage());
        }
    }

    private static void DelayController() {
        Long startTime = DateTime.now().getTime();
        Long finishTime = DateTime.now().getTime();
        while ((finishTime - startTime) < 1000) {
            //sleep for 1s
            finishTime = DateTime.now().getTime();
        }
    }

    public static void initiateCongaAPI(){
         try {
             // Step 1: call the Conga Auth server to get the Conga AccessToken

            String congaAuthApiResponse = retrieveCongaAuthToken();
            system.debug('congaAuthApiResponse:' + congaAuthApiResponse);
            String congaAccessToken = ((Map<String, Object>)JSON.deserializeUntyped(congaAuthApiResponse)).get('access_token').toString();
            system.debug('congaAccessToken:' + congaAccessToken);

            // Step 2: call the Salesforce Auth server to get the Conga AccessToken

            String salesforceAccessTokenResponse = retrieveSalesforceAccessToken();
            Map<String, Object> salesforceAccessTokenResponseMap = (Map<String, Object>)JSON.deserializeUntyped(salesforceAccessTokenResponse);
            String salesforceAccessToken = (salesforceAccessTokenResponseMap).get('access_token').toString();
            String instanceUrl = (salesforceAccessTokenResponseMap).get('instance_url').toString();
            system.debug('salesforceAccessToken:' + salesforceAccessToken);
            system.debug('instanceUrl:' + instanceUrl);

            String congaIngressApiResponse = invokeCongaIngressApi(congaAccessToken, salesforceAccessToken, instanceUrl);
            Map<String, Object> congaIngressApiResponseMap = (Map<String, Object>)JSON.deserializeUntyped(congaIngressApiResponse);
            String correlationId = (congaIngressApiResponseMap).get('correlationId').toString();
            system.debug('correlationId:' + correlationId);

// Check the status-service for 50 times, and wait 1 seconds between each request. Exit the loop if check over 100 times or the status contains `"detail":""`

            for (Integer i = 0; i < 50; i++) {
                String congaStatusServiceResponse = invokeCongaStatusServiceApi(congaAccessToken, correlationId);

                DelayController();

                system.debug('congaStatusServiceResponse:' + congaStatusServiceResponse);
                if (congaStatusServiceResponse.replaceAll( '\\s+', '').indexOf('"message":"Completed"')!=-1){
                    system.debug('merge is successful. correlationId is: ' + correlationId);
                    break;
                } else if (congaStatusServiceResponse.replaceAll( '\\s+', '').indexOf('"message":"Error"')!=-1) {
                    system.debug('merge is failed. correlationId is: ' + correlationId);
                    break;
                }
            }
         } catch(Exception e) {
            throw new CongaHandledException(Label.Toast_General_Merge_Error).withMessage(e.getMessage());
        }
    }
}
CODE

This function 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.
    In the sample code above the current user session is used for the merge. This is how the legacy composer solutions behave. However, it may be preferable to use an integration user for the API to control permissions.
    Sample Code (It may differ depending on your usage):

    // If Salesforce OAuth is desired, a connected app needs to be created within the Salesforce Org. Copy the Client_Id value to salesforceConnectedAppClientId and the Client_Secret value to salesforceConnectedAppClientSecret.
    
        private static String salesforceConnectedAppClientId = 'SALESFORCE_CONNECTED_APP_CLIENT_ID';
        private static String salesforceConnectedAppClientSecret = 'SALESFORCE_CONNECTED_APP_CLIENT_SECRET';
    
        private static String orgUserName = 'SFDC_USER_PSWD';
        private static String orgUserPassword = 'SFDC_USER_PSWD';
    
    // call the Salesforce Auth endpoint to get Salesforce-AccessToken
    
        private static String retrieveSalesforceAccessToken() {
            try {
                HttpRequest req = new HttpRequest();
    
                String postBody = 'grant_type=password&username=' + orgUserName + '&password=' + orgUserPassword+ '&client_id=' + salesforceConnectedAppClientId + '&client_secret=' + salesforceConnectedAppClientSecret;
                req.setBody(postBody);
                req.setHeader('Content-Length', String.valueOf(postBody.Length()));
                req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
    
                // change the auth endpoint to https://login.salesforce.com/services/oauth2/token for production or dev org.
                req.setEndpoint('https://test.salesforce.com/services/oauth2/token');
                req.setMethod('POST');
                req.setTimeout(30000);
                HttpResponse res = new Http().send(req);
                return res.getBody();
            } catch(Exception e) {
                throw new CongaHandledException(Label.Toast_General_Merge_Error).withMessage(e.getMessage());
            }
        }
    CODE

    This modified method makes an API call using additional parameters to get a session associated with the Connected App identified by salesforceConnectedAppClientId and salesforceConnectedAppClientSecret.

    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_secret

      Create 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-5029-dev-ed.cs40.my.salesforce.com",
          "id": "https://test.salesforce.com/id/00D9A000000NlEbUAK/0058A000009SUwTQAW",
          "token_type": "Bearer",
          "issued_at": "1649798597751",
          "signature": "1p0VXuOaqgppeP8oMBOnQqhgwt6Fr6plKi8SaYkfMYo="
      }
      CODE

    For more information on Salesforce Auth Access Token, see Salesforce OAuth and JWT Documentation.

  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": {"sc0":"1","sc1":"attachments","DeFaultpdf":"1","Pdfa":"1a","DS7":"12","EmailToId":"0048e00001WzHFQAA3",
             "CongaEmailTemplateId":"a036e00000BjBriAAF","attachmentid":"0991e0000018STQ"}
       }
      CODE

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

    • Sample Response:

      {
          "correlationId": "<correlationId>",
          "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

Using Status Service:
The Status service should be an integral part of your API solution. For each API request, the Status service should be polled. When it is used correctly, your solution can monitor the progress of the merge and provide updates along the way in processing your requests.

The status service provides not only flow updates such as InProgress to Completed, but it also reports errors in your request composition. Missing data, missing template ids, missing session ids, missing or misconfigured JSON data, and merge fields, such as TableStart and TableEnd, are examples of what the status service can provide detail on for requests as they move through the API flow.

Using and integrating the Status service into your solutions can provide earlier notifications and reduce the need to contact support for troubleshooting issues when they arise. You could use the status service to notify administrators or end users about the status.

Security Considerations

Endpoint Authentication in Salesforce Apex:

It is recommended to use Salesforce platform mechanisms to protect credentials used for Conga platform callouts (and Salesforce platform if applicable). These callouts require credentials that should be securely stored on the Salesforce platform rather than hard coded. It is also recommended to configure Named Credentials for the apex callouts.

Salesforce IP Restrictions:

Obtaining a new session from Salesforce to be used for merging will always require a call from a trusted IP address or Connected App. This is required if executed within Salesforce as Apex, in a client browser, or on an external application server. Trusted IP will only be required for the origin of the login call; whitelisting the Conga platform is not required. There are three ways to build trust.

  • Relax the IP restrictions for the OAuth-connected app that is being used to obtain the session.
  • Add the caller IP address (the client making the login call) to the list of Trusted IP ranges:
    • For the Organization Network Access
    • On the Salesforce User’s Profile
  • Append the Salesforce User Security Token to the password.

For more information, see Salesforce Security and the API.