Conversations Inbox | Integration with Salesforce

Welcome to the tutorial on how to integrate Salesforce with tyntec Conversations Inbox. In this integration, we will connect Salesforce campaigns and contacts to tyntec Conversations Inbox. 

This integration can be implemented as follows, but it may also be expanded to include a bot builder such as Cognigy (for inspiration, check out a use case video at the bottom of this tutorial). 

You will need

  • A tyntec Conversations Inbox account.
  • A Conversations Inbox API key.
  • A Conversations Inbox WhatsApp Channel JID.
  • A Salesforce account.

Please note that although the steps described in this tutorial are based on the Salesforce Lightning Experience, it’s not necessary to use the Lightning Experience to connect Salesforce with tyntec Conversations Inbox.

Step One: Connect Salesforce to tyntec Conversations Inbox

1. Register a new remote site

    1. Click Setup.
    2. Find Security - Remote Site Settings.

    3. Create a new Remote Site.
    4. Insert Remote Site URL: https://api.cmd.tyntec.com.
    5. Click
Save.

More information about Remote Sites can be found here.

2. Create a named credential

    1. Click Setup.
    2. Find Security - Named Credentials.

    3. Create a new Named Credential.
    4. 
Insert:

  • Label: Conversations Inbox API
  • Name: Conversations_Inbox_API
  • URL: https://api.cmd.tyntec.com
  • Identity type: Per User
  • Authentication protocol: Password Authentication
  • Generate Authorization Header: NO
  • Allow Merge Fields in HTTP Header: YES

    5. Click Save.

More information about Name Credentials can be found here.

3. Set up user authentication

    1. Click on your profile avatar.
    2. Select Settings.
    3. Select Authentication Settings for External Systems.
    4. Click New.
    5. Insert:

  • External System Definition: Named Credential.
  • Named Credential: Conversations Inbox API.
  • UserSalesforce username.
  • Authentication protocol: Password Authentication.
  • Username: Username is a mandatory parameter for Salesforce, but tyntec doesn’t use it. We use  a token for authorization. So we recommend completing the field with a  placeholder, eg. "none” or “unused".
  • Password: Your Conversations Inbox API key.

    6. Click Save.

More information about Authentication Settings can be found here.

4. Create a custom Campaigns field

    1. Select Setup - Object Manager -  Campaign - Fields & Relationships.
   
2. Add a new Field.
    3. Select
Data Type - Text.

    4. Set Field Name: ConversationsInboxAPiId
    5. Set
Length: Since the JID is a long character string, a length of at least 100 characters is recommended.
    6. 
Enable
External ID.   
    7. Save your changes.

5. Create a custom Contacts field

In the same way as before, create a Contacts field:

    1. Select Setup - Object Manager -  Contact - Fields & Relationships.
    
2. Add a new field.
    3. Select
Data Type - Text.
    4. Set
Field Name: ConversationsInboxAPiId
    5. Set
Length: Since the JID is a long character string, a length of at least 100 characters is recommended.
    6. 
Enable
External ID.
    7. Save your changes.

6. Define an Apex Class

The Apex Class is created in a Salesforce test environment. Keep in mind the code coverage requirement of >75% for deployment and if necessary write a test class. To make it simpler for you, we’ve written a test class you can use, see the More? section on the bottom.

    1. Go to Setup - Custom Code - Apex Classes.
   
2. Add a new
Apex Class.
    3. Insert this code into the Apex Class:

public class ConversationsInboxApi {
  public static final String CHANNEL = 'TODO';
 
