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.

Rapid, Robust, and Scalable Webhook Processing with Custom Logic Using Shipwell and Google Apps Script

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:

  1. 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.
  2. Deploy the function as a web app.
  3. 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.

apps-script-create-project.png

View the project and select Dashboard after the project has been created.

apps-script-view-project.png

In the Project Info tile note the Project number as this will be used when creating a script instance to run in this project.

apps-script-project-details.png

Create an Apps Script project from the Google Drive associated with the account and name it Add TARP Accessorial.

apps-script-create-drive-project.png

The Apps Script editor is opened with an almost empty file.

apps-script-initial-starter.png

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.

apps-script-settings.png

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: apps-script-full-example-code.png

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.

Copy
Copied
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.

Copy
Copied
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.

image-20240808-190501.png

Copy
Copied
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.

image-20240813-145146.png

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.

image-20240815-143717.png

Copy
Copied
{
  "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.

apps-script-deploy.png

The URL for the deployed web app is provided creation and used for the URL property of when creating the webhook configuration in Shipwell.

apps-script-new-deploy-url.png

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

We recommend adding custom_data to the webhook configuration with properties that are echoed into webhook payloads as documented here (Google Apps Scripts do not allow access to look up headers, so this is one way to add what would be in a header to the body or payload of a request).

The complete Google Apps Script code in this example scenario is listed below:

Copy
Copied
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;
}
Copyright © Shipwell 2024. All right reserved.