Using Webhooks to Receive Status Updates

Terminal49 posts status updates to a webhook that you register with us.

A Webhook is another name for a web-based callback URL, or a HTTP Push API. They provide a method for an API to post a notification to your service. Specifically, a webhook is simply a URL that can receive HTTP Post Requests from the Terminal49 API.

The HTTP Post request from Terminal49 has a JSON payload which you can parse to extract the relevant information.

How do I use a Webhook with Terminal49?

First, you need to register a webhook. You can register as many webhooks as you like. Webhooks are associated with your account. All updates relating to that account are sent to the Webhook associated with it.

You can setup a new webhook by visiting https://app.terminal49.com/developers/webhooks and clicking the ‘Create Webhook Endpoint’ button.

Authentication

The API uses HTTP Bearer Token authentication.

This means you send your API Key as your token in every request.

Webhooks are associated with API tokens, and this is how the Terminal49 knows who to return relevant shipment information to.

Anatomy of a Webhook Notification

Here’s what you’ll see in a Webhook Notification, which arrives as a POST request to your designated URL.

For more information, refer to the Webhook In Depth guide.

Note that for clarity I’ve deleted some of the data that is less useful right now, and replaced them with ellipses (…). Bolded areas are also mine to point out important data.

Note that there are two main sections:

Data. The core information being returned.

Included. Included are relevant objects that you are included for convenience.

{
   "data": {
      "id": "87d4f5e3-df7b-4725-85a3-b80acc572e5d",
      "type": "webhook_notification",
      "attributes": {
         "id": "87d4f5e3-df7b-4725-85a3-b80acc572e5d",
         "event": "tracking_request.succeeded",
         "delivery_status": "pending",
         "created_at": "2020-09-13 14:46:37 UTC"
      },
      "relationships": {
        ...
      }
   },
   "included":[
      {
         "id": "90873f19-f9e8-462d-b129-37e3d3b64c82",
         "type": "tracking_request",
         "attributes": {
            "request_number": "MEDUNXXXXXX",
             ...
         },
        ...
      },
      {
         "id": "66db1d2a-eaa1-4f22-ba8d-0c41b051c411",
         "type": "shipment",
         "attributes": {
            "created_at": "2020-09-13 14:46:36 UTC",
            "bill_of_lading_number": "MEDUNXXXXXX",
            "ref_numbers":[
               null
            ],
            "shipping_line_scac": "MSCU",
            "shipping_line_name": "Mediterranean Shipping Company",
            "port_of_lading_locode": "PLGDY",
            "port_of_lading_name": "Gdynia",
            ....
         },
         "relationships": {
            ...
         },
         "links": {
            "self": "/v2/shipments/66db1d2a-eaa1-4f22-ba8d-0c41b051c411"
         }
      },
      {
         "id": "4d556105-015e-4c75-94a9-59cb8c272148",
         "type": "container",
         "attributes": {
            "number": "CRLUYYYYYY",
            "seal_number": null,
            "created_at": "2020-09-13 14:46:36 UTC",
            "equipment_type": "reefer",
            "equipment_length": 40,
            "equipment_height": "high_cube",
            ...
         },
         "relationships": {
          ....
         }
      },
      {
         "id": "129b695c-c52f-48a0-9949-e2821813690e",
         "type": "transport_event",
         "attributes": {
            "event": "container.transport.vessel_loaded",
            "created_at": "2020-09-13 14:46:36 UTC",
            "voyage_number": "032A",
            "timestamp": "2020-08-07 06:57:00 UTC",
            "location_locode": "PLGDY",
            "timezone": "Europe/Warsaw"
         },
       ...
      }
   ]
}

Why so much JSON? (A note on JSON API)

The Terminal49 API is JSON API compliant, which means that there are nifty libraries which can translate JSON into a fully fledged object model that can be used with an ORM. This is very powerful, but it also requires a larger, more structured payload to power the framework. The tradeoff, therefore, is that it’s less convenient if you’re parsing the JSON directly. Ultimately we strongly recommend you set yourself up with a good library to use JSON API to its fullest extent. But for the purposes of understanding the API’s fundamentals and getting your feet wet, we’ll work with the data directly.

What type of webhook event is this?

This is the first question you need to answer so your code can handle the webhook.

The type of update can be found in [“data”][“attributes”].

The most common Webhook notifications are status updates on tracking requests, like tracking_request.succeeded and updates on ETAs, shipment milestone, and terminal availability.

You can find what type of event you have received by looking at the “attributes”, “event”.

"data" : {
  ...
  "attributes": {
         "id": "87d4f5e3-df7b-4725-85a3-b80acc572e5d",
         "event": "tracking_request.succeeded",
         "delivery_status": "pending",
         "created_at": "2020-09-13 14:46:37 UTC"
      },
}

Inclusions: Tracking Requests & Shipment Data

When a tracking request has succeeded, the webhook event includes information about the shipment, the containers in the shipment, and the milestones for that container, so your app can present this information to your end users without making further queries to the API.

In the payload below (again, truncated by ellipses for clarity) you’ll see a list of JSON objects in the “included” section. Each object has a type and attributes. The type tells you what the object is. The attributes tell you the data that the object carries.