 @future(callout=true)
 public static void uploadCampaign(String campaignId) {
   Campaign campaign = [SELECT Name, ConversationsInboxApiId__c FROM Campaign Where Id = :campaignId];
 
   JSONGenerator gen = JSON.createGenerator(true);
   gen.writeStartObject();
   gen.writeStringField('name', 'Campaign-' + campaign.Name);
   gen.writeEndObject();
 
   HttpRequest request = new HttpRequest();
   request.setMethod('POST');
   request.setEndPoint('callout:Conversations_Inbox_API/v3/channels/' + ConversationsInboxApi.CHANNEL + '/lists');
   request.setHeader('Authorization', 'Bearer {!$Credential.Password}');
   request.setHeader('Accept', 'application/json');
   request.setHeader('Content-Type', 'application/json');
   request.setBody(gen.getAsString());
   HttpResponse response = new HTTP().send(request);
 
   if (response.getStatusCode() != 201) {
     throw new ConversationsInboxApiException('Upload failed');
   }
 
   String listJid;
   JSONParser parser = JSON.createParser(response.getBody());
   while (parser.nextToken() != null) {
     if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) &&
         (parser.getText() == 'jid')) {
           parser.nextToken();
           listJid = parser.getText();
     }
   }
 
   campaign.ConversationsInboxApiId__c = listJid;
   update campaign;
 }
 
 @future(callout=true)
 public static void uploadCampaignMember(String campaignMemberId) {
   CampaignMember campaignMember = [SELECT CampaignId, ContactId FROM CampaignMember Where Id = :campaignMemberId];
   Campaign campaign = [SELECT ConversationsInboxApiId__c, Name FROM Campaign Where Id = :campaignMember.CampaignId];
   Contact contact = [SELECT MobilePhone, Name, ConversationsInboxApiId__c FROM Contact Where Id = :campaignMember.ContactId];
 
   try {
       contact.ConversationsInboxApiId__c = ConversationsInboxApi.createContact(contact, campaign);
   } catch (ConversationsInboxApiException e) {}
 
   HttpRequest request = new HttpRequest();
   request.setMethod('POST');
   request.setEndPoint('callout:Conversations_Inbox_API/v3/channels/' + ConversationsInboxApi.CHANNEL + '/lists/' + campaign.ConversationsInboxApiId__c + '/participants/' + contact.ConversationsInboxApiId__c);
   request.setHeader('Authorization', 'Bearer {!$Credential.Password}');
   HttpResponse response = new HTTP().send(request);
 
   update contact;
 
   if (response.getStatusCode() != 200) {
     throw new ConversationsInboxApiException('Upload failed');
   }
 }
  private static String createContact(Contact contact, Campaign campaign) {
   String jid = contact.MobilePhone + '@whatsapp.eazy.im';
 
   JSONGenerator gen = JSON.createGenerator(true);
   gen.writeStartObject();
   gen.writeStringField('jid', jid);
   gen.writeStringField('name', contact.Name);
   gen.writeStringField('reference', 'Campaign-' + campaign.Name);
   gen.writeEndObject();
 
   HttpRequest request = new HttpRequest();
   request.setMethod('POST');
   request.setEndPoint('callout:Conversations_Inbox_API/v3/channels/' + ConversationsInboxApi.CHANNEL + '/contacts');
   request.setHeader('Authorization', 'Bearer {!$Credential.Password}');
   request.setHeader('Content-Type', 'application/json');
   request.setBody(gen.getAsString());
   HttpResponse response = new HTTP().send(request);
 
   if (response.getStatusCode() != 201) {
     throw new ConversationsInboxApiException('Upload failed');
   }
 
   return jid;
 }
  public class ConversationsInboxApiException extends Exception {}
}

    4. Set your Conversations Inbox WhatsApp Channel JID:

static final String CHANNEL = 'TODO'; //fill in your WhatsApp Channel JID instead of "TODO"

More information about the JID format can be found here.
If you don't have a JID or don't know how to get one, please contact our support at support@tyntec.com.

    5. Save your changes.

7. Define your Campaign's Apex Trigger

    1. Go to Setup - Custom Code - Apex Triggers.
    
2. Add a new
Apex Trigger.

    3. Insert this code into the Apex Trigger:

