Salesforce Apex Trigger Support

For your Integration, you will likely want to track additions/changes to any records in your tenants Salesforce instance. You can do this by easily leveraging Salesforce's Apex Code and Fusebit!

Salesforce doesn't have a built-in Webhooks support, instead they require developers to write custom Apex Code that enables this functionality and then manually configure their Tenants Salesforce accounts.

To do this, you will need to do the following:

  • Set up POST endpoint in your Integration to receive incoming messages
  • After Installation, configure your Tenants Salesforce account to allow it to send outgoing messages to your endpoint
  • After Installation, generate the required Apex Code to trigger a notification and install it in your Tenant's Salesforce

Additionally, for some Salesforce Accounts, you may not be able to do this all via API, so as a back up you may need to ask your Tenant to manually install & configure the Apex Code. We will go through that process in this guide as well.

Set up a POST Endpoint

πŸ“˜

Native Fusebit Support for Webhooks is coming soon!

Currently, Fusebit does not support incoming Webhooks from Salesforce natively, but this is actively being designed and worked on right now. In the meantime, you can set up an endpoint in your Integration to receive incoming messages from Salesforce.

From the incoming message, you will likely want to do two things:

  1. Identify which Salesforce Instance the message belonged to
  2. Perform your Business Logic for the Tenant

Here is a Sample Fusebit Endpoint you will add to your Integration to receive messages:

router.post('/api/webhooks', async (ctx) => {
  // Get the Opportunity Id of the new record from the Trigger
  const opportunityId = ctx.req.body.new[0].Id;

  // First get a list of all Installs
  //https://developer.fusebit.io/reference/listinstalls
  const installs = await integration.service.listInstalls(ctx);

  // Find Installation tied to the UrlInstance
  const instanceUrl = ctx.req.body.instanceUrl;
  const filteredInstalls = installs.items.filter((item) => `webhook/${instanceUrl}` in item.tags)

  // Get TenantID from Filtered Install
  const tenantId = filteredInstalls[0].tags['fusebit.tenantId'];
  const salesforceClient = await integration.tenant.getSdkByTenant(ctx, connectorName, tenantId);

  // Perform your Business Logic
  const opportunities = await salesforceClient.query(`SELECT Id, Name FROM Opportunity WHERE Id = '${opportunityId}'`);
  console.log(opportunities)
});

Enable Remote Site Setting

Now, as part of the installation process for your Tenant, you need to perform the following actions:

  • Enable RemoteSite Setting to whitelist the Domain for your Integration endpoint
  • Install the Apex Code in your Salesforce Tenant's Website
// After Installation - You will want to FIRST enable RemoteSiteSetting for your Tenants
router.post('/api/tenant/:tenantId/enableRemoteSiteSetting', integration.middleware.authorizeUser('install:get'), async (ctx) => {
    const salesforceClient = await integration.tenant.getSdkByTenant(ctx, connectorName, ctx.params.tenantId);
    try {
        const remoteSite = await salesforceClient.metadata.create("RemoteSiteSetting", {
            "fullName": "FusebitWebhook",
            // Disable Protocol Security = TRUE to allow HTTP messaging
            "disableProtocolSecurity": "true",
            "url": "https://api.us-west-1.on.fusebit.io",
            "isActive": "true",
        });

        return remoteSite;
    } catch (e) {
        console.log(`Remote Site URL probably existed because this failed with the ${e.message}`);
    }
});

Generate the Required Apex Code

To set up a webhook that fires off when a Salesforce Object is created/updated, you need a minimum of three files installed in the Salesforce Organization of your Tenant.

  • Apex Trigger: Executes on Changes to a Salesforce Object
  • Apex Class: Gets called by the Trigger to send an HTTP Message
  • Apex Test: Required by Salesforce, you must have a minimum of 75% coverage to be able to distribute your package successfully.
trigger TriggerFusebitWebhooksSample on Opportunity (after insert, after update) {
      
      String url = 'YOUR_WEBHOOK_URL';
      String content = FusebitWebhooksSample.jsonContent(Trigger.new, Trigger.old);
      FusebitWebhooksSample.callout(url, content);
       
}
public class FusebitWebhooksSample implements HttpCalloutMock {

