Service Invocation

Invocation is the process where an Oracle Marketing product invokes an app to perform a task.

The product will retrieve the app's instance context from AMS, which includes the app's Invoke URL endpoint. Then the product will invoke the app based on the app's Invoke URL endpoint. The app should directly respond to the product to relay the result of the invocation back to the product.

Effectively, invocation is how products can use apps using the app's endpoints stored in AMS.

Requirements

  • Services must be configured before invocation. Service configuration allows the app to establish record definition field mappings so that the app will know how the product will invoke it.

In this topic:

Invocation types

There are two different types of invocations. Apps should be developed for both invocation types.

  • Invoke with data. Apps need to parse data and then perform an action.
  • Invoke without data. Apps need to pull data.

The invocation type is based on the dataSet parameter. For Invokes with data, the DataRow(s) sent by the product will be populated. For Invokes without data, the DataRow(s) will be null, but the DataSet size will be set, so the app can know how many rows to import.

public class DataSet {
	private String id;
	private List<DataRow> rows;
	private Long size;
}

Invoke with data

Workflow

The following diagram illustrates the invocation with data flow.

Invoke URL endpoint

When an invocation with data occurs, the request that a CX product will send to an app resembles:

POST <service-base-url><service-invoke-url>
Authorization : Bearer <JWT>
{ 
   "instanceContext":{ 
      "appId":"38281836-4bb4-2cdb-6006-592a98d02da1",
      "appVersion":null,
      "installId":"a28b7df0-2a16-26e1-08e4-a302199208d9",
      "instanceId":"6ea036bb-8cfb-46c5-a826-d001d3a0349b",
      "serviceId":"13023bae-8350-f75f-d953-f7a96d6928b6",
      "tenantId":"6607",
      "productId":"78ffbba2-cf29-a607-c611-3f05e9199a39",
      "maxPushBatchSize":1000,
      "secret":"c4321e9f-19a7-48b2-9796-c21142c709c9-fb24667dde63-4b6a-99af-10b23122a6d0",
      "recordDefinition":{ 
         "inputParameters":[ 
            { 
               "name":"appcloud_row_correlation_id",
               "dataType":"Text",
               "width":40,
               "unique":true,
               "required":true,
               "readOnly":null,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":null,
               "format":null,
               "resources":null
            },
            { 
               "name":"COUNTRY_",
               "dataType":"Text",
               "width":50,
               "unique":null,
               "required":null,
               "readOnly":true,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":null,
               "format":null,
               "resources":null
            },
            { 
               "name":"CITY_",
               "dataType":"Text",
               "width":50,
               "unique":null,
               "required":null,
               "readOnly":true,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":null,
               "format":null,
               "resources":null
            }
         ],
         "outputParameters":[ 
            { 
               "name":"appcloud_row_correlation_id",
               "dataType":"Text",
               "width":40,
               "unique":true,
               "required":true,
               "readOnly":null,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":null,
               "format":null,
               "resources":null
            },
            { 
               "name":"appcloud_row_status",
               "dataType":"Text",
               "width":10,
               "unique":null,
               "required":true,
               "readOnly":null,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":[ 
                  "success",
                  "warning",
                  "failure"
               ],
               "format":null,
               "resources":null
            },
            { 
               "name":"appcloud_row_errormessage",
               "dataType":"Text",
               "width":5120,
               "unique":null,
               "required":null,
               "readOnly":null,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":null,
               "format":null,
               "resources":null
            },
            { 
               "name":"WEATHER1",
               "dataType":"Text",
               "width":500,
               "unique":null,
               "required":null,
               "readOnly":false,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":null,
               "format":null,
               "resources":null
            },
            { 
               "name":"WEATHER1",
               "dataType":"Text",
               "width":500,
               "unique":null,
               "required":null,
               "readOnly":false,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":null,
               "format":null,
               "resources":null
            }
         ]
      },
      "maxBatchSize":1000
   },
   "dataSet":{ 
      "id":"my-test-data-set",
      "rows":[ 
         [ 
            "72a50e37-4dbc-4c97-bb4e-366dd4dcce6d",
            "Canada",
            "Toronto"
         ]
      ],
      "size":null
   },
   "productExportEndpoint":null,
   "productImportEndpoint":{ 
      "url":"http://product.com/import/123/data",
      "method":"POST",
      "headers":{ 
 
      }
   },
   "onCompletionCallbackEndpoint":{ 
      "url":"http://product.com/feedback",
      "method":"POST",
      "headers":{ 
 
      }
   },
   "maxPullPageSize":5,
   "maxPushBatchSize":5
}

