This post will walk you through a process of building a new connector function for SAS Customer Intelligence 360 (CI360) connector. We will use Amazon Web Services (AWS) environment in this example and will include steps to deploy your code in AWS environment, but logic of processing CI360 events and data applies regardless of the environment you choose.
This post is aimed to more technical resources, who will take part in building the actual integration. The goal is to provide a step-by-step guide to building a connector function and demonstrate the ease of doing so. By the end of this article, you will understand how to build a simple connector function, which can then be configured for use in CI360 and used with CI360 External System Tasks to communicate with 3rd party services.
About SAS CI360 connectors and events
SAS Customer Intelligence 360 connector framework allows customers to easily integrate additional external systems and further leverage and enhance CI360 customer journey orchestration capabilities.
We will not go into details of how connectors are configured in CI360 in this article, or how to configure an External System Task which would use a connector. You can find more information about these in CI360 documentation:
https://go.documentation.sas.com/doc/en/cintcdc/production.a/cintag/ch-connectors-about.htm
https://go.documentation.sas.com/doc/en/cintcdc/production.a/cintug/ext-about-task.htm
The most important thing to know and understand when building connector functions, is that the connector framework in CI360 is event-based framework, as is the whole CI360 platform. When External System Task is invoked within CI360 it generates an outbound event which is then processed by the connector framework and passed on to an external API, or in this case, our connector function.
The outbound event is a JSON object that contains all the information we need for execution, and we need to call our target system (and more).
In turn, that means that what our connector function will do is parse the JSON object it receives when invoked, and then use specific data elements from the full event object that it needs to call the external system of our choice.
Writing the code
With that introduction, let’s get to building a brand-new connector function. As an example, we will build a bridge to an SMS service provided by Twilio. If you’d like to test this service, and you don’t have an existing account with Twilio, you can get a free developer account.
Also, since we will be creating this function on AWS platform, you will need an AWS account if you want to follow the example, and you can register for one for free as well.
Create a new AWS Lambda function
Navigate to Lambda section of AWS console, then use “Create Function”. We will create a new function from scratch and name it twilioSMSEventHandler and select Python as our runtime environment language. Of course, you can use a language of your choice when building your own functions.
For the purposes of this example, you can use the default setting letting AWS create a new role with basic permissions, but in practice a good rule is to create a dedicated role for your connector functions especially if they will share access to similar set of services (for example CloudWatch, Secret Manager and Dynamo).
For this example, we will store any configuration items, including credentials, using Lambda environment variables. In practice, sensitive information should be stored using platform services like AWS Secret Manager, or another secure method if using a different platform.
Parsing event JSON object
Entry point into a Python Lambda function is lambda_handler Python function. By definition, this function has two input parameters, event and context. You can learn more about Lambda function handler at: https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html
For the purposes of our connector function, we are interested only in event object passed to the function, and specifically the “body” element of the event object. Since we are invoking this Lambda function as a result of a call to AWS API Gateway, “body” element of the event object will contain the POST body of the HTTP request being made to the API gateway by CI360 connector framework.
Let’s start with simply parsing the body of the request into a JSON object. You can edit the code directly in AWS console, in the Code tab of your Lambda function:
def lambda_handler(event, context): if event is not None and event["body"] is not None: body = json.loads(event["body"]) print("Received event:", body["eventName"]) return { 'statusCode': 200, 'body': json.dumps('OK') }
Now we have the body of the CI360 event in our own “body” object and have printed out the incoming event name for visibility. For now, we will simply use default response from the function, a simple HTTP 200 OK response back to the CI360.
Processing the event
Now that we have our CI360 event parsed, let’s get the necessary information out of this object and use it to create the call to Twilio API in order to send the SMS message. I will include a sample event below for reference.
To keep the lamda_handler function simple, we will place all the logic specific to processing the incoming event into a separate function called process_event and call that function from the main lamnda_handler function passing in the event body:
def lambda_handler(event, context): if event is not None and event["body"] is not None: body = json.loads(event["body"]) print("Received event:", body["eventName"]) process_event(body) return { 'statusCode': 200, 'body': json.dumps('OK') } def process_event(event_body): print("identityId:", event_body["identityId"], "tenant_id:", event_body["externalTenantId"]) # get creative creative_content = event_body["impression"]["creativeContent"] # get phone mobile_phone = event_body["properties"]["mobile"] return
We will not use identity and tenant IDs in this example, but we are printing them out to show you some of the information carried in the CI360 event object.
Contents of the creative attached to the External System Task being executed in CI360 are stored in “creativeContent” element of the “impression” object within the event. Also, we will get the mobile phone we need to send the message to from the event object. In this example, we are assuming the phone number is being sent via a Form Submit event in CI360, and thus it is part of “properties” object.
With these two data elements, we have nearly everything we need to send an SMS message through Twilio API. The remaining pieces we need are the sender number, Twilio account ID and authentication token and of course Twilio API URL to call. We will get all of these from our configuration variables stored as Lambda environment variables.
Before we can do that, we need to import a few modules we will use in our function (as usual, we’re doing this at the very beginning of our source code). Also, we’ll initialize a HTTP resource we’ll use to make the API call as well as a few global variables that will store our configuration data items:
import os import json import urllib3 # Get the service resource http = urllib3.PoolManager() # Initialize global variables twilio_api_url = os.environ['twilio_api_url'] twilio_account_sid = os.environ['twilio_account_sid'] twilio_auth_token = os.environ['twilio_auth_token'] default_sender = os.environ['default_sender']
Configuration items need to be added as environment variables to our Lambda function. In AWS Console, go to the new Lambda function you just created, and add the variables under the “Environment Variables” section of “Configuration” tab. Populate the environment variables with appropriate values for API URL, credentials and your originating phone number (default_sender). API URL will contain your Twilio account SID and be similar to:
https://api.twilio.com/2010-04-01/Accounts/myAccountSID/Messages.json
With configuration items in place, we are ready to call the Twilio API. To make the code cleaner, we will first create an API request object within our process_event function and then pass it into a separate function that will be responsible for making the HTTP call to Twilio API:
def process_event(event_body): print("identityId:", event_body["identityId"], "tenant_id:", event_body["externalTenantId"]) # get creative message_text = event_body["impression"]["creativeContent"] # get phone mobile_phone = event_body["properties"]["mobile"] msg_req = { "From": default_sender, "To": mobile_phone, "Body": message_text } http_status = call_twilio_api(msg_req) return def call_twilio_api(msg_req): auth_string = twilio_account_sid + ":" + twilio_auth_token req_headers = urllib3.util.make_headers(basic_auth=auth_string) r = http.request('POST', twilio_api_url, fields = msg_req, headers = req_headers) print("Response Status:", r.status, "Body:", r.data) return r.status
And its simplest form, this is your new connector function. For reference, the complete function code for Lambda connector function should look like this:
import os import json import urllib3 # Get the service resource http = urllib3.PoolManager() # Initialize global variables twilio_api_url = os.environ['twilio_api_url'] twilio_account_sid = os.environ[' twilio_account_sid'] twilio_auth_token = os.environ['twilio_auth_token'] default_sender = os.environ['default_sender'] def lambda_handler(event, context): if event is not None and event["body"] is not None: body = json.loads(event["body"]) print("Received event:", body["eventName"]) process_event(body) return { 'statusCode': 200, 'body': json.dumps('OK') } def process_event(event_body): print("identityId:", event_body["identityId"], "tenant_id:", event_body["externalTenantId"]) # get creative message_text = event_body["impression"]["creativeContent"] # get phone mobile_phone = event_body["properties"]["mobile"] msg_req = { "From": default_sender, "To": mobile_phone, "Body": message_text } http_status = call_twilio_api(msg_req) return def call_twilio_api(msg_req): auth_string = twilio_account_sid + ":" + twilio_auth_token req_headers = urllib3.util.make_headers(basic_auth=auth_string) r = http.request('POST', twilio_api_url, fields = msg_req, headers = req_headers) print("Response Status:", r.status, "Body:", r.data) return r.status
Notice that this function does not really contain any error handling and it relies on environment variables for storing sensitive information, but we are keeping it simple to illustrate the processing of CI360 events, and we’ll cover those elements in a future article. As a best practice, connector function should return an error code (a non-200 response) if the call to external system fails so that CI360 is notified of the failure.
In AWS console, code should look like this:
Exposing the function through API Gateway
Before we can invoke our new connector function from CI360, we need to expose it as a web service using AWS API Gateway.
Create a new API within AWS API Gateway service. Create a HTTP API and add integration to Lambda function we just created. Finally name it twilioConnectorApi.
We will only use POST method when invoking our connector function, so you can change the routing from ANY to POST:
On the final screen, simply leave stage name as default and set to auto-deploy. We will not discuss features of API Gateway in more detail in this post. However, you can read more about exposing Lambda function using API Gateway here:
https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html
https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway-tutorial.html
When you create the API, the final screen will give you details you need to invoke your connector function:
The Invoke URL is a base URL for your API gateway, so the actual endpoint we will invoke and use to configure our connector in CI360 will be:
https://afacg6w0g5.execute-api.us-east-1.amazonaws.com/twilioSMSEventHandler
A connector endpoint configured in CI360 will look like this:
Conclusion
You have seen how building a connector function involves writing just a handful of lines of code and a few simple steps to deploy the function when using cloud infrastructure like AWS. This same exercise can be done in any other cloud platform using a language of choice or can be deployed as traditional web service in the customer owned infrastructure.
This particular integration can even be accomplished fully using only connector configuration within CI360, by specifying custom request payload for the connector endpoint and formatting it to Twilio needs, but we have chosen this very simple example to highlight the steps required to write and deploy a connector function.
Sample Event Object
For reference, you will find a partial sample event JSON object below. Actual event object contains even more information, and type of information carried depends on the type of event itself, but we are limiting the data included to select fields and child objects for space and readability. Even with this abbreviated version of event object, we are only using a small portion of included data, which you can see referenced in code. But we are hoping that having this sample included will make it easier to follow the example above.
{ "guid": "d27f3313-c283-41f0-b2ce-807f823dc28f", "eventName": "Outgoing_SMS_Dynamic_External_CRM_v1", "customName": null, "eventType": "outboundSystem", "sessionId": "2987", "channelId": "2987", "channelType": "external", "date": { "generatedTimestamp": 1666274457377, "utcOffset": null }, "externalTenantId": "115886462700013adcd822d0", "internalTenantId": 1029, "identityId": "805bfeca-d9e3-378f-a533-bf5597199652", "properties": { "subject_id": "2987", "externalCode": "TSK_168", "channel_user_type": "subject_id", "parent_event": "external", "mobile": "15552121234", "customer_name": "John Smith", "event_datetime_utc": "1666274457377", "parent_eventname": "external-crm" }, "outboundProperties": null, "identity": { "identityId": "805bfeca-d9e3-378f-a533-bf5597199652", "identityType": null }, "obfuscateIpAddress": null, "eventCategory": "unifiedAndEngage", "eventSource": null, "customGroupName": null, "parentEventUid": "1a3d1b60-b1c6-4478-9137-5db4b6f1fce2", "activity": { "abPathAssignmentId": null, "activityId": "967d11c1-918b-43c5-92af-6960d6a2661e", "activityName": null, "activityNodeId": null, "activityTaskType": "EVENT", "iaTagValue": "90a92e7d-695f-4bff-a3c0-448779889792", "activityTimebox": null, "activityCancelGoals": null }, "contactResponse": { "contactResponseCode": null, "contactResponseTag": null, "responseTrackingCode": "3469e066-1499-4c9c-be57-4cf10318ee01", "responseType": null, "responseValue": null }, "contentChange": null, "impression": { "controlGroup": null, "creativeId": "24bb3c95-1814-44a6-9062-97c921f62880", "creativeVersionId": "d4uR8KF1R5l2KZiSHpK_17oJqW4Nvfmc", "creativeContent": "John Smith, we have a great 36 month low interest loan program for clients with excellent credit.", "goalEvent": null, "goalId": null, "imprintId": "0", "isControlGroup": false, "taskId": "643faaa4-e3fd-4dcb-bfff-afcd707058dc", "taskName": null, "taskType": null, "taskVersionId": "IebUmaAlGshdqO_jE8xa2NEDt9G6mIvK" }, "trigger": { "triggerEventId": "90a92e7d-695f-4bff-a3c0-448779889792", "executionContext": null, "expressionFilterTotal": null, "expressionFrequency": null, "expressionId": null }, "ruleVersion": null }
Superb post, just opens up countless possibilities in integrating to martech and channels ecosystem.
Thanks!
With the release of Custom Task Types last year, it is also possible to create dedicated task types for better user experience, for example an SMS (or Twilio SMS) task type.
When custom task types are used, outbound data specifies data elements needed for channel activation, for example a mobile phone number, and a marketing user can then specify where that values should come from - a customer profile, recent event or somewhere else.
Any data elements captured as part of Outbound data on task Delivery tab are passed to connector function as part of "outboundProperties" JSON element. For example:
"properties": { "subject_id": "2987", "externalCode": "TSK_168", "channel_user_type": "subject_id", "parent_event": "external", "event_datetime_utc": "1666274457377", "parent_eventname": "external-crm" }, "outboundProperties": { "properties": { "mobile": "+19195551212" }, }, "identity": { "identityId": "805bfeca-d9e3-378f-a533-bf5597199652", "identityType": null },
With this in mind, in the example above, one would reference mobile phone number as:
mobile_phone = event_body["outboundProperties"]["properties"]["mobile"]
More information about setting up and using custom task types can be found in my post:
Tutorial: SAS Customer Intelligence 360 Custom Task Types
Want to review SAS CI360? G2 is offering a gift card or charitable donation for each accepted review. Use this link to opt out of receiving anything of value for your review.
Listen to the Reimagine Marketing podcast
Assess your marketing efforts with a free tool
SAS Customer Intelligence Learning Subscription (login required)
Compatibility notice re: SAS 9.4M8 (TS1M8) or later
SAS' Peter Ansbacher shows you how to use the dashboard in SAS Customer Intelligence 360 for better results.
Find more tutorials on the SAS Users YouTube channel.
Want to review SAS CI360? G2 is offering a gift card or charitable donation for each accepted review. Use this link to opt out of receiving anything of value for your review.
Listen to the Reimagine Marketing podcast
Assess your marketing efforts with a free tool
SAS Customer Intelligence Learning Subscription (login required)
Compatibility notice re: SAS 9.4M8 (TS1M8) or later