    public static HttpRequest request;
    public static HttpResponse response;

    public HTTPResponse respond(HTTPRequest req) {
        request = req;
        response = new HttpResponse();
        response.setStatusCode(200);
        return response;
    }

    public static String jsonContent(List<Object> triggerNew, List<Object> triggerOld) {
        String newObjects = '[]';
        if (triggerNew != null) {
            newObjects = JSON.serialize(triggerNew);
        }

        String oldObjects = '[]';
        if (triggerOld != null) {
            oldObjects = JSON.serialize(triggerOld);
        }

        String userId = JSON.serialize(UserInfo.getUserId());
        String instanceUrl = JSON.serialize(EncodingUtil.urlEncode(URL.getSalesforceBaseUrl().toExternalForm(), 'UTF-8'));
 
        String content = '{"new": ' + newObjects + ', "old": ' + oldObjects + ', "userId": ' + userId + ', "instanceUrl": ' + instanceUrl + '}';
        
        return content;
    }

    @future(callout=true)
    public static void callout(String url, String content) {

        if (Test.isRunningTest()) {
            Test.setMock(HttpCalloutMock.class, new FusebitWebhooksSample());
        }

        Http h = new Http();

        HttpRequest req = new HttpRequest();
        req.setEndpoint(url);
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(content);

        h.send(req);
    }

}
@isTest
  public class FusebitWebhooksWebhookTriggerTest {

      static SObject mock(String sobjectName) {
          SObjectType t = Schema.getGlobalDescribe().get(sobjectName);

          SObject o = t.newSobject();

          Map<String, Schema.SObjectField> m = t.getDescribe().fields.getMap();

          for (String fieldName : m.keySet()) {
              DescribeFieldResult f = m.get(fieldName).getDescribe();
              if (!f.isNillable() && f.isCreateable() && !f.isDefaultedOnCreate()) {
                  if (f.getType() == DisplayType.Boolean) {
                      o.put(f.getName(), false);
                  }
                  else if (f.getType() == DisplayType.Currency) {
                      o.put(f.getName(), 0);
                  }
                  else if (f.getType() == DisplayType.Date) {
                      o.put(f.getName(), Date.today());
                  }
                  else if (f.getType() == DisplayType.DateTime) {
                      o.put(f.getName(), System.now());
                  }
                  else if (f.getType() == DisplayType.Double) {
                      o.put(f.getName(), 0.0);
                  }
                  else if (f.getType() == DisplayType.Email) {
                      o.put(f.getName(), '[email protected]');
                  }
                  else if (f.getType() == DisplayType.Integer) {
                      o.put(f.getName(), 0);
                  }
                  else if (f.getType() == DisplayType.Percent) {
                      o.put(f.getName(), 0);
                  }
                  else if (f.getType() == DisplayType.Phone) {
                      o.put(f.getName(), '555-555-1212');
                  }
                  else if (f.getType() == DisplayType.String) {
                      o.put(f.getName(), 'TEST');
                  }
                  else if (f.getType() == DisplayType.TextArea) {
                      o.put(f.getName(), 'TEST');
                  }
                  else if (f.getType() == DisplayType.Time) {
                      o.put(f.getName(), System.now().time());
                  }
                  else if (f.getType() == DisplayType.URL) {
                      o.put(f.getName(), 'http://foo.com');
                  }
                  else if (f.getType() == DisplayType.PickList) {
                      o.put(f.getName(), f.getPicklistValues()[0].getValue());
                  }
              }
          }
          return o;
      }

      @isTest static void testTrigger() {
          SObject o = mock('Opportunity');

          Test.startTest();
          insert o;
          update o;
          delete o;
          Test.stopTest();

          System.assertEquals(200, Webhook.response.getStatusCode());
          System.assertEquals('YOUR_WEBHOOK_URL', Webhook.request.getEndpoint());

          if (Webhook.request != null) {
              Map<String, Object> jsonResponse = (Map<String, Object>) JSON.deserializeUntyped(Webhook.request.getBody());
              System.assertNotEquals(null, jsonResponse.get('userId'));
          }
      }

  }

You can programatically generate the Apex Code required to point to your Endpoint in Fusebit and also install it on your Tenants Salesforce Instance.