Tip: The JWT Token in the Authorization Header is generated by following the Product to App token generation. For more information about this call, including authentication details, see the endpoint API reference.

Looking at the payload, there are a few important things to note:

  • The recordDefinition indicates the structure of the dataSet rows. This also indicates the structure (record definition) that the application should return to the product. See Record definitions for more information.
  • dataSet contains the data the app is being invoked with.
  • productExportEndpoint is null and should be ignored for invocations with data. This is used for invocations without data.
  • productImportEndpoint is the endpoint the product expects the app to respond to with the processed DataRows. Used to push results back into a product by the app based on the maxPushBatchSize. The maxPushBatchSize determines the largest amount of records to push per batch to the productImportEndpoint. Should be a POST or a PUT. Is omitted if product is not interested in the results.
  • onCompletionCallbackEndpoint (optional) should be called upon invocation completion by the app to indicate the results can be retrieved.

Note: During invocation for “Invoke without data” and “Invoke with data (small batch) flow", to support high concurrency and to ensure data processing, there is a delay of 15-20 minutes after the app sends back the “onCompletionCallbackEndpoint” request before enactments are moved to the next stage in the Responsys program.

App's response to the Invoke URL request

RESPONSE NOTES:

  • In the case the invoke was successful, successful will be set to true.
  • After a successful invocation, an invocationId is produced by the app and should be unique. This gets passed to the product so it can determine which result comes from which invocation Id. The actual data for the invocation result is sent directly from the app to the products's productImportEndpoint. This happens asynchronously and independent of AMS.

When a service instance is executed or invoked, apps will respond with a JSON payload that resembles the following.

{
  "successful": true,
  "content": {
    "invocationId": "64a9e8a1-e3cd-4bb4-a4b0-93d55832f90d",
    "instanceId": "6ea036bb-8cfb-46c5-a826-d001d3a0349b"
  },
  "errorMessage": null
}
Property Description
successful A boolean value.
content A mixed type, usually an object or an array.
errorMessage A string which provides details into an issue such as a "401 Unauthorized". When successful is true, errorMessage will be null.

Sample implementation

The sample code app checks the DataRows to determine if the invocation is with or without data. Note that in the payload parameter dataSet, the rows contain values.

// Invoke without data will have DataSet with size, but no rows in DataSet
if (dataSet.getRows() == null || dataSet.getRows().isEmpty()) {
   // Export DataSet rows from Product
   exportProcessAndImportData(invocation, serviceInvocationDto);
} else {
   // Process data and Import data to Product
   processAndImportData(invocation, serviceInvocationDto);
}

It then extracts the DataRows from the invocation and processes each row individually.

The processing basically just gets the weather for the location specified in the dataSet rows and returns the result in the format defined by the recordDefinition (outputParameters). It then sends the results of each processed row to the productImportEndpoint.

Collection<DataRowDTO> rows = dataSet.getRows();
 
if (rows == null || rows.isEmpty()) {
   throw new IllegalArgumentException("Rows cannot be null || empty");
}
 
EndpointDTO productImportEndpoint = serviceInvocationDto.getProductImportEndpoint();
validateEndpoint(productImportEndpoint);
try {
   List<DataRowDTO> processedRows = new ArrayList<>();
 
   rows.forEach(row -> {
      Map<String, Object> input = recordDefinition.translateInputRowToMap(row); // Map <Record Definition Name, Record Definition>
      Map<String, Object> output = recordDefinition.generateOutputRowAsNewMap(input); // Map <Record Definition Name, Record Definition>
      DataRowDTO processedRow = processRow(instanceContext, null, instanceConfiguration,
            input, output);
      processedRows.add(processedRow);
   });
 
   DataSetDTO dataSetDto = new DataSetDTO();
   dataSetDto.setId(dataSet.getId());
   dataSetDto.setRows(processedRows);
   dataSetDto.setSize(Long.valueOf(processedRows.size()));
 
   if (productImportEndpoint != null) {
      importDataToProductImportEndpoint(invocation, serviceInvocationDto, dataSetDto);
   }
} catch (Exception e) {
   log.error("Exception occurred while processing rows for invocationUuid:" + invocationUuid, e);
}