trigger UploadCompaignToConversationsInboxApi on Campaign (after insert) {
 for(Campaign campaign : Trigger.New) {
   ConversationsInboxApi.uploadCampaign(campaign.Id);
 }
}

    8. Save your changes.

8. Define a Campaign Member's Apex Trigger

In the same way as before, create a Campaign Member's Apex Trigger:

    1. Go to Setup - Custom Code - Apex Triggers.
    
2. Add a new
Apex Trigger.
    3. Insert this code into the
Apex Trigger:

trigger UploadCampaignMemberToConversationsInboxApi on CampaignMember (after insert) {
 for(CampaignMember campaignMember : Trigger.New) {
   ConversationsInboxApi.uploadCampaignMember(campaignMember.Id);
 }
}

    4. Save your changes.

Step Two: Test your app

1. Create and set up a new Salesforce Campaign

    1. Log in or create a Salesforce account.
    2. Select Campaigns and click New Campaign.

    3. Fill in any information for the new Campaign.
    4. Once the Campaign has been created, add Contacts to it.

    5. Click Add Contacts.
    6. Select the
Contacts you want to add to the Campaign.

    7.  Select your current Campaign and click Submit.

    8. Open tyntec Conversations Inbox.
    9. Select
Contacts - Lists.
    10. The Campaign should be automatically imported, with contacts.

More?

Setup your Salesforce Apex Class in production environment

    1. Create a Salesforce Sandbox. Here is the official guide on how to do it.
    2. Create the Apex Class. See Step 6: Define an Apex Class for how to create an Apex Class. 
    3. Create a T
est Class
for the Apex Class. Here is the official tutorial on how to do it.

To make it simpler for you, we’ve prepared a test class you can use, below.

@isTest 
private class ConversationsInboxApiTestClass {
    @isTest
    static void testUploadCampaign() {
        Test.setMock(HttpCalloutMock.class, new TestUploadCampaignHttpCalloutMock());
        Campaign campaign = new Campaign(Name='Test Campaign');
 
        Test.startTest();
        insert campaign;
        Test.stopTest();
 
        campaign = [SELECT ConversationsInboxApiId__c FROM Campaign WHERE Id =:campaign.Id];
        System.assertEquals('test@list.eazy.im', campaign.ConversationsInboxApiId__c);
    }
 
    @isTest
    static void testUploadCampaignError() {
        Test.setMock(HttpCalloutMock.class, new TestUploadCampaignErrorHttpCalloutMock());
        Campaign campaign = new Campaign(Name='Test Campaign');
        ConversationsInboxApi.ConversationsInboxApiException thrownException = null;
 
        try {
            Test.startTest();
            insert campaign;
            Test.stopTest();
        } catch (ConversationsInboxApi.ConversationsInboxApiException e) {
            thrownException = e;
        }
 
        campaign = [SELECT ConversationsInboxApiId__c FROM Campaign WHERE Id =:campaign.Id];
        System.assertEquals(null, campaign.ConversationsInboxApiId__c);
        System.assertNotEquals(null, thrownException);
    }
 
    @isTest
    static void testUploadCampaignMember() {
        TestUploadCampaignMemberHttpCalloutMock mock = new TestUploadCampaignMemberHttpCalloutMock();
        Test.setMock(HttpCalloutMock.class, mock);
        Contact contact = new Contact(LastName='Test', FirstName='Contact', MobilePhone='420999000000');
        insert contact;
        Campaign campaign = new Campaign(Name='Test Campaign');
        insert campaign;
        CampaignMember campaignMember = new CampaignMember(ContactId=contact.Id, CampaignId=campaign.Id);
 
        Test.startTest();
        insert campaignMember;
        Test.stopTest();
 
        contact = [SELECT ConversationsInboxApiId__c FROM Contact WHERE Id =:contact.Id];
        System.assertEquals('420999000000@whatsapp.eazy.im', contact.ConversationsInboxApiId__c);
        System.assertEquals(true, mock.contactAddedToList);
    }
 