Here are three endpoints which should generate the Apex Code for you properly.

// After the Remote Site Setting has been Confirmed - Install the Apex Class
router.post('/api/tenant/:tenantId/installApexClass', integration.middleware.authorizeUser('install:get'), async (ctx) => {
  const salesforceClient = await integration.tenant.getSdkByTenant(ctx, connectorName, ctx.params.tenantId);

  const webhookApexClass = `
  public class FusebitWebhooksSample implements HttpCalloutMock {

    public static HttpRequest request;
    public static HttpResponse response;

    public HTTPResponse respond(HTTPRequest req) {
        request = req;
        response = new HttpResponse();
        response.setStatusCode(200);
        return response;
    }

    public static String jsonContent(List<Object> triggerNew, List<Object> triggerOld) {
        String newObjects = '[]';
        if (triggerNew != null) {
            newObjects = JSON.serialize(triggerNew);
        }

        String oldObjects = '[]';
        if (triggerOld != null) {
            oldObjects = JSON.serialize(triggerOld);
        }

        String userId = JSON.serialize(UserInfo.getUserId());
        String instanceUrl = JSON.serialize(EncodingUtil.urlEncode(URL.getSalesforceBaseUrl().toExternalForm(), 'UTF-8'));
       
 
        String content = '{"new": ' + newObjects + ', "old": ' + oldObjects + ', "userId": ' + userId + ', "instanceUrl": ' + instanceUrl + '}';
        
        return content;
    }

    @future(callout=true)
    public static void callout(String url, String content) {

        if (Test.isRunningTest()) {
            Test.setMock(HttpCalloutMock.class, new FusebitWebhooksSample());
        }

        Http h = new Http();

        HttpRequest req = new HttpRequest();
        req.setEndpoint(url);
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(content);

        h.send(req);
    }

  }
`
  const create_class = await salesforceClient.tooling.sobject('ApexClass').create({
    body: webhookApexClass
  }, function (err, res) {
    if (err) { return console.error(err); }
    console.log(res);
  });

  return create_class;

});
// After the ApexClass has been Installed Successfully - Install the Apex Trigger.
router.post('/api/tenant/:tenantId/installApexTrigger', integration.middleware.authorizeUser('install:get'), async (ctx) => {
  const salesforceClient = await integration.tenant.getSdkByTenant(ctx, connectorName, ctx.params.tenantId);
  const webhookEndpoint = `${ctx.state.params.baseUrl}/api/webhooks`;

  const webhookApexTrigger = `
  trigger TriggerFusebitWebhooksSample on Opportunity (after insert, after update) {
      
      String url = '${webhookEndpoint}';
      String content = FusebitWebhooksSample.jsonContent(Trigger.new, Trigger.old);
      FusebitWebhooksSample.callout(url, content);
       
}
`
  const create_class = await salesforceClient.tooling.sobject('ApexTrigger').create({
    name: 'TriggerFusebitWebhooksSample',
    tableEnumOrId: 'Opportunity',
    body: webhookApexTrigger
  }, function (err, res) {
    if (err) { return console.error(err); }
    console.log(res);
  });

  return create_class;

});
// After the ApexClass Trigger - Install the Apex Class Test.
router.post('/api/tenant/:tenantId/installApexClassTest', integration.middleware.authorizeUser('install:get'), async (ctx) => {
  const salesforceClient = await integration.tenant.getSdkByTenant(ctx, connectorName, ctx.params.tenantId);
  const webhookEndpoint = `${ctx.state.params.baseUrl}/api/webhooks`;

const webhookApexTestClass = `
  @isTest
  public class FusebitWebhooksWebhookTriggerTest {

      static SObject mock(String sobjectName) {
          SObjectType t = Schema.getGlobalDescribe().get(sobjectName);

          SObject o = t.newSobject();

          Map<String, Schema.SObjectField> m = t.getDescribe().fields.getMap();

          for (String fieldName : m.keySet()) {
              DescribeFieldResult f = m.get(fieldName).getDescribe();
              if (!f.isNillable() && f.isCreateable() && !f.isDefaultedOnCreate()) {
                  if (f.getType() == DisplayType.Boolean) {
                      o.put(f.getName(), false);
                  }
                  else if (f.getType() == DisplayType.Currency) {
                      o.put(f.getName(), 0);
                  }
                  else if (f.getType() == DisplayType.Date) {
                      o.put(f.getName(), Date.today());
                  }
                  else if (f.getType() == DisplayType.DateTime) {
                      o.put(f.getName(), System.now());
                  }
                  else if (f.getType() == DisplayType.Double) {
                      o.put(f.getName(), 0.0);
                  }
                  else if (f.getType() == DisplayType.Email) {
                      o.put(f.getName(), '[email protected]');
                  }
                  else if (f.getType() == DisplayType.Integer) {
                      o.put(f.getName(), 0);
                  }
                  else if (f.getType() == DisplayType.Percent) {
                      o.put(f.getName(), 0);
                  }
                  else if (f.getType() == DisplayType.Phone) {
                      o.put(f.getName(), '555-555-1212');
                  }
                  else if (f.getType() == DisplayType.String) {
                      o.put(f.getName(), 'TEST');
                  }
                  else if (f.getType() == DisplayType.TextArea) {
                      o.put(f.getName(), 'TEST');
                  }
                  else if (f.getType() == DisplayType.Time) {
                      o.put(f.getName(), System.now().time());
                  }
                  else if (f.getType() == DisplayType.URL) {
                      o.put(f.getName(), 'http://foo.com');
                  }
                  else if (f.getType() == DisplayType.PickList) {
                      o.put(f.getName(), f.getPicklistValues()[0].getValue());
                  }
              }
          }
          return o;
      }

      @isTest static void testTrigger() {
          SObject o = mock('Opportunity');

          Test.startTest();
          insert o;
          update o;
          delete o;
          Test.stopTest();

          System.assertEquals(200, FusebitWebhooksSample.response.getStatusCode());
          System.assertEquals('${webhookEndpoint}', FusebitWebhooksSample.request.getEndpoint());

          if (FusebitWebhooksSample.request != null) {
              Map<String, Object> jsonResponse = (Map<String, Object>) JSON.deserializeUntyped(FusebitWebhooksSample.request.getBody());
              System.assertNotEquals(null, jsonResponse.get('userId'));
          }
      }

  }
`

  const create_class = await salesforceClient.tooling.sobject('ApexClass').create({
    body: webhookApexTestClass
  }, function (err, res) {
    if (err) { return console.error(err); }
    console.log(res);
  });

  return create_class;

});