It is important that all callouts from the app to the product include the correct headers to indicate the context of the import. This allows the product to correctly handle the inbound data contents. The following is the our sample headers implementation:

private Map<String, String> createHeadersMap(Invocation invocation, ServiceInvocationCallDTO serviceInvocationDto) {
   InstanceContextDTO instanceContext = serviceInvocationDto.getInstanceContext();
 
   Map<String, String> headers = new HashMap<>();
 
   headers.put("Authorization", "Bearer " + createJwtAuthorizationHeader(invocation, instanceContext));
   headers.put("Content-Type", "application/json");
   headers.put("OMC-ID", instanceContext.getInstanceId());
   headers.put("INVOCATION-ID", invocation.getUuid());
   headers.put("DATASET-ID", invocation.getDataSet().getId());
 
   return headers;
}

(Optional) After the invocation is complete, the app can call the product's onCompletionCallbackEndpoint to indicate that the invocation has been completed.

public void callProductOnCompletionCallbackEndpoint(Invocation invocation, ServiceInvocationCallDTO serviceInvocationDto) {
    Map<String, String> headersMap = createHeadersMap(invocation, serviceInvocationDto);
    Map<String, Object> body = new HashMap<>();
    body.put(STATUS, COMPLETED); // {"status": "COMPLETED"}
    restClient.invoke(onCompletionCallbackEndpoint.getUrl(), onCompletionCallbackEndpoint.getMethod(), headersMap, body);
}

Invoke without data

Workflow

Invoke URL endpoint

When an invocation without data occurs, the request that a CX product will send to an app resembles:

POST <service-base-url><service-invoke-url>
Authorization:Bearer <JWT>
{ 
   "instanceContext":{ 
      "appId":"38281836-4bb4-2cdb-6006-592a98d02da1",
      "appVersion":null,
      "installId":"a28b7df0-2a16-26e1-08e4-a302199208d9",
      "instanceId":"6ea036bb-8cfb-46c5-a826-d001d3a0349b",
      "serviceId":"13023bae-8350-f75f-d953-f7a96d6928b6",
      "tenantId":"6607",
      "productId":"78ffbba2-cf29-a607-c611-3f05e9199a39",
      "maxPushBatchSize":1000,
      "secret":"c4321e9f-19a7-48b2-9796-c21142c709c9-fb24667dde63-4b6a-99af-10b23122a6d0",
      "recordDefinition":{ 
         "inputParameters":[ 
            { 
               "name":"appcloud_row_correlation_id",
               "dataType":"Text",
               "width":40,
               "unique":true,
               "required":true,
               "readOnly":null,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":null,
               "format":null,
               "resources":null
            },
            { 
               "name":"COUNTRY_",
               "dataType":"Text",
               "width":50,
               "unique":null,
               "required":null,
               "readOnly":true,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":null,
               "format":null,
               "resources":null
            },
            { 
               "name":"CITY_",
               "dataType":"Text",
               "width":50,
               "unique":null,
               "required":null,
               "readOnly":true,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":null,
               "format":null,
               "resources":null
            }
         ],
         "outputParameters":[ 
            { 
               "name":"appcloud_row_correlation_id",
               "dataType":"Text",
               "width":40,
               "unique":true,
               "required":true,
               "readOnly":null,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":null,
               "format":null,
               "resources":null
            },
            { 
               "name":"appcloud_row_status",
               "dataType":"Text",
               "width":10,
               "unique":null,
               "required":true,
               "readOnly":null,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":[ 
                  "success",
                  "warning",
                  "failure"
               ],
               "format":null,
               "resources":null
            },
            { 
               "name":"appcloud_row_errormessage",
               "dataType":"Text",
               "width":5120,
               "unique":null,
               "required":null,
               "readOnly":null,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":null,
               "format":null,
               "resources":null
            },
            { 
               "name":"WEATHER1",
               "dataType":"Text",
               "width":500,
               "unique":null,
               "required":null,
               "readOnly":false,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":null,
               "format":null,
               "resources":null
            },
            { 
               "name":"WEATHER1",
               "dataType":"Text",
               "width":500,
               "unique":null,
               "required":null,
               "readOnly":false,
               "minimumValue":null,
               "maximumValue":null,
               "possibleValues":null,
               "format":null,
               "resources":null
            }
         ]
      },
      "maxBatchSize":1000
   },
   "dataSet":{ 
      "id":"my-test-data-set",
      "rows":null,
      "size":6
   },
   "productExportEndpoint":{ 
      "url":"http://product.com/export/123/data",
      "method":"GET",
      "headers":{ 
 
      }
   },
   "productImportEndpoint":{ 
      "url":"http://product.com/import/123/data",
      "method":"POST",
      "headers":{ 
 
      }
   },
   "onCompletionCallbackEndpoint":{ 
      "url":"http://product.com/feedback",
      "method":"PATCH",
      "headers":{ 
 
      }
   },
   "maxPullPageSize":5,
   "maxPushBatchSize":5
}

