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 {} }
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.

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.

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.
- User: Salesforce 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.

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

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





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 Test 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') {