    @isTest
    static void testUploadCampaignMemberContactExists() {
        TestUploadCampaignMemberContactExistsHttpCalloutMock mock = new TestUploadCampaignMemberContactExistsHttpCalloutMock();
        Test.setMock(HttpCalloutMock.class, mock);
        Contact contact = new Contact(LastName='Test', FirstName='Contact', MobilePhone='420999000000', ConversationsInboxApiId__c='unchanged@whatsapp.eazy.im');
        insert contact;
        Campaign campaign = new Campaign(Name='Test Campaign');
        insert campaign;
        CampaignMember campaignMember = new CampaignMember(ContactId=contact.Id, CampaignId=campaign.Id);
 
        Test.startTest();
        insert campaignMember;
        Test.stopTest();
 
        contact = [SELECT ConversationsInboxApiId__c FROM Contact WHERE Id =:contact.Id];
        System.assertEquals('unchanged@whatsapp.eazy.im', contact.ConversationsInboxApiId__c);
        System.assertEquals(true, mock.contactAddedToList);
    }
 
    @isTest
    static void testUploadCampaignMemberError() {
        TestUploadCampaignMemberErrorHttpCalloutMock mock = new TestUploadCampaignMemberErrorHttpCalloutMock();
        Test.setMock(HttpCalloutMock.class, mock);
        Contact contact = new Contact(LastName='Test', FirstName='Contact', MobilePhone='420999000000');
        insert contact;
        Campaign campaign = new Campaign(Name='Test Campaign');
        insert campaign;
        CampaignMember campaignMember = new CampaignMember(ContactId=contact.Id, CampaignId=campaign.Id);
        ConversationsInboxApi.ConversationsInboxApiException thrownException = null;
 
        try {
            Test.startTest();
            insert campaignMember;
            Test.stopTest();
        } catch (ConversationsInboxApi.ConversationsInboxApiException e) {
            thrownException = e;
        }
 
        System.assertEquals(true, mock.contactAddedToList);
        System.assertNotEquals(null, thrownException);
    }
 
    private class TestUploadCampaignHttpCalloutMock implements HttpCalloutMock {
        public HttpResponse respond(HttpRequest request) {
            System.assertEquals('POST', request.getMethod());
            System.assertEquals('callout:Conversations_Inbox_API/v3/channels/' + ConversationsInboxApi.CHANNEL + '/lists', request.getEndpoint());
            System.assertEquals('Bearer {!$Credential.Password}', request.getHeader('Authorization'));
 
            HttpResponse response = new HttpResponse();
            response.setStatusCode(201);
            response.setHeader('Content-Type', 'application/json');
            response.setBody('{"jid":"test@list.eazy.im"}');
            return response;
        }
    }
 
    private class TestUploadCampaignErrorHttpCalloutMock implements HttpCalloutMock {
        public HttpResponse respond(HttpRequest request) {
            System.assertEquals('POST', request.getMethod());
            System.assertEquals('callout:Conversations_Inbox_API/v3/channels/' + ConversationsInboxApi.CHANNEL + '/lists', request.getEndpoint());
            System.assertEquals('Bearer {!$Credential.Password}', request.getHeader('Authorization'));
 
            HttpResponse response = new HttpResponse();
            response.setStatusCode(500);
            response.setHeader('Content-Type', 'plain/text');
            response.setBody('A test error');
            return response;
        }
    }
 
    private class TestUploadCampaignMemberHttpCalloutMock implements HttpCalloutMock {
        public boolean contactAddedToList = false;
 
