Creating Webhooks (v2) in Box-Salesforce Integration
- Oliver Jones
- Oct 15, 2021
- 6 min read
Updated: Oct 19, 2021
Introduction
Webhooks are acknowledgements that are generated from Box during various events. We can use this to automate flows in Salesforce. Generally, a webhook can communicate through callback end points which is captured in Salesforce via Apex Rest.
For example, let’s assume that a user uploads a file in a specific folder which is connected to a Salesforce Case record, using this file upload event (FILE.UPLOADED), then we can use webhooks to receive the acknowledgement that the file has been uploaded now we can update the Case status to “Closed” and add comment as “File has been uploaded”.
Why we need a Webhook?
By default, Box provides a feature called Box Relay which supports some basic events like file/folder created, updated, deleted, etc. But Box Relay does not provide connections outside of Box i.e., to Salesforce and does not support some advanced events. By using webhooks we can overcome these cons.
List of Webhook events
The below is the list of events supported in Webhooks v2, that indicates every event’s name which we will be used as Identifier, when will it be triggered, whether it is supported by file or folder or both.
Event | Triggered | File? | Folder? |
COLLABORATION.CREATED
| A collaboration is created | No | Yes |
COLLABORATION.ACCEPTED | A collaboration has been accepted | No | Yes |
COLLABORATION.REJECTED | A collaboration has been rejected | No | Yes |
COLLABORATION.REMOVED | A collaboration has been removed | No | Yes |
COLLABORATION.UPDATED | A collaboration has been updated. | No | Yes |
COMMENT.CREATED | A comment object is created | Yes | Yes |
COMMENT.UPDATED | A comment object is edited | Yes | Yes |
COMMENT.DELETED | A comment object is removed | Yes | Yes |
FILE.UPLOADED | A file is uploaded to or moved to this folder | No | Yes |
FILE.PREVIEWED | A file is previewed | Yes | Yes |
FILE.DOWNLOADED | A file is downloaded | Yes | Yes |
FILE.TRASHED | A file is moved to the trash | Yes | Yes |
FILE.DELETED | A file is permanently deleted | Yes | Yes |
FILE.RESTORED | A file is restored from the trash | Yes | Yes |
FILE.COPIED | A file is copied | Yes | Yes |
FILE.MOVED | A file is moved from one folder to another | Yes | Yes |
FILE.LOCKED | A file is locked | Yes | Yes |
FILE.UNLOCKED | A file is unlocked | Yes | Yes |
FILE.RENAMED | A file was renamed. | Yes | Yes |
FOLDER.CREATED | A folder is created | No | Yes |
FOLDER.RENAMED | A folder was renamed. | No | Yes |
FOLDER.DOWNLOADED | A folder is downloaded | No | Yes |
FOLDER.RESTORED | A folder is restored from the trash | No | Yes |
FOLDER.DELETED | A folder is permanently removed | No | Yes |
Event | Triggered | File? | Folder? |
FOLDER.COPIED | A copy of a folder is made | No | Yes |
FOLDER.MOVED | A folder is moved to a different folder | No | Yes |
FOLDER.TRASHED | A folder is moved to the trash | No | Yes |
METADATA_INSTANCE.CREATED | A new metadata template instance is associated with a file or folder | Yes | Yes |
METADATA_INSTANCE.UPDATED | An attribute (value) is updated/deleted for an existing metadata template instance associated with a file or folder | Yes | Yes |
METADATA_INSTANCE.DELETED | An existing metadata template instance associated with a file or folder is deleted | Yes | Yes |
SHARED_LINK.DELETED | A shared link was deleted | Yes | Yes |
SHARED_LINK.CREATED | A shared link was created | Yes | Yes |
SHARED_LINK.UPDATED | A shared link was updated | Yes | Yes |
TASK_ASSIGNMENT.CREATED | A task is created | Yes | Yes |
TASK_ASSIGNMENT.UPDATED | A task assignment is changed | Yes | Yes |
WEBHOOK.DELETED | When a webhook is deleted | No | No |
Use Case
Consider the following use case,
Insurance - A custom Object record will create a Box folder every time a record is created
Users uploads files (docx) in Salesforce which will automatically transferred to Box
Users collaborate with others and modify the files in the Box, and once ready, they will convert to PDFs
Users use Meta Data in Box to indicate a file is ready to be picked up Salesforce
Developer creates webhooks using the event METADATA_INSTANCE.UPDATED and transfers the final PDF back to Salesforce from Box
Setup Metadata in Box
Make sure you are logged in as Co Admin (Service Account)
Go to your Box -> select Admin console -> Content -> Metadata -> Create New
“Name your Template” is the metadata template name that we will use to identify in the webhook, so give it a proper name.
Set status to Visible
Name your attribute as “Status”
Choose Format as “Dropdown-Single Select”
Add options as “New”, “In Progress” and “Save to SF”
Click on Save.

Setting up Server Authentication and Custom App with Client Id and Secret

Make sure you logged in as Co Admin (Service Account), and if you can find Dev console in your homepage, click on it otherwise follow the steps below,
Go to your Box -> select Admin console -> select Apps -> go to Custom Apps -> Click on “Developer Website” as highlighted in this image below,
Once you are in the Dev console, click on the “My Apps” and create a new App, choose custom app and choose authentication method as “Server Authentication (Client Credentials Grant)”.
Give a name to the app and click on create app.
Once the app is created, go to configuration tab to get client Id and secret. If you can’t get client secret, you will need to enable 2 factor Authentication. Go to your Box account Settings page or go to this page https://app.box.com/account/developer to enable 2 factor Authentication.
You will need a Google Authenticator App and scan the QR code to authenticate the first time you login here after (only the developer). Now go back to configuration tab and get the Client Id and Secret.
Make note of the “User Id” or “Enterprise Id” in the “General Settings” tab in the custom app. This will be needed to set up the webhook.


Setting up a Webhook URL in Salesforce using Apex REST webservice
Create an Apex class as a REST service with URL Mapping of '/BoxRestAPI/V1/*' and add this class to a public site in Salesforce.
@RestResource(urlMapping='/BoxRestAPI/V1/*')
global class BoxREST {
@HttpPost
global static void postItems() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
try {
//Request Body will be a Blob we need to encode
String responseJson = EncodingUtil.base64Encode(req.requestBody);
Blob responseBlob = EncodingUtil.base64Decode(responseJson);
String responseJson2 = responseBlob.toString();
if(responseJson2 != null) {
responseJson2 = responseJson2.replace('trigger', 'trigger_event');
BoxPayload wrap = (BoxPayload)JSON.deserialize(responseJson2, BoxPayload.class);
if(wrap.source.type == 'file') {
//Get Access Token
String accessToken = getAccessToken();
String parentFolderId = wrap.source.parent.id;
String fileId = wrap.source.id;
String metadataTemplateName = 'ReviewProcess';
String endPoint = 'https://api.box.com/2.0/files/' + fileId + '/metadata/enterprise/' + metadataTemplateName;
HttpResponse response = doCallout(endPoint, '', 'GET', accessToken);
Map<String,String> resp = (Map<String,String>)json.deserialize(response.getBody(), Map<String,String>.class);
//Read File only if the status is updated to Save to SF
if(resp.get('status') == 'Save to SF') {
BoxHandler.readFileContentFromBoxAsync(wrap.source.name, parentFolderId, fileId);
}
}
}
}
catch(Exception e) {
System.debug('Error while calling REST server '+e.getMessage());
}
}
}
Create Box folder upon Record insert
We use Apex Trigger to create Box Folders upon Insert. The important thing to note here is that while creating a Box folder we need to configure the folder to support Webhook in the root folder as well as in its sub folders (cascading). Also, we need to configure which metadata to apply to the folder here.
trigger Insurance_Trigger_AT on Insurance__c (after insert) {
//Check only one record is being inserted
if(Trigger.New.size() == 1) {
try {
Insurance__c insRec = Trigger.New[0];
box.Toolkit boxKit = new box.Toolkit();
//First create object folder, if its already there returns the folder Id
String objectFolderId = boxKit.createObjectFolderForRecordId(insRec.Id);
if(String.isNotBlank(objectFolderId)) {
//Create a Folder in Box within the parent folder Id
String folderId = boxKit.createFolder(insRec.Name, objectFolderId, null);
if(String.isNotBlank(folderId)) {
//Associate the SF record with its Box folder Id
box__FRUP__c assc = boxKit.createFolderAssociation(insRec.Id, folderId);
//Calling method to configure Metadata and Webhook to the fold BoxHandler.configureMetadata(folderId);
BoxHandler.configureWebhook(folderId);
boxKit.commitChanges();
Insurance__c insUpdate = new Insurance__c(Id = insRec.Id);
insUpdate.Box_Folder_Id__c = folderId;
update insUpdate;
}
else {
boxKit.commitChanges();
System.debug(''+boxKit.mostRecentError);
}
}
else {
boxKit.commitChanges();
System.debug(''+boxKit.mostRecentError);
}
}
catch(Exception e) {
System.debug('Error while calling Box server '+e.getMessage());
}
}
}
The BoxHandler Class has the following methods,
Get Access Token:
//Get the Current Access Token
public static String getAccessToken() {
Map<string,string> requestBody = new Map<string,string>();
requestBody.put('client_id', <Client_ID>);
requestBody.put('client_secret', <Client_SECRET>);
requestBody.put('grant_type','client_credentials');
requestBody.put('box_subject_type', <USER_OR_ENTERPRISE>);
requestBody.put('box_subject_id', <USER_ID_OR_ENTERPRISE_ID>);
String endPoint = 'https://api.box.com/oauth2/token';
HttpResponse response = doCallout(endPoint, JSON.serialize(requestBody), 'POST', '');
}
Apply Metadata and Metadata Cascading to Box folder:
//Method that setup Metadata and Metadata Cascading
public static void configureMetadata(String folderId) {
//Get Access Token to proceed for Metadata and Webhook
String accessToken = getAccessToken();
//Setting up Metadata in the folder
JSONGenerator jsGen = JSON.createGenerator(true);
jsGen.writeStartObject();
jsGen.writeStringField('status', 'New');
jsGen.writeEndObject();
//The Metadata Template name we created earlier
String metadataTemplateName = 'ReviewProcess';
String endPoint2 = 'https://api.box.com/2.0/folders/'+ folderId+'/metadata/enterprise/'+metadataTemplateName+'/';
HttpResponse response = doCallout(endPoint2, jsGen.getAsString(), 'POST', accessToken);
//Setting up Metadata Cascading in the folder
Map<String, Object> metadata = new Map<String, Object>{
'folder_id' => folderId,
'scope' => 'enterprise',
'templateKey' => metadataTemplateName
};
//The Metadata Template name we created earli String metadataTemplateName = 'ReviewProcess';
String endPoint3 = 'https://api.box.com/2.0/metadata_cascade_policies/';
HttpResponse response = doCallout(endPoint3, JSON.serialize(metadata), 'POST', accessToken);
}
Apply Webhook v2 to a Box folder:
//Method that setup Webhook
public static void configureWebhook(String folderId) {
//Get Access Token to proceed for Metadata and Webhook
String accessToken = getAccessToken();
JSONGenerator jsGen = JSON.createGenerator(true);
jsGen.writeStartObject();
//Webhook URL is your Site Org URL where you added your REST class + Webhook URL Mapping in the REST class
jsGen.writeStringField('address', 'https://demodev-mstaz-mysite.cs33.force.com /services/apexrest/BoxRestAPI/V1');
List<String> webhook_events = METADATA_INSTANCE.UPDATED.split(',|;')
jsGen.writeStringField('triggers', webhook_events
jsGen.writeFieldName('target');
jsGen.writeStartObject();
jsGen.writeStringField('id', folderId);
jsGen.writeStringField('type', 'folder');
jsGen.writeEndObject();
jsGen.writeEndObject();
String endPoint = 'https://api.box.com/2.0/webhooks/';
HttpResponse response = doCallout(endPoint, jsGen.getAsString(), 'POST', accessToken);
}
Sending uploaded files (docx) from Salesforce to Box
//We use the Apex trigger on Attachment to send the file to Box
trigger Attachment_Trigger_AT on Attachment (after insert) {
//Check only one record is being inserted
if(Trigger.New.size() == 1) {
Attachment attach = Trigger.New[0];
String parentName = attach.parentId.getSObjectType().getDescribe().getName();
//Check if this is the right attachment from our object
if(parentName == 'Insurance__c' && attach.name.endsWithIgnoreCase('.docx')) {
Insurance__c insRec = [SELECT Id,Box_Folder_Id__c FROM Insurance__c WHERE Id =: attach.parentId];
if(String.isNotBlank(insRec.Box_Folder_Id__c)) {
box.Toolkit boxKit = new box.Toolkit();
//Send the attachment to Box
//No need to specify the folder Id here if the parent record is already associated with box folder Id properly, Box folder Id is optional here
String fileId = boxKit.createFileFromAttachment(attach, null, insRec.Box_Folder_Id__c, null);
boxKit.commitChanges();
if(String.isBlank(fileId)) {
System.debug(''+boxKit.mostRecentError);
}
}
}
}
}
Download File from Box to Salesforce Attachment:
//Method that reads file from Box and saves as Attachment in SF
public static void readFileContentFromBoxAsync(String fileName, String parentFolderId, String fileId) {
Box.Toolkit boxKit = new Box.Toolkit();
//Will work only if the SF record is properly associated with a Box folder
Id recId = boxKit.getRecordIdByFolderId(parentFolderId);
boxKit.commitChanges();
String endPoint = 'https://api.box.com/2.0/files/'+fileId+'/content/';
HttpResponse response = doCallout(endPoint, '', 'GET', accessToken);
String fileLocation = String.isNotEmpty(response.getHeader('location'))
? response.getHeader('location')
: response.getHeader('Location');
HttpRequest req = new HttpRequest();
//Replace any spaces in fileLocation with %20
String extFileUrl = fileLocation.replace(' ', '%20');
//Set the end point URL
req.setEndpoint(extFileUrl);
req.setMethod('GET');
req.setHeader('Content-Type', 'application/pdf');
req.setCompressed(true);
req.setTimeout(60000);
HttpResponse res = new Http().send(req);
Blob fileBlob = res.getBodyAsBlob();
insert new Attachment(Name = fileName, body = fileBlob, contentType = 'application/pdf', parentId = recId);
}
Common Callout Method
//Method that reads file from Box and saves as Attachment in SF
public static HttpResponse doCallout(String endpoint, String reqBody, String method, String accessToken) {
HttpRequest request = new HttpRequest();
request.setMethod(method);
request.setEndpoint(endpoint);
if(String.isNotBlank(reqBody)
request.setBody(reqBody);
if(String.isNotBlank(accessToken))
request.setHeader('Authorization','Bearer ' + access_token);
request.setHeader('content-type', 'application/json');
return new Http().send(request);
}
Conclusion:
By using Webhooks v2, we can automate several other processes that Box provides as listed in the webhook events above and supports more features that the v1 webhooks.
Reference links:
Comentários