Some objects have relationships. These are simply links to another object. The most essential objects in relationships are often included, but objects that don’t change very often, for example an object that describes a teminal, are not included - once you query these, you should consider caching them locally.

 "included":[
      {
         "id": "90873f19-f9e8-462d-b129-37e3d3b64c82",
         "type": "tracking_request",
         "attributes" : {
              ...
         }
      },
      {
         "id": "66db1d2a-eaa1-4f22-ba8d-0c41b051c411",
         "type": "shipment",
         "attributes": {
            "created_at": "2020-09-13 14:46:36 UTC",
            "bill_of_lading_number": "MEDUNXXXXXX",
            "ref_numbers":[
               null
            ],
            "shipping_line_scac": "MSCU",
            "shipping_line_name": "Mediterranean Shipping Company",
            "port_of_lading_locode": "PLGDY",
            "port_of_lading_name": "Gdynia",
            ....
         },
         "relationships": {
            ...
         },
         "links": {
            "self": "/v2/shipments/66db1d2a-eaa1-4f22-ba8d-0c41b051c411"
         }
      },
      {
         "id": "4d556105-015e-4c75-94a9-59cb8c272148",
         "type": "container",
         "attributes": {
            "number": "CRLUYYYYYY",
            "seal_number": null,
            "created_at": "2020-09-13 14:46:36 UTC",
            "equipment_type": "reefer",
            "equipment_length": 40,
            "equipment_height": "high_cube",
            ...
         },
         "relationships": {
          ....
         }
      },
      {
         "id": "129b695c-c52f-48a0-9949-e2821813690e",
         "type": "transport_event",
         "attributes": {
            "event": "container.transport.vessel_loaded",
            "created_at": "2020-09-13 14:46:36 UTC",
            "voyage_number": "032A",
            "timestamp": "2020-08-07 06:57:00 UTC",
            "location_locode": "PLGDY",
            "timezone": "Europe/Warsaw"
         },
       ...
      }
   ]

Code Examples

Registering a Webhook

function registerWebhook(){
  // Make a POST request with a JSON payload.
  options = {
    "method" : "POST"
    "headers" : {
      "content-type": "application/vnd.api+json",
      "authorization" : "Token YOUR_API_KEY"
    },
    "payload" : {
      "data": {  
        "type": "webhook",
        "attributes": {
          "url": "http://yourwebhookurl.com/webhook",
          "active": true,
          "events": ["tracking_request.succeeded"]
        }
      }
    }
  };

  options.payload = JSON.stringify(data)
  var response = UrlFetchApp.fetch('https://api.terminal49.com/v2/webhooks', options);
}

Receiving a Post Webhook

Here’s an example of some Javascript code that receives a Post request and parses out some of the desired data.

function receiveWebhook(postReq) {
  try {
    var json = postReq.postData.contents;
    var webhook_raw = JSON.parse(json);
    var webhook_data = webhook_raw["data"]
    var notif_string = "";
    if (webhook_data["type"] == "webhook_notification"){
      if (webhook_data["attributes"]["event"] == "shipment.estimated.arrival"){
        /* the webhook "event" attribute tell us what event we are being notified
         * about. You will want to write a code path for each event type.     */

        var webhook_included = webhook_raw["included"];
        // from the list of included objects, extract the information about the ETA update. This should be singleton.
        var etas = webhook_included.filter(isEstimatedEvent);
        // from the same list, extract the tracking Request information. This should be singleton.
        var trackingReqs = webhook_included.filter(isTrackingRequest);
        if(etas.length > 0 && trackingReqs.length > 0){
          // therethis is an ETA updated for a specific tracking request.
          notif_string = "Estimated Event Update: " +  etas[0]["attributes"]["event"] + " New Time: " +  etas[0]["attributes"]["estimated_timestamp"];
          notif_string += " for Tracking Request: " + trackingReqs[0]["attributes"]["request_number"] + " Status: " + trackingReqs[0]["attributes"]["status"];
        } else {
          // this is a webhook type we haven't written handling code for.
        notif_string = "Error. Webhook Returned Unexpected Data.";
      }
      if (webhook_data["attributes"]["event"] == "shipment.estimated.arrival"){

      }
    }
    return HtmlService.createHtmlOutput(notf_string);
  } catch (error){
      return HtmlService.createHtmlOutput("Webhook failed: " + error);
  }

}

// JS helper functions to filter events of certain types.
function isEstimatedEvent(item){
  return item["type"] == "estimated_event";
}

function isTrackingRequest(item){
  return item["type"] == "tracking_request";
}

Try It Out & See More Sample Code

Update your API key below, and register a simple Webhook.

View the “Code Generation” button to see sample code.

http
{
  "method": "post",
  "url": "https://api.terminal49.com/v2/webhooks",
  "headers": {
    "Content-Type": "application/vnd.api+json",
    "Authorization": "Token YOUR_API_KEY"
  },
  "body": "{\r\n  \"data\": {\r\n    \"type\": \"webhook\",\r\n    \"attributes\": {\r\n      \"url\": \"https:\/\/webhook.site\/\",\r\n      \"active\": true,\r\n      \"events\": [\r\n        \"tracking_request.succeeded\"\r\n      ]\r\n    }\r\n  }\r\n}"
}