        public HttpResponse respond(HttpRequest request) {
            System.assertEquals('Bearer {!$Credential.Password}', request.getHeader('Authorization'));
 
            if (request.getMethod() == 'POST' && request.getEndpoint() == 'callout:Conversations_Inbox_API/v3/channels/' + ConversationsInboxApi.CHANNEL + '/lists') {
                HttpResponse response = new HttpResponse();
                response.setStatusCode(201);
                response.setHeader('Content-Type', 'application/json');
                response.setBody('{"jid":"test@list.eazy.im"}');
                return response;
            } else if (request.getMethod() == 'POST' && request.getEndpoint() == 'callout:Conversations_Inbox_API/v3/channels/' + ConversationsInboxApi.CHANNEL + '/contacts') {
                HttpResponse response = new HttpResponse();
                response.setStatusCode(201);
                return response;
            } else if (request.getMethod() == 'POST' && request.getEndpoint() == 'callout:Conversations_Inbox_API/v3/channels/' + ConversationsInboxApi.CHANNEL + '/lists/test@list.eazy.im/participants/420999000000@whatsapp.eazy.im') {
                contactAddedToList = true;
                HttpResponse response = new HttpResponse();
                response.setStatusCode(200);
                return response;
            }
            System.assert(false, 'Unexpected request: ' + request.getMethod() + ' ' + request.getEndpoint());
            return null;
        }
    }
 
    private class TestUploadCampaignMemberContactExistsHttpCalloutMock implements HttpCalloutMock {
        public boolean contactAddedToList = false;
 
        public HttpResponse respond(HttpRequest request) {
            System.assertEquals('Bearer {!$Credential.Password}', request.getHeader('Authorization'));
 
            if (request.getMethod() == 'POST' && request.getEndpoint() == 'callout:Conversations_Inbox_API/v3/channels/' + ConversationsInboxApi.CHANNEL + '/lists') {
                HttpResponse response = new HttpResponse();
                response.setStatusCode(201);
                response.setHeader('Content-Type', 'application/json');
                response.setBody('{"jid":"test@list.eazy.im"}');
                return response;
            } else if (request.getMethod() == 'POST' && request.getEndpoint() == 'callout:Conversations_Inbox_API/v3/channels/' + ConversationsInboxApi.CHANNEL + '/contacts') {
                HttpResponse response = new HttpResponse();
                response.setStatusCode(400);
                return response;
            } else if (request.getMethod() == 'POST' && request.getEndpoint() == 'callout:Conversations_Inbox_API/v3/channels/' + ConversationsInboxApi.CHANNEL + '/lists/test@list.eazy.im/participants/unchanged@whatsapp.eazy.im') {
                contactAddedToList = true;
                HttpResponse response = new HttpResponse();
                response.setStatusCode(200);
                return response;
            }
            System.assert(false, 'Unexpected request: ' + request.getMethod() + ' ' + request.getEndpoint());
            return null;
        }
    }
 
    private class TestUploadCampaignMemberErrorHttpCalloutMock implements HttpCalloutMock {
        public boolean contactAddedToList = false;
 
        public HttpResponse respond(HttpRequest request) {
            System.assertEquals('Bearer {!$Credential.Password}', request.getHeader('Authorization'));
 
            if (request.getMethod() == 'POST' && request.getEndpoint() == 'callout:Conversations_Inbox_API/v3/channels/' + ConversationsInboxApi.CHANNEL + '/lists') {
                HttpResponse response = new HttpResponse();
                response.setStatusCode(201);
                response.setHeader('Content-Type', 'application/json');
                response.setBody('{"jid":"test@list.eazy.im"}');
                return response;
            } else if (request.getMethod() == 'POST' && request.getEndpoint() == 'callout:Conversations_Inbox_API/v3/channels/' + ConversationsInboxApi.CHANNEL + '/contacts') {
                HttpResponse response = new HttpResponse();
                response.setStatusCode(201);
                return response;
            } else if (request.getMethod() == 'POST' && request.getEndpoint() == 'callout:Conversations_Inbox_API/v3/channels/' + ConversationsInboxApi.CHANNEL + '/lists/test@list.eazy.im/participants/420999000000@whatsapp.eazy.im') {

That's it. You can use your Apex class in the Salesforce production environment!

 

Use Case Demo

See how you can use the integration to trigger a customer outreach campaign: