How to create Salesforce webhooks in Node.js

How to create Salesforce webhooks in Node.js

What I am trying to create

I am going to configure a webhook for which a web request will be made if any changes are made to salesforce records by any of your app users. These webhooks will be created programatically so they can be integrated within another application

Example — Lets say one of your app users created a new salesforce Lead, then the webhook will be requested with all the details for newly created salesforce Lead.

Steps

Creating webhooks in salesforce requires the following steps

  • Salesforce Oauth for getting access token.

  • creating Apex code for webhook

  • creating the webhook using salesforce tooling api

  • whitelisting the domain of your webhook with salesforce.

Pre-requisites

Things that are required for follwing this article

Limitations of this approach.

Step 1 — Salesforce Oauth

(skip if already done)

  • create salesforce oauth url. It will be of the following format. `https://login.salesforce.com/services/oauth2/authorize?client_id=${SALESFORCE_CLIENT_ID}&redirect_uri=${redirectUrl}&response_type=code&prompt=consent&scope=${SALESFORCE_SCOPES}`

  • For scopes , more details are here — https://help.salesforce.com/articleView?id=remoteaccess_oauth_tokens_scopes.htm&type=5

  • I have found that you will need these scopes in most cases but they could be different for your usecase

    refresh_token,offline_access, api, id,profile,email,address,phone
    
  • redirecting to this url in your application UI will open a salesforce page commonly refered to as consent screen.

  • Here once user has authenticated , the browser will redirect to your redirect Url with a code=xxxxxxxx query string in the url .

  • your application server’s endpoint for the redirect url will be requested with the code = xxxxxxx and inside of this endpoint you will request the salesforce servers for getting accesstoken using the code = xxxxx that you have recieved.

    const querystring = require('querystring');
    
    await axios.post(
      'https://login.salesforce.com/services/oauth2/token',
      querystring.stringify({
        grant_type: 'authorization_code',
        code,
        client_secret: SALESFORCE_CLIENT_SECRET,
        redirect_uri: redirectUrl,
        client_id: SALESFORCE_CLIENT_ID,
      }),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
      }
    );
    
    • Here is the sample code . Salesforce api requires you to provide data as form-url-encoded so I have used querystring package for it.

    • The oauth is pretty straightforward and similar to any other oauth provider — google, facebook.

    • Once you have the access token and refresh token you will have to save refresh token in database .

    • the access token will expire after some time so you will have to use refresh token to get a new access token.

  • Step -2 Creating Apex code for salesforce webhooks

  • Webhooks cannot be created via salesforce UI . Salesforce has a programming language APEX which can be used to create webhooks —
    https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_intro_what_is_apex.htm

  • I need the data from user info endpoint so I will make a request to it . Here access token from oauth step will be needed.

    await axios.get(`https://login.salesforce.com/services/oauth2/userinfo`, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });
    
    • the response will have type
```typescript
type UserInfoResponse = {
  urls: {
   metadata: string;
   rest: string
  }
};
```
  • In the response.urls I need to specify the version number https://ap16.salesforce.com/services/data/v{version}/
    so
    I will change the url to https://ap16.salesforce.com/services/data/v50/
    This
    url will be used to make request to salesforce servers.
    Salesforce response does not have api version in it.

  • The above step is required as the salesforce urls can be different for different users. Its like different users have different salesforce server through which salesforce api can be used (may be).

  • We need to create an Apex trigger in salesforce terms. Here is the Apex that needs to be created

    pawasTriggerTest on Lead(after insert) {
       System.debug('trigger hit');
       String url = 'https://radiant- thicket.herokuapp.com/salesforceCallback';
       String content = Webhook.jsonContent(Trigger.new, Trigger.old);
       System.debug('about to call webhook');
       Webhook.callout(url, content);
       System.debug('after webhook call');
     }
    
    • Here pawasTriggerTest is the name for Apex trigger .

    • The trigger is being created on salesforce Lead which is a salesforce resource type.

    • after insert specifies the event on which the trigger needs to be executed. There are other events like before insert etc.

    • Webhook.callout is how we make web requests in salesforce apex .

    • Here in the url we are specifying the url of our webhook.

    • Trigger.new contains data for the newly created Lead . This data will be present in the body of webhook post request.

    • System.debug are useful to test the code. the logs can be checked using the salesforce dashboard ui.

  • Step -3 registering webhook with the salesforce tooling API

  • we need to make a post request to tooling api with the access token

  • create a webhook class in apex