Tip: The JWT Token in the Authorization Header is generated by following the Product to App token generation. For more information about this call, including authentication details, see the endpoint API reference.

Looking at the payload, there are a few important things to note:

  • The recordDefinition indicates the structure of the DataSet rows. This also indicates the structure (record definition) that the app should return to the product. See Record definitions to learn more.
  • dataSet contains the data that the app is being invoked with (rows is null, but size is declared).
  • productExportEndpoint is the endpoint on the product where the invocation data resides. The app needs to call this endpoint to get the invocation data with the correct REST call specified in the productExportEndpoint. Should be a GET. Used to fetch pages of data from a product and into an app based on the maxPullPageSize. The maxPullPageSize parameter determines the largest pagination size provided by the productExportEndpoint. Is omitted if the invoke is done with data.
  • maxPullPageSize is the largest pagination size provided by the productExportEndpoint.
  • productImportEndpoint is the endpoint the product expects the app to respond to with the processed DataRows. Used to push results back into a product by the app based on the maxPushBatchSize. The maxPushBatchSize determines the largest amount of records to push per batch to the productImportEndpoint. Should be a POST or a PUT. Is omitted if product is not interested in the results. Learn more.
  • onCompletionCallbackEndpoint (optional) should be called upon invocation completion by the app to indicate the results can be retrieved.

Note: During invocation for “Invoke without data” and “Invoke with data (small batch) flow", to support high concurrency and to ensure data processing, there is a delay of 15-20 minutes after the app sends back the “onCompletionCallbackEndpoint” request before enactments are moved to the next stage in the Responsys program.

App's response to the Invoke URL request

RESPONSE NOTES:

  • In the case the invoke was successful, successful will be set to true.
  • After a successful invocation, an invocationId is produced by the app and should be unique. This gets passed to the product so it can determine which result comes from which invocation Id. The actual data for the invocation result is sent directly from the app to the products's productImportEndpoint. This happens asynchronously and independent of AMS. Learn more.

When a service instance is executed or invoked, apps will respond with a JSON payload that resembles the following.

{
  "successful": true,
  "content": {
    "invocationId": "64a9e8a1-e3cd-4bb4-a4b0-93d55832f90d",
    "instanceId": "6ea036bb-8cfb-46c5-a826-d001d3a0349b"
  },
  "errorMessage": null
}
Property Description
successful A boolean value.
content A mixed type, usually an object or an array.
errorMessage A string which provides details into an issue such as a "401 Unauthorized". When successful is true, errorMessage will be null.

Sample implementation

First, the app calculates the number of pages that need to be processed.

private Long getNumberOfPages(Integer pullPageSize, Long dataSetSize) {
   if (dataSetSize != null) {
      if (dataSetSize >= 0) {
         Long pages = dataSetSize / pullPageSize;
         pages += dataSetSize % pullPageSize != 0 ? 1 : 0;
         return pages;
      } else {
         throw new IllegalArgumentException("dataSize < 0");
      }
   } else {
      throw new IllegalArgumentException("dataSize is null");
   }
}

Now, the app needs to callout to the productExportEndpoint to get a page of data. There are two important query parameters in the URL that need to be specified: offset and limit.

  • offset indicates the offset within the Products entire dataset to begin exporting a page of data. It is important that this offset is correctly populated as it can result in duplication of data if the offsets are not correctly calculated. The maxPullPageSize in the payload is used as the exportPageSize as the sample app attempts to grab as much data as it can per export. For example, if there are 100 contacts in the product and the maxPullPageSize is 10, the initial offset should be 0 so the first 10 entries are exported, the second callout should have an offset of 10 so that the 10-19 entries are exported, and so on. The default is 0 (no offset).

    For example: ?offset=10.

  • limit indicates the size of the export that the app wants. So if the limit is set to 10, the product will export a maximum of 10 contacts to the app. The app should not try to export more than the maxPullPageSize specified by the product as that is undefined behaviour. The default limit value is 1000.

    For example: ?limit=10.

