Skip to content

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 eventmodel 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:

  1. Create the CirrusInsightWebhook Apex Class
  2. Activate your Public Site
  3. Create a Webhook Site Url
  4. Edit the Site’s Public Access Settings
  5. 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 new Apex Class

New Apex 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.

java
@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.

Salesforce Public Site

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 Settings 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.

Public Access Enabled

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.

Raleigh, NC a Cirruspath, Inc. company