const getWebhookApexClass = (): string => {
  return `public class Webhook 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 content = '{\"new\": ' + newObjects + ', \"old\": ' + oldObjects + ', \"userId\": ' + userId + '}';      return content;  } @future(callout=true)  public static void callout(String url, String content) {     if (Test.isRunningTest()) {       Test.setMock(HttpCalloutMock.class, new Webhook());     }      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);  }}`;
};


await axios.post(
          `${SALESFORCE_INSTANCE_URL}/services/data/v49.0/tooling/sobjects/ApexClass`,
          {
            ApiVersion: 49,
            Name: `Webhook`,
            Body: getWebhookApexClass(),
          },
          {
            headers: {
              'Content-Type': 'application/json',
              Authorization: `Bearer ${accessToken}`,
            },
          },
        );
view rawcreateApexClass.js hosted with ❤ by GitHub
  • the tooling api url can be obtained by using the url from user info as the base url and then appending tooling/sobjects/ApexTriggers to it.
const payload = {
  ApiVersion: 49,
  Name: 'dynamicallyCreatedWebook',
  Body: "trigger pawasTriggerTest on Account (after insert)    {\nSystem.debug('trigger hit');\nString url = 'https://radiant-thicket-36896.herokuapp.com/sfcb';\nString content = Webhook.jsonContent(Trigger.new, Trigger.old);\nSystem.debug('about to call webhook');\nWebhook.callout(url, content);\nSystem.debug('after webhook call');\n}",
  TableEnumOrId: 'Account',
};

const toolingApiUrl = 'https://ap16.salesforce.com/services/data/v49.0/tooling/sobjects/ApexTrigger';

await axios.post(toolingApiUrl, payload, {
  headers: {
    'Content-Type': 'application/json',
  },
});
  • here name is the name for webhook in salesforce.

  • the Body property contains the apex trigger as a string .

  • Api version property is used for specifying the version for apex trigger .

  • TableEnumOrId is the name of salesforce resource.

  • You would want to save the response somewhere in the database for unregistering the webhook later.

  • To check it you can goto salesforce dashboard and click top right go to setup from gear icon.

  • on salesforce dashboard setup left menu has customCode > Apex triggers.
    these options may change in future.

Step -4 Whitelist your webhook’s DNS with salesforce

  • Whitelisting the DNS is in salesforce is not supported through Rest. But needs to be done using salesforce SOAP api.

  • I need the base url for soap api similar to what I did before the apex register.

  • So I will hit the user info endpoint and use the response.urls.metadata property and replace the api version. you can scroll up to check the userinfo code again.

  • I will use the easy-soap-request npm package to make the request .

  • the accesstoken is required for this step as well.

const soapRequest = require('easy-soap-request');
// this url is obtained from userInfo endpoint in salesforce
// userInfo > urls > metadata
const salesforceSoapUrl =
  'https://ap16.salesforce.com/services/Soap/m/49.0/00D2w000009Mrh6';
const sampleHeaders = {
  'Content-Type': 'text/xml;',
  soapAction: 'RemoteSiteSetting',
};
// I am updating the RemoteSiteSetting resource for salesforce api here
//here url property is the webhook url
//session Id is the accesstoken
//fullName is the name for this remote site in salesforce
const xml = `<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<env:Header>
  <urn:SessionHeader xmlns:urn="http://soap.sforce.com/2006/04/metadata">
    <urn:sessionId>YOUR_ACCESS_TOKEN_GOES_HERE</urn:sessionId>
  </urn:SessionHeader>
</env:Header>
<env:Body>
  <createMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
    <metadata xsi:type="RemoteSiteSetting">
      <fullName>node_soap_remote_setting_test</fullName>
      <isActive>true</isActive>
      <url>https://radiant-thicket.herokuapp.com/salesforceCallback</url>
    </metadata>
  </createMetadata>
</env:Body>
</env:Envelope>`;

(async () => {
  const { response } = await soapRequest({
    url: salesforceSoapUrl,
    headers: sampleHeaders,
    xml: xml,
  });
  const { headers, body, statusCode } = response;
  console.log(headers);
  console.log(body);
  console.log(statusCode);
})();
  • Here the main part is the envelope . It contains the data required for request as xml.

  • To check it you can goto salesforce dashboard and click top right go to setup from gear icon.

  • click security > RemoteSiteSettings . Here the new DNS should be present by the name it was registered .

I found this github repository https://github.com/jamesward/salesforce-webhook-creator which has helped me.

You can use useparagon.com to get develop salesforce triggers . They handle all the mess for you.