Google Apps Script - Processing Webhooks with Custom Logic
Overview
Learn how to process webhooks generated by the Shipwell platform without standing up your own webhook processing infrastructure using Google Apps Script. This integration guide will walk you through creating a webhook processing URL endpoint with custom processing logic and without needing to install anything.
Discover how easy it is to define and execute custom logic to respond to webhook event payloads generated in the Shipwell platform. With this guide, you’ll learn to process specific event types without the need for building or managing any infrastructure. Powerful and seamless custom integrations in record time.
Steps
Scenario
This example scenario uses Google Apps Script process webhook event payloads that are generated in the Shipwell platform when shipments are created to add a TARP
accessorial to a shipment if the equipment type is FLATBED
.
To accomplish this task the following three steps are taken:
- Create a Google Apps Script function to inspect a webhook request body for the equipment type and accessorials of any shipments as they are created.
- Deploy the function as a web app.
- Create a webhook configuration using the webhook URL endpoint created in the previous step.
What is Google Apps Script?
Google Apps Script is a web-based rapid development platform to create business applications that runs on Google Cloud servers. The applications and logic are written in JavaScript and can integrate with Google Workspace apps and data(Google Sheets, Drive, etc.) a while also providing the option to create a URL for the apps you create.
Think of Google Apps Script as a cloud-based IDE (Integrated Development Environment) with code/script execution ability, source control versioning, logging of execution history, and deploy version management rolled into one.
Prerequisites
Create a Google Cloud Project
Using an active and logged in Google account, navigate to https://console.cloud.google.com in a browser to create a new project named Webhook Demo
.
View the project and select Dashboard
after the project has been created.
In the Project Info tile note the Project number
as this will be used when creating a script instance to run in this project.
Create an Apps Script project from the Google Drive associated with the account and name it Add TARP Accessorial
.
The Apps Script editor is opened with an almost empty file.
Select the settings for the Apps Script and assign the Project number
noted earlier so when this script is deployed as a web app it will run where it can be monitored.
Note
If the Google account does not have authentication permissions set then the user will be provided a link to set those permissions.
The following is a preview of the complete script this walkthrough creates and builds step by step:
Define Custom Script
Handle POST Data
Google Apps Script has a number of reserved and required function names for certain functionality. The doPost()
function is used to process any HTTP POST
requests to a REST call to a Google Apps Script web app that is available via a URL (i.e. what we are creating in this guide). An event object is passed into the doPost
function (learn more here). The event object contains info about the call made and includes the request body or payload of the call made to the web app or service. In our case, this is the request body or payload of the webhook. The first step is to create event logging by sending the webhook request body to the log console using console.info('webhook received: ' + e.postData.contents);
. Note the return includes a required HtmlService
response.
const SHIPWELL_HOST = "sandbox-api.shipwell.com"; // change this to "api.shipwell.com" for production
function doPost(e) {
console.info('webhook received: ' + e.postData.contents);
const requestBody = JSON.parse(e.postData.contents);
// TODO: Define processing logic.
return HtmlService.createHtmlOutput();
}
Define Processing Logic
In the scenario in this guide, the Shipwell API is called to get the shipment details using the self_link
in the webhook request body (aka request payload) if the TARP
accessorial is not on the shipment and the equipment type is FLATBED
. The self_link
is also used to update shipment via the HTTP method PUT
after adding the accessorial.
We use Google Apps Script's UrlFetchApp function to communicate with external applications or resources on the web by fetching URLs. This function leverages Google's network infrastructure to issue HTTP/HTTPS requests and receive responses. In the example code, UrlFetchApp is incorporated within the callShipwellAPI
helper function to connect to the Shipwell API.
Note
A valid Shipwell API token is needed. Learn how to obtain a Shipwell API token here.
const SHIPWELL_HOST = "sandbox-api.shipwell.com"; // change this to "api.shipwell.com" for production
function doPost(e) {
console.info('webhook received: ' + e.postData.contents);
const requestBody = JSON.parse(e.postData.contents);
var tarp = function(element) {
return element === 'TARP';
};
// determine if the shipment is using a FLATBED truck and if the accessorials do not already have
// a TARP accessorial, then add that a TARP is needed on the shipment.
if (requestBody.details.equipment_type == 'FLATBED' && !requestBody.details.accessorials.some(tarp)) {
const targetUrl = 'https://' + SHIPWELL_HOST + requestBody.details.self_link;
const shipmentBody = callShipwellAPI(targetUrl, 'get');
// Set the accessorial details using Shipwell accessorial reference data information
// See the Shipwell documentation on reference data and accessorials to look up the id, code, etc.
// if you have a similar use case, but do not know which accessorial id and code to use
shipmentBody.accessorials.push({
"id": 232,
"code": "TARP",
"description": "Tarp Charge",
"long_description": "Tarp Charge",
"can_be_dispatched": true,
"is_charge_code": true,
"price": 0.0,
"currency_unit": "USD",
"type": "non-specific"
});
callShipwellAPI(targetUrl, 'put', shipmentBody);
}
return HtmlService.createHtmlOutput();
}
function callShipwellAPI(url, verb, requestBody) {
requestBody = requestBody || {};
const authParam = "Token {YOUR_TOKEN}"; // Replace with your Shipwell API token
const config = {
method: verb,
payload: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
"Authorization": authParam
},
muteHttpExceptions: false
};
const response = UrlFetchApp.fetch(url, config);
const data = JSON.parse(response.getContentText());
return data;
}
Add Mock Data
Adding sample or mock data is done by creating a function that mimics real data we would encounter in our use case. A new function doPostTest()
is created that recreates the details of a mock shipment.created
event object that is the webhook request body payload and passes it to doPost(mockData)
. Doing this allows debugging of the doPost()
function with data that matches the properties and data of an actual shipment.created
webhook delivery.
function doPostTest() {
const mockPayload = '{"id":"01J4S779B8CS5J1CB87AWR0JZA","occurred_at":"2024-08-08T14:45:22.414479+00:00","source":{"user_id":"f0b979c9-28e2-41d6-883e-cb1f5c320bb4","company_id":"a54ef012-77d4-44a0-8ba5-115b09b655be","request_id":"00-66b4da0200000000f639e84d8925dcc7-191d8b28a7cd80dc-01","publishing_system":"backend","environment":"dev"},"event_name":"shipment.created","webhook_id":"01J4S75SK3QX59BZR7ZS01Z9RD","custom_data":"","details":{"id":"46213fe2-8d4a-43e4-89f8-d0dd223e4c41","mode":"FTL","name":"FTL - NUCOR STEEL MEMPHIS, INC. to Presrite Jefferson Division (Steel Receiving)","status":"draft","delayed":false,"group_id":null,"self_link":"/v2/shipments/46213fe2-8d4a-43e4-89f8-d0dd223e4c41/","bol_number":null,"pro_number":null,"weight_lbs":null,"description":null,"total_miles":798.351,"accessorials":[],"reference_id":"KL9GKK","pickup_number":null,"resource_type":"shipment","service_level":"STD","equipment_type":"FLATBED","est_trip_miles":null,"created_by_user":{"id":"f0b979c9-28e2-41d6-883e-cb1f5c320bb4","self_link":"/v2/companies/a54ef012-77d4-44a0-8ba5-115b09b655be/users/f0b979c9-28e2-41d6-883e-cb1f5c320bb4/","resource_type":"user"},"tracking_number":null,"final_trip_miles":null,"created_by_source":"SHIPWELL_WEB","notes_for_carrier":"Carrier instructions for all shipments test.","outsider_view_key":"iHXgZXEMNrgzOXQExZkq9u4KGP7MS2EK","status_updated_at":"2024-08-08T14:45:22.511874+00:00","total_linear_feet":null,"preferred_currency":"USD","cancellation_reason":null,"drayage_seal_number":null,"drayage_release_date":null,"total_declared_value":null,"label_last_printed_at":null,"purchase_order_number":null,"drayage_booking_number":null,"drayage_chassis_number":null,"drayage_last_free_date":null,"external_tracking_json":null,"temperature_lower_limit":null,"temperature_upper_limit":null,"drayage_container_number":null,"additional_bol_recipients":[],"customer_reference_number":null,"drayage_vessel_imo_number":null,"significantly_modified_at":null,"created_by_freight_authority":{"id":"0f128e3d-ebb2-49d8-bfed-84001fc14afd","self_link":"/v2/companies/0f128e3d-ebb2-49d8-bfed-84001fc14afd/","resource_type":"freight_authority"},"drayage_container_return_date":null,"drayage_estimated_arrival_date":null,"capacity_provider_customer_reference_number":null}}';
const payloadSize = Utilities.newBlob(mockPayload).length; // content length is 2262
const mockData = {
contentLength: payloadSize,
postData: {
length: payloadSize,
type: 'application/json',
contents: mockPayload,
name: 'postData'
}
};
doPost(mockData);
}
Enable Execution Logging
Google Apps Script includes execution monitoring with runtime logging to help investigate issues not revealed using the mock data. If you need additional help, Shipwell also has an API endpoint with details on webhook delivery attempts that is documented here.
Tip
You may also use this execution logging to determine what data your Google Apps Script webhook endpoint is called with when it is deployed. You can even change your doMockTest()
function to use data encountered in your debugging and logs.
Set Script Permissions
Scripts in Google Apps Script have a manifest file named appscript.json
(learn more here). The appscript.json
file contains properties that you may set for the permissions and settings of the script. For this use case, we need the script to be available for execution as a webapp.
{
"webapp": {
"executeAs": "USER_DEPLOYING",
"access": "ANYONE_ANONYMOUS"
},
"exceptionLogging": "STACKDRIVER",
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/script.scriptapp"
],
"runtimeVersion": "V8"
}
Deploy the Script as a Web App
Deploying the web app is done selecting the Deploy
button seen in previous screenshots. The user is asked to provide a description and then select to deploy the web app.
The URL for the deployed web app is provided creation and used for the URL property of when creating the webhook configuration in Shipwell.
Make note of the URL for the deployed web app and copy it for use in creating a Shipwell webhook configuration. The steps to create a Shipwell webhook configuration using this URL as the URL endpoint of the webhook configuration are available here.
Tip
The complete Google Apps Script code in this example scenario is listed below:
const SHIPWELL_HOST = "sandbox-api.shipwell.com";
function doPostTest() {
const mockPayload = '{"id":"01J4S779B8CS5J1CB87AWR0JZA","occurred_at":"2024-08-08T14:45:22.414479+00:00","source":{"user_id":"f0b979c9-28e2-41d6-883e-cb1f5c320bb4","company_id":"a54ef012-77d4-44a0-8ba5-115b09b655be","request_id":"00-66b4da0200000000f639e84d8925dcc7-191d8b28a7cd80dc-01","publishing_system":"backend","environment":"dev"},"event_name":"shipment.created","webhook_id":"01J4S75SK3QX59BZR7ZS01Z9RD","custom_data":"","details":{"id":"46213fe2-8d4a-43e4-89f8-d0dd223e4c41","mode":"FTL","name":"FTL - NUCOR STEEL MEMPHIS, INC. to Presrite Jefferson Division (Steel Receiving)","status":"draft","delayed":false,"group_id":null,"self_link":"/v2/shipments/46213fe2-8d4a-43e4-89f8-d0dd223e4c41/","bol_number":null,"pro_number":null,"weight_lbs":null,"description":null,"total_miles":798.351,"accessorials":[],"reference_id":"KL9GKK","pickup_number":null,"resource_type":"shipment","service_level":"STD","equipment_type":"FLATBED","est_trip_miles":null,"created_by_user":{"id":"f0b979c9-28e2-41d6-883e-cb1f5c320bb4","self_link":"/v2/companies/a54ef012-77d4-44a0-8ba5-115b09b655be/users/f0b979c9-28e2-41d6-883e-cb1f5c320bb4/","resource_type":"user"},"tracking_number":null,"final_trip_miles":null,"created_by_source":"SHIPWELL_WEB","notes_for_carrier":"Carrier instructions for all shipments test.","outsider_view_key":"iHXgZXEMNrgzOXQExZkq9u4KGP7MS2EK","status_updated_at":"2024-08-08T14:45:22.511874+00:00","total_linear_feet":null,"preferred_currency":"USD","cancellation_reason":null,"drayage_seal_number":null,"drayage_release_date":null,"total_declared_value":null,"label_last_printed_at":null,"purchase_order_number":null,"drayage_booking_number":null,"drayage_chassis_number":null,"drayage_last_free_date":null,"external_tracking_json":null,"temperature_lower_limit":null,"temperature_upper_limit":null,"drayage_container_number":null,"additional_bol_recipients":[],"customer_reference_number":null,"drayage_vessel_imo_number":null,"significantly_modified_at":null,"created_by_freight_authority":{"id":"0f128e3d-ebb2-49d8-bfed-84001fc14afd","self_link":"/v2/companies/0f128e3d-ebb2-49d8-bfed-84001fc14afd/","resource_type":"freight_authority"},"drayage_container_return_date":null,"drayage_estimated_arrival_date":null,"capacity_provider_customer_reference_number":null}}';
const payloadSize = Utilities.newBlob(mockPayload).length;
const mockData = {
contentLength: payloadSize,
postData: {
length: payloadSize,
type: 'application/json',
contents: mockPayload,
name: 'postData'
}
};
doPost(mockData);
}
function doPost(e) {
console.info('webhook received: ' + e.postData.contents);
const requestBody = JSON.parse(e.postData.contents);
var tarp = function(element) {
return element === 'TARP';
};
if (requestBody.details.equipment_type == 'FLATBED' && !requestBody.details.accessorials.some(tarp)) {
const targetUrl = 'https://' + SHIPWELL_HOST + requestBody.details.self_link;
const shipmentBody = callShipwellAPI(targetUrl, 'get');
shipmentBody.accessorials.push({
"id": 232,
"code": "TARP",
"description": "Tarp Charge",
"long_description": "Tarp Charge",
"can_be_dispatched": true,
"is_charge_code": true,
"price": 0.0,
"currency_unit": "USD",
"type": "non-specific"
});
callShipwellAPI(targetUrl, 'put', shipmentBody);
}
return HtmlService.createHtmlOutput();
}
function callShipwellAPI(url, verb, requestBody) {
requestBody = requestBody || {}; // Set default value manually
const authParam = "Token {YOUR_TOKEN}";
const config = {
method: verb,
payload: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
"Authorization": authParam
},
muteHttpExceptions: false
};
const response = UrlFetchApp.fetch(url, config);
const data = JSON.parse(response.getContentText());
return data;
}