private DataDTO exportPage(Invocation invocation, ServiceInvocationCallDTO serviceInvocationDto,
      EndpointDTO productExportEndpoint, Integer exportPageSize, int pageNumber) throws Exception {
   try {
      Map<String, String> headersMap = createHeadersMap(invocation, serviceInvocationDto);
      String url = productExportEndpoint.getUrl() + "?offset=" + (pageNumber * exportPageSize) + "&limit="
            + exportPageSize;
      ResponseEntity<String> responseEntity = restClient.invoke(url, null, productExportEndpoint.getMethod(),
            headersMap);
 
      int statusCode = responseEntity.getStatusCodeValue();
      if (statusCode >= 400) {
         String msg = "Calling Product Export Endpoint: " + productExportEndpoint.getUrl()
               + " resulted in an error status: " + statusCode;
         log.error(msg);
         throw new RuntimeException(msg);
      }
 
      DataDTO data = objectMapper.readValue(responseEntity.getBody().toString(), DataDTO.class);
 
      List<DataRowDTO> rows = data.getDataSet().getRows();
 
      log.info("Successful Product Export from " + productExportEndpoint.getUrl());
      log.debug("Pulled rows[{}]", rows);
      return data;
   } catch (Exception e) {
      log.error("Dataset was not properly exported", e);
      throw e;
   }
}

It is important that all callouts from the app to the product include the correct headers to indicate the context of the import. This allows the product to correctly handle the inbound data contents. The following is the sample code headers implementation:

private Map<String, String> createHeadersMap(Invocation invocation, ServiceInvocationCallDTO serviceInvocationDto) {
   InstanceContextDTO instanceContext = serviceInvocationDto.getInstanceContext();
 
   Map<String, String> headers = new HashMap<>();
 
   headers.put("Authorization", "Bearer " + createJwtAuthorizationHeader(invocation, instanceContext));
   headers.put("Content-Type", "application/json");
   headers.put("OMC-ID", instanceContext.getInstanceId());
   headers.put("INVOCATION-ID", invocation.getUuid());
   headers.put("DATASET-ID", invocation.getDataSet().getId());
 
   return headers;
}

Each page is processed sequentially by the sample. The actual work done inside the main loop is basically the same as Invoke with Data. The only difference is that the DataRows are coming from a product endpoint rather than in the payload passed into the invoke endpoint.

for (int pageNumber = 0; pageNumber < numberOfPages; pageNumber++) {
   List<DataRowDTO> processedRows = new ArrayList<>();
 
   // Export a page of data from Product
   DataDTO exportPageData = exportPage(invocation, serviceInvocationDto, productExportEndpoint, exportPageSize,
         pageNumber);
   List<DataRowDTO> rows = exportPageData.getDataSet().getRows();
 
   // Process a page of data
   rows.forEach(row -> {
      Map<String, Object> input = recordDefinition.translateInputRowToMap(row);
      Map<String, Object> output = recordDefinition.generateOutputRowAsNewMap(input);
      DataRowDTO processedRow = processRow(instanceContext, null, instanceConfiguration,
            input, output);
      processedRows.add(processedRow);
   });
 
   DataSetDTO dataSetDto = new DataSetDTO();
   dataSetDto.setId(dataSet.getId());
   dataSetDto.setRows(processedRows);
   dataSetDto.setSize(Long.valueOf(processedRows.size()));
 
   // Import a page of data
   if (productImportEndpoint != null) {
      importDataToProductImportEndpoint(invocation, serviceInvocationDto, dataSetDto);
   }
}

Finally, the app can optionally call the product's onCompletionCallbackEndpoint to indicate that it has finished with the invocation.

public void callProductOnCompletionCallbackEndpoint(Invocation invocation, ServiceInvocationCallDTO serviceInvocationDto) {
    Map<String, String> headersMap = createHeadersMap(invocation, serviceInvocationDto);
    Map<String, Object> body = new HashMap<>();
    body.put(STATUS, COMPLETED); // {"status": "COMPLETED"}
    restClient.invoke(onCompletionCallbackEndpoint.getUrl(), onCompletionCallbackEndpoint.getMethod(), headersMap, body);
}

Learn more

Developing Apps for CX Apps

Service Lifecycle