Apex Example - Webhook Endpoint
Follow along to see how to set up a webhook endpoint in Salesforce. The endpoint can be used to receive events from the Cirrus Insight system and perform logic in Salesforce directly.
Example Code
This example code is for demonstration and not intended to be used directly in production. Check with your Salesforce administrator and IT/compliance teams to ensure you are following the development an security best practices required by your organization.
Overview
The following example walks through the setup of a public Salesforce site that can recieve a webhook event
model from Cirrus Insight. The example code shows how to validate the CirrusInsight-Signature
using a signing key to ensure message authenticity. It also demonstrates how to deserializes the data
payload based on eventType
and uses that data to create a Salesforce Lead if the eventType
is a scheduling.smartscheduler.scheduled
event. Finally, the sample returns a 200 OK
to ensure the event
is marked as successfully delivered.
The steps involved to reproduce this example are:
- Create the
CirrusInsightWebhook
Apex Class - Activate your Public Site
- Create a Webhook Site Url
- Edit the Site’s Public Access Settings
- Get your Webhook URL endpoint
CirrusInsightWebhook
Apex Class
INFO
You must be a Salesforce System Administrator role or have access to the Organization settings to do this. If you have your own Salesforce Developer org, you should be fine.
- Click the top right gear icon and select
Setup
- Use the search to search for
Apex
- Select
Apex Classes
from the left menu - Click the
New
button to create a newApex Class
Add the following code to the Apex Class and make sure to updated the first variable with the secret from the webhook you will be testing. This can be obtained from the Cirrus Insight Developer portal.
@RestResource(urlMapping='/ci-webhook/')
global with sharing class CirrusInsightWebhook {
private static String cirrusInsightSecret = '';
@HttpPost
global static void postWebhook() {
RestRequest request = RestContext.request;
RestResponse response = RestContext.response;
response.addHeader('Content-Type', 'application/json');
// Extract the signature from the request headers
Map<String, String> headers = request.headers;
String signatureHeader = headers.get('CirrusInsight-Signature');
CirrusInsightSignature parsedSignature = parseSignature(signatureHeader);
// Get the json body
String jsonRequest = request.requestBody.toString();
//Validate the signature
if(!isSignatureValid(parsedSignature, jsonRequest, cirrusInsightSecret)) {
throw new IllegalArgumentException('Signature validation failed');
}
// Parse event JSON
JSONParser parser = JSON.createParser(jsonRequest);
CirrusInsightEvent event = (CirrusInsightEvent)parser.readValueAs(CirrusInsightEvent.class);
// Process the data JSON from the request
String dataJson = null;
parser = JSON.createParser(jsonRequest);
while (parser.nextToken() != null) {
if (parser.getCurrentToken() == JSONToken.FIELD_NAME) {
if (parser.getText() == 'data') {
// Advance to the label value.
parser.nextToken();
if (parser.getCurrentToken() == JSONToken.START_OBJECT) {
switch on event.EventType {
when 'scheduling.smartscheduler.scheduled' {
CirrusInsightSchedluingData schedulingData = (CirrusInsightSchedluingData)parser.readValueAs(CirrusInsightSchedluingData.class);
insertLead(schedulingData.MeetingInfo.PrimaryInvitee, schedulingData.MeetingInfo.PrimaryInviteeName, schedulingData.MeetingInfo.CalendarUrl);
response.responseBody = Blob.valueOf(JSON.serialize(schedulingData));
return;
}
when 'developer.webhook.test' {
CirrusInsightTestData testData = (CirrusInsightTestData)parser.readValueAs(CirrusInsightTestData.class);
response.responseBody = Blob.valueOf(JSON.serialize(testData));
return;
}
}
}
}
}
}
response.responseBody = Blob.valueOf('{}');
}
private static void insertLead(String emailAddress, String name, String calendarUrl) {
try {
Lead lead = new Lead(
Email = emailAddress,
LeadSource = calendarUrl
);
List<String> nameParts = name.split(' ');
if (nameParts.size() == 2) {
lead.FirstName = nameParts[0];
lead.LastName = nameParts[1];
}
else {
lead.LastName = name;
}
List<string> emailParts = emailAddress.split('@');
if(emailParts.size() == 2) {
lead.Company = emailParts[1];
}
else {
lead.Company = emailAddress;
}
insert lead;
}
catch(DmlException e) {
System.debug('An unexpected error has occurred: ' + e.getMessage());
}
}
private static Boolean isSignatureValid(CirrusInsightSignature signature, String payload, String cirrusInsightSecret) {
// Reconstruct the payload
String signedPayload = signature.timeStamp + '.' + payload;
// Get bytes for the payload and for the signing secret
Blob bSignedPayload = blob.valueOf(signedPayload);
Blob bSecret = blob.valueOf(cirrusInsightSecret);
// Compute the HMAC and compare
Blob hmac = Crypto.generateMac('HMACSHA256', bSignedPayload, bSecret);
String payloadHmac = EncodingUtil.base64Encode(hmac);
return signature.signature == payloadHmac;
}
private static CirrusInsightSignature parseSignature(String header) {
if(String.isBlank(header)) {
throw new IllegalArgumentException('Signature Header is missing');
}
// Split the signature into its two parts (t and sig)
List<String> headerParts = header.split(',', 2);
if(headerParts.size() != 2) {
throw new IllegalArgumentException('Signature Header has incorrect format');
}
// Split the t into its key and value
List<String> timestampParts = headerParts[0].split('=', 2);
if(timestampParts.size() != 2) {
throw new IllegalArgumentException('Signature Header has incorrect format');
}
if(timestampParts[0] != 't' || String.isBlank(timestampParts[1])) {
throw new IllegalArgumentException('Signature Header has incorrect format');
}
// Split the sig into its key and value
List<String> signatureParts = headerParts[1].split('=', 2);
if(signatureParts.size() != 2) {
throw new IllegalArgumentException('Signature Header has incorrect format');
}
if(signatureParts[0] != 'sig' || String.isBlank(signatureParts[1])) {
throw new IllegalArgumentException('Signature Header has incorrect format');
}
return new CirrusInsightSignature(timestampParts[1],signatureParts[1]);
}
private class CirrusInsightEvent {
public CirrusInsightEvent() { }
public String OrganizationGuid { get; set; }
public String EventId { get; set; }
public String EventType { get; set; }
public String WebhookId { get; set; }
public String EndpointUrl { get; set; }
public String EventTimestamp { get; set; }
public Integer DeliveryAttempt { get; set; }
public String DeliveryTimestamp { get; set; }
}
private class CirrusInsightTestData {
public CirrusInsightTestData() { }
public String WebhookEndpoint { get; set; }
public string Username { get; set; }
}
private class CirrusInsightSchedluingData {
public CirrusInsightSchedluingData() { }
public String Id { get; set; }
public String OrganizationId { get; set; }
public String EmailMeetingId { get; set; }
public ScheduledMeetingOwner MeetingOwner { get; set; }
public String ZoomMeetingId { get; set; }
public Boolean IsCanceled { get; set; }
public String MeetingStart { get; set; }
public String MeetingEnd { get; set; }
public ScheduledMeetingInfo MeetingInfo { get; set; }
}
private class ScheduledMeetingOwner {
public ScheduledMeetingOwner() { }
public String UserId { get; set; }
public String Username { get; set; }
public String FirstName { get; set; }
public String LastName { get; set; }
}
private class ScheduledMeetingInfo {
public ScheduledMeetingInfo() { }
public String CalendarUrl { get; set; }
public String OrganizationPath { get; set; }
public String CalendarPath { get; set; }
public String Location { get; set; }
public String MeetingName { get; set; }
public String OptionalInvitees { get; set; }
public String PrimaryInvitee { get; set; }
public String PrimaryInviteeName { get; set; }
public String ExistingEmailMeetingId { get; set; }
public List<ScheduledMeetingSurveyQuestion> SurveyQuestions { get; set; }
public List<ScheduledMeetingFormValue> FormValues { get; set; }
}
public class ScheduledMeetingSurveyQuestion {
public ScheduledMeetingSurveyQuestion() { }
public String Answer { get; set; }
public Boolean IsRequired { get; set; }
public String Question { get; set; }
}
public class ScheduledMeetingFormValue {
public ScheduledMeetingFormValue() { }
public String Question { get; set; }
public String AnswerText { get; set; }
}
private class CirrusInsightSignature {
public CirrusInsightSignature(String ts, String sig) {
timeStamp = ts;
signature = sig;
}
public String timeStamp { get; private set; }
public String signature { get; private set; }
}
}
Save the Apex Class.
Activate a Public Site
Use the Settings
search bar to search for Sites
If you want to customize the domain name, you can first go the Domains
page, and edit the record labeled My Domain
to pick a customized prefix, but this step is optional, not required. Once you are happy with your site prefix, check the box and activate your developer organization’s public domain. You will be prompted with a warning that you must acknowledge that once your Site domain name is set, it cannot be modified.
Permanent
Once you set your organization's public site domain, it can not be changed. If you are making this change in a production Salesforce organization, proceed with caution, and confirm changes before commiting them.
Webhook Site Url
At the bottom of the page, add a new Site and fill in the required fields, setting the Default Web Address path to webhooks
.
Public Access Settings
Next add the CirrusInsightWebhook
Apex class to the Public Access Setting
s of the newly created Webhook
site. This can be done by clicking the Public Access Settings
button at the top of the Webhooks Site Details screen. Then scroll down the page until you reach the Enabled Apex Class Access section, and click the Edit button. Add the CirrusInsightWebhook
to the Enabled Apex Classes
list and click Save. It should appear in the table.
Webhook URL endpoint
You can access your webhook URL by using the following format:
Public Site domain (eg: *-dev-ed.develop.my.salesforce-sites.com - make sure to use your own domain from step 2 above)
+
Site Path: /webhooks
+
Apex Class Path: /services/apexrest/ci-webhook/
For example, a completed URL may look like:
https://*-dev-ed.develop.my.salesforce-sites.com/webhooks/services/apexrest/ci-webhook/
This URL can now be used to set up your webhook endpoint in the Cirrus Insight Developer portal.