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
A salesforce Oauth app. you can create one using instructions — https://trailhead.salesforce.com/en/content/learn/v/modules/sfdx_travis_ci/sfdx_travis_ci_connected_app
A deployed web server that can handle request from salesforce . you can create one for free at — https://devcenter.heroku.com/articles/getting-started-with-nodejs
Limitations of this approach.
creating apex is a salesforce sandbox org feature. These need to be deployed to salesforce production org after creating them on a sandbox org.
I did not find a way to do the deployment programatically.
what are salesforce Production orgs? https://developer.salesforce.com/forums/?id=906F00000009498IAA
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.htmI 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.