❗️

Manual Installation Path

If, for any reason, installing via API fails on any of the classes, you will have to ask the Tenant to manually install the package themselves by providing them with a link.

Create and Deploy a Salesforce Package

As mentioned earlier, you will have to provide a backup path to allow your Tenants to install the Apex Package directly on to their Salesforce Account.

πŸ“˜

Sample Package w/ All Required Apex Code

To get you started without having to do any Salesforce code yourself, we created a "Sample Webhooks Trigger" Package with all the required classes & a trigger that fires each time there is a new/update on an Opportunity object.

Add them directly to your Developer Salesforce Instance by installing this sample Package.

Note: This is a sample package without a pre-defined URL, you will need to update the classes in Salesforce to point to the correct Fusebit Endpoint in your Integration. Once the package has been installed, you'll be able to see the Apex code by searching for Apex Classes & Apex Triggers or navigating to Custom Code in the Setup Menu to update it.

**Note: You must update the url in the Apex Trigger & the Apex Test Class for it to work with your Integration****Note: You must update the url in the Apex Trigger & the Apex Test Class for it to work with your Integration**

Note: You must update the url in the Apex Trigger & the Apex Test Class for it to work with your Integration

Once you have updated the Apex Code to include the URL of your Connector's Webhook. You must create a new Package and add this Apex Code to it before you can distribute it to your Tenants.

To create a package, navigate to Package Manager and select 'New'.

Next, add your Apex Code to this newly created Package.

Finally, when you're done - hit Upload. This will run the tests and then generate an Install URL that you will need to store. This is the link to the package installation that you will display to your Tenants if loading via API fails.

πŸ‘

Done!

Once you have set that flow up properly, your Fusebit Integration will start receiving notifications from Salesforce Triggers immediately!


Did this page help you?