# null
Source: https://terminal49.com/docs/AGENTS
# Documentation agent instructions
These instructions guide automated changes for the Terminal49 docs in this repository.
## Scope
* Primary docs live in `docs/` (MDX pages, `docs/docs.json`, and `docs/openapi.json`).
* Do not edit generated files unless explicitly asked (e.g., `Terminal49-API.postman_collection.json`).
## Audience focus
* Primary persona: integration engineers at BCOs/shippers/exporters who may not know logistics terms.
* Secondary personas: logistics operators and decision-makers who know the domain but are less technical.
* Each page should be laser-focused on one persona and one goal.
## Content goals by section
* Getting Started: tutorial-style onboarding and first success within 30 minutes.
* In Depth Guides: how-to and explanation content for workflows and best practices.
* Useful Info: explanation/FAQ content that supports decisions and integrations.
* API Reference: reference-only endpoint lookups, no narrative.
## Voice and terminology
* Sound like a domain expert but stay friendly and easy to understand.
* Use active voice and second person ("you").
* Use consistent product terms: "Terminal49", "tracking request", "shipment", "container", "webhook".
* Define acronyms on first use (e.g., Bill of Lading (BOL)); link to a glossary if available.
* Approved positioning phrases (use where relevant, do not invent new claims):
* Automated Container Tracking API
* Tracking shipments and containers from empty-out at the origin to empty-return at the destination
* Single API to track bill of ladings, bookings, and container numbers with global coverage
* Complete import milestones in North America including rail data
## API and code examples
* Base URL is `https://api.terminal49.com/v2` unless a page says otherwise.
* Examples should be realistic but safe (no real keys, emails, or customer data).
* Use JSON with 2-space indentation; label code fences (e.g., `json, `bash, \`\`\`json http).
* Prefer copy-pasteable snippets with complete headers.
* For auth examples, use `Authorization: Token YOUR_API_KEY`.
## When updating API reference
* If you change API behavior or schemas, update `docs/openapi.json` first.
* Regenerate the Postman collection with:
`openapi2postmanv2 -s docs/openapi.json -o Terminal49-API.postman_collection.json -p -O folderStrategy=Tags`
## MDX conventions
* Every page must include frontmatter with a `title`.
* Use Mintlify components like `` or `` sparingly for emphasis.
* Keep headings concise and action-oriented.
# Edit a container
Source: https://terminal49.com/docs/api-docs/api-reference/containers/edit-a-container
patch /containers
Update a container
# Get a container
Source: https://terminal49.com/docs/api-docs/api-reference/containers/get-a-container
get /containers/{id}
Retrieves the details of a container.
# Get a container's raw events
Source: https://terminal49.com/docs/api-docs/api-reference/containers/get-a-containers-raw-events
get /containers/{id}/raw_events
#### Deprecation warning
The `raw_events` endpoint is provided as-is.
For past events we recommend consuming `transport_events`.
---
Get a list of past and future (estimated) milestones for a container as reported by the carrier. Some of the data is normalized even though the API is called raw_events.
Normalized attributes: `event` and `timestamp` timestamp. Not all of the `event` values have been normalized. You can expect the the events related to container movements to be normalized but there are cases where events are not normalized.
For past historical events we recommend consuming `transport_events`. Although there are fewer events here those events go through additional vetting and normalization to avoid false positives and get you correct data.
# Get a container's transport events
Source: https://terminal49.com/docs/api-docs/api-reference/containers/get-a-containers-transport-events
get /containers/{id}/transport_events
Get a list of past transport events (canonical) for a container. All data has been normalized across all carriers. These are a verified subset of the raw events may also be sent as Webhook Notifications to a webhook endpoint.
This does not provide any estimated future events. See `container/:id/raw_events` endpoint for that.
# Get container map GeoJSON
Source: https://terminal49.com/docs/api-docs/api-reference/containers/get-container-map-geojson
get /containers/{id}/map_geojson
Returns a GeoJSON FeatureCollection containing all map-related data for a container, including port locations, current vessel position (if at sea), past vessel paths, and estimated future routes. The response can be directly used with most mapping libraries (Leaflet, Mapbox GL, Google Maps, etc.). This is a paid feature. Please contact sales@terminal49.com.
This endpoint returns a GeoJSON FeatureCollection containing all map-related data for a container in a single response. The response includes port locations, current vessel position (if at sea), past vessel paths, and estimated future routes.
For detailed documentation on the response structure, feature types, and their properties, see the [Container Map GeoJSON Data guide](/api-docs/in-depth-guides/routing).
# List containers
Source: https://terminal49.com/docs/api-docs/api-reference/containers/list-containers
get /containers
Returns a list of container. The containers are returned sorted by creation date, with the most recently refreshed containers appearing first.
This API will return all containers associated with the account.
# Refresh container
Source: https://terminal49.com/docs/api-docs/api-reference/containers/refresh-container
patch /containers/{id}/refresh
Schedules the container to be refreshed immediately from all relevant sources.
To be alerted of updates you should subscribe to the [relevant webhooks](/api-docs/in-depth-guides/webhooks). This endpoint is limited to 10 requests per minute.This is a paid feature. Please contact sales@terminal49.com.
# Get a metro area using the un/locode or the id
Source: https://terminal49.com/docs/api-docs/api-reference/metro-areas/get-a-metro-area-using-the-unlocode-or-the-id
get /metro_areas/{id}
Return the details of a single metro area.
# null
Source: https://terminal49.com/docs/api-docs/api-reference/parties/create-a-party
post /parties
Creates a new party
# null
Source: https://terminal49.com/docs/api-docs/api-reference/parties/edit-a-party
patch /parties/{id}
Updates a party
# null
Source: https://terminal49.com/docs/api-docs/api-reference/parties/get-a-party
get /parties/{id}
Returns a party by it's given identifier
# null
Source: https://terminal49.com/docs/api-docs/api-reference/parties/list-parties
get /parties
Get a list of parties
# Get a port using the locode or the id
Source: https://terminal49.com/docs/api-docs/api-reference/ports/get-a-port-using-the-locode-or-the-id
get /ports/{id}
Return the details of a single port.
# Edit a shipment
Source: https://terminal49.com/docs/api-docs/api-reference/shipments/edit-a-shipment
patch /shipments/{id}
Update a shipment
# Get a shipment
Source: https://terminal49.com/docs/api-docs/api-reference/shipments/get-a-shipment
get /shipments/{id}
Retrieves the details of an existing shipment. You need only supply the unique shipment `id` that was returned upon `tracking_request` creation.
# List shipments
Source: https://terminal49.com/docs/api-docs/api-reference/shipments/list-shipments
get /shipments
Returns a list of your shipments. The shipments are returned sorted by creation date, with the most recent shipments appearing first.
This api will return all shipments associated with the account. Shipments created via the `tracking_request` API aswell as the ones added via the dashboard will be retuned via this endpoint.
# Resume tracking a shipment
Source: https://terminal49.com/docs/api-docs/api-reference/shipments/resume-tracking-shipment
patch /shipments/{id}/resume_tracking
Resume tracking a shipment. Keep in mind that some information is only made available by our data sources at specific times, so a stopped and resumed shipment may have some information missing.
# Stop tracking a shipment
Source: https://terminal49.com/docs/api-docs/api-reference/shipments/stop-tracking-shipment
patch /shipments/{id}/stop_tracking
We'll stop tracking the shipment, which means that there will be no more updates. You can still access the shipment's previously-collected information via the API or dashboard.
You can resume tracking a shipment by calling the `resume_tracking` endpoint, but keep in mind that some information is only made available by our data sources at specific times, so a stopped and resumed shipment may have some information missing.
# Get a single shipping line
Source: https://terminal49.com/docs/api-docs/api-reference/shipping-lines/get-a-single-shipping-line
get /shipping_lines/{id}
Return the details of a single shipping line.
# Shipping Lines
Source: https://terminal49.com/docs/api-docs/api-reference/shipping-lines/shipping-lines
get /shipping_lines
Return a list of shipping lines supported by Terminal49.
N.B. There is no pagination for this endpoint.
# Get a terminal using the id
Source: https://terminal49.com/docs/api-docs/api-reference/terminals/get-a-terminal-using-the-id
get /terminals/{id}
Return the details of a single terminal.
# Infer Tracking Number (Beta)
Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/auto-detect-carrier
post /tracking_requests/infer_number
Predict the carrier SCAC (VOCC) and number type from a tracking number. Provide a container number, bill of lading number, or booking number and receive the predicted carrier with confidence and a decision value. Use this to auto-populate carrier fields before creating a tracking request.
**Beta Feature** - This endpoint is currently in beta. The API is stable, but
the schema and behavior may evolve based on feedback.
## What this endpoint does
Give us a tracking number (container, bill of lading, or booking). We return:
* The **predicted VOCC carrier SCAC** to use for tracking
* The **predicted number type**
* A confidence-driven **decision** (`auto_select`, `needs_confirmation`, `no_prediction`)
We use machine learning prediction across container, bill of lading, and
booking numbers. For container numbers, we leverage tens of millions of
historical container movements to predict which carrier is moving the
container (about 9 out of 10 times).
## How to use the result
This endpoint is rate limited to 200 requests per minute per API key.
Learn how to use Infer Tracking Number to reliably create tracking requests
# Create a tracking request
Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/create-a-tracking-request
post /tracking_requests
To track an ocean shipment, you create a new tracking request.
Two attributes are required to track a shipment. A `bill of lading/booking number` and a shipping line `SCAC`.
Once a tracking request is created we will attempt to fetch the shipment details and it's related containers from the shipping line. If the attempt is successful we will create in new shipment object including any related container objects. We will send a `tracking_request.succeeded` webhook notification to your webhooks.
If the attempt to fetch fails then we will send a `tracking_request.failed` webhook notification to your `webhooks`.
A `tracking_request.succeeded` or `tracking_request.failed` webhook notificaiton will only be sent if you have atleast one active webhook.
This endpoint is limited to 100 tracking requests per minute.
**Don't know the SCAC?** Call [Auto-Detect
Carrier](/api-docs/api-reference/tracking-requests/auto-detect-carrier) first
to identify the shipping line from your tracking number.
# Edit a tracking request
Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/edit-a-tracking-request
patch /tracking_requests/{id}
Update a tracking request
# Get a single tracking request
Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/get-a-single-tracking-request
get /tracking_requests/{id}
Get the details and status of an existing tracking request.
# List tracking requests
Source: https://terminal49.com/docs/api-docs/api-reference/tracking-requests/list-tracking-requests
get /tracking_requests
Returns a list of your tracking requests. The tracking requests are returned sorted by creation date, with the most recent tracking request appearing first.
# Get a vessel using the id
Source: https://terminal49.com/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-id
get /vessels/{id}
Returns a vessel by id. `show_positions` is a paid feature. Please contact sales@terminal49.com.
# Get a vessel using the imo
Source: https://terminal49.com/docs/api-docs/api-reference/vessels/get-a-vessel-using-the-imo
get /vessels/{imo}
Returns a vessel by the given IMO number. `show_positions` is a paid feature. Please contact sales@terminal49.com.
# Get vessel future positions
Source: https://terminal49.com/docs/api-docs/api-reference/vessels/get-vessel-future-positions
get /vessels/{id}/future_positions
Returns the estimated route between two ports for a given vessel. The timestamp of the positions has fixed spacing of one minute.This is a paid feature. Please contact sales@terminal49.com.
# Get vessel future positions from coordinates
Source: https://terminal49.com/docs/api-docs/api-reference/vessels/get-vessel-future-positions-with-coordinates
get /vessels/{id}/future_positions_with_coordinates
Returns the estimated route between two ports for a given vessel from a set of coordinates. The timestamp of the positions has fixed spacing of one minute.This is a paid feature. Please contact sales@terminal49.com.
# Get a single webhook notification
Source: https://terminal49.com/docs/api-docs/api-reference/webhook-notifications/get-a-single-webhook-notification
get /webhook_notifications/{id}
# Get webhook notification payload examples
Source: https://terminal49.com/docs/api-docs/api-reference/webhook-notifications/get-webhook-notification-payload-examples
get /webhook_notifications/examples
Returns an example payload as it would be sent to a webhook endpoint for the provided `event`
# List webhook notifications
Source: https://terminal49.com/docs/api-docs/api-reference/webhook-notifications/list-webhook-notifications
get /webhook_notifications
Return the list of webhook notifications. This can be useful for reconciling your data if your endpoint has been down.
# Create a webhook
Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/create-a-webhook
post /webhooks
You can configure a webhook via the API to be notified about events that happen in your Terminal49 account. These events can be realted to tracking_requests, shipments and containers.
This is the recommended way tracking shipments and containers via the API. You should use this instead of polling our the API periodically.
# Delete a webhook
Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/delete-a-webhook
delete /webhooks/{id}
Delete a webhook
# Edit a webhook
Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/edit-a-webhook
patch /webhooks/{id}
Update a single webhook
# Get single webhook
Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/get-single-webhook
get /webhooks/{id}
Get the details of a single webhook
# List webhook IPs
Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/list-webhook-ips
get /webhooks/ips
Return the list of IPs used for sending webhook notifications. This can be useful for whitelisting the IPs on the firewall.
# List webhooks
Source: https://terminal49.com/docs/api-docs/api-reference/webhooks/list-webhooks
get /webhooks
Get a list of all the webhooks
# 3. List Your Shipments & Containers
Source: https://terminal49.com/docs/api-docs/getting-started/list-shipments-and-containers
## Shipment and Container Data in Terminal49
After you've successfully made a tracking request, Terminal49 will begin to track shipments and store relevant information about that shipment on your behalf.
The initial tracking request starts this process, collecting available data from Carriers and Terminals. Then, Terminal49 periodically checks for new updates adn pulls data from the carriers and terminals to keep the data we store up to date.
You can access data about shipments and containers on your tracked shipments any time. We will introduce the basics of this method below.
Keep in mind, however, that apart from initialization code, you would not usually access shipment data in this way. You would use Webhooks (described in the next section). 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.
## List all your Tracked Shipments
If your tracking request was successful, you will now be able to list your tracked shipments.
**Try it below. Click "Headers" and replace YOUR\_API\_KEY with your API key.**
Sometimes it may take a while for the tracking request to show up, but usually no more than a few minutes.
If you had trouble adding your first shipment, try adding a few more.
**We suggest copy and pasting the response returned into a text editor so you can examine it while continuing the tutorial.**
```json http theme={null}
{
"method": "get",
"url": "https://api.terminal49.com/v2/shipments",
"headers": {
"Content-Type": "application/vnd.api+json",
"Authorization": "Token YOUR_API_KEY"
}
}
```
> ### 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.
## 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 Shipments JSON Response
Here's what you'll see come back after you get the /shipments endpoint.
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.
The **Data** attribute contains an array of objects. Each object is of type "shipment" and includes attributes such as bill of lading number, the port of lading, and so forth. Each Shipment object also has Relationships to structured data objects, for example, Ports and Terminals, as well as a list of Containers which are on this shipment.
You can write code to access these structured elements from the API. The advantage of this approach is that Terminal49 cleans and enhances the data that is provided from the steamship line, meaning that you can access a pre-defined object definition for a specific port in Los Angeles.
```jsx theme={null}
{
"data": [
{
/* this is an internal id that you can use to query the API directly, i.e by hitting https://api.terminal49.com/v2/shipments/123456789 */
"id": "123456789",
// the object type is a shipment, per below.
"type": "shipment",
"attributes": {
// Your BOL number that you used in the tracking request
"bill_of_lading_number": "99999999",
...
"shipping_line_scac": "MAEU",
"shipping_line_name": "Maersk",
"port_of_lading_locode": "INVTZ",
"port_of_lading_name": "Visakhapatnam",
...
},
"relationships": {
"port_of_lading": {
"data": {
"id": "bde5465a-1160-4fde-a026-74df9c362f65",
"type": "port"
}
},
"port_of_discharge": {
"data": {
"id": "3d892622-def8-4155-94c5-91d91dc42219",
"type": "port"
}
},
"pod_terminal": {
"data": {
"id": "99e1f6ba-a514-4355-8517-b4720bdc5f33",
"type": "terminal"
}
},
"destination": {
"data": null
},
"containers": {
"data": [
{
"id": "593f3782-cc24-46a9-a6ce-b2f1dbf3b6b9",
"type": "container"
}
]
}
},
"links": {
// this is a link to this specific shipment in the API.
"self": "/v2/shipments/7f8c52b2-c255-4252-8a82-f279061fc847"
}
},
...
],
...
}
```
## Sample Code: Listing Tracked Shipment into a Google Sheet
Below is code written in Google App Script that lists the current shipments into the current sheet of a spreadsheet. App Script is very similar to Javascript.
Because Google App Script does not have native JSON API support, we need to parse the JSON directly, making this example an ideal real world application of the API.
```jsx theme={null}
function listTrackedShipments(){
// first we construct the request.
var options = {
"method" : "GET",
"headers" : {
"content-type": "application/vnd.api+json",
"authorization" : "Token YOUR_API_KEY"
},
"payload" : ""
};
try {
// note that URLFetchApp is a function of Google App Script, not a standard JS function.
var response = UrlFetchApp.fetch("https://api.terminal49.com/v2/shipments", options);
var json = response.getContentText();
var shipments = JSON.parse(json)["data"];
var shipment_values = [];
shipment_values = extractShipmentValues(shipments);
listShipmentValues(shipment_values);
} catch (error){
//In JS you would use console.log(), but App Script uses Logger.log().
Logger.log("error communicating with t49 / shipments: " + error);
}
}
function extractShipmentValues(shipments){
var shipment_values = [];
shipments.forEach(function(shipment){
// iterating through the shipments.
shipment_values.push(extractShipmentData(shipment));
});
return shipment_values;
}
function extractShipmentData(shipment){
var shipment_val = [];
//for each shipment I'm extracting some of the key info i want to display.
shipment_val.push(shipment["attributes"]["shipping_line_scac"],
shipment["attributes"]["shipping_line_name"],
shipment["attributes"]["bill_of_lading_number"],
shipment["attributes"]["pod_vessel_name"],
shipment["attributes"]["port_of_lading_name"],
shipment["attributes"]["pol_etd_at"],
shipment["attributes"]["pol_atd_at"],
shipment["attributes"]["port_of_discharge_name"],
shipment["attributes"]["pod_eta_at"],
shipment["attributes"]["pod_ata_at"],
shipment["relationships"]["containers"]["data"].length,
shipment["id"]
);
return shipment_val;
}
function listShipmentValues(shipment_values){
// now, list the data in the spreadsheet.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var homesheet = ss.getActiveSheet();
var STARTING_ROW = 1;
var MAX_TRACKED = 500;
try {
// clear the contents of the sheet first.
homesheet.getRange(STARTING_ROW,1,MAX_TRACKED,shipment_values[0].length).clearContent();
// now insert all the shipment values directly into the sheet.
homesheet.getRange(STARTING_ROW,1,shipment_values.length,shipment_values[0].length).setValues(shipment_values);
} catch (error){
Logger.log("there was an error in listShipmentValues: " + error);
}
}
```
## List all your Tracked Containers
You can also list out all of your Containers. Container data includes Terminal availability, last free day, and other logistical information that you might use for drayage operations at port.
**Try it below. Click "Headers" and replace YOUR\_API\_KEY with your API key.**
**We suggest copy and pasting the response returned into a text editor so you can examine it while continuing the tutorial.**
```json http theme={null}
{
"method": "get",
"url": "https://api.terminal49.com/v2/containers",
"headers": {
"Content-Type": "application/vnd.api+json",
"Authorization": "Token YOUR_API_KEY"
}
}
```
## Anatomy of Containers JSON Response
Now that you've got a list of containers, let's examine the response you've received.
```jsx theme={null}
// We have an array of objects in the data returned.
"data": [
{
//
"id": "internalid",
// this object is of type Container.
"type": "container",
"attributes": {
// Here is your container number
"number": "OOLU-xxxx",
// Seal Numbers aren't always returned by the carrier.
"seal_number": null,
"created_at": "2020-09-13T19:16:47Z",
"equipment_type": "reefer",
"equipment_length": null,
"equipment_height": null,
"weight_in_lbs": 54807,
//currently no known fees; this list will expand.
"fees_at_pod_terminal": [],
"holds_at_pod_terminal": [],
// here is your last free day.
"pickup_lfd": "2020-09-17T07:00:00Z",
"pickup_appointment_at": null,
"availability_known": true,
"available_for_pickup": false,
"pod_arrived_at": "2020-09-13T22:05:00Z",
"pod_discharged_at": "2020-09-15T05:27:00Z",
"location_at_pod_terminal": "CC1-162-B-3(Deck)",
"final_destination_full_out_at": null,
"pod_full_out_at": "2020-09-18T10:30:00Z",
"empty_terminated_at": null
},
"relationships": {
// linking back to the shipment object, found above.
"shipment": {
"data": {
"id": "894befec-e7e2-4e48-ab97-xxxxxxxxx",
"type": "shipment"
}
},
"pod_terminal": {
"data": {
"id": "39d09f18-cf98-445b-b6dc-xxxxxxxxx",
"type": "terminal"
}
},
...
}
},
...
```
# 4. How to Receive Status Updates
Source: https://terminal49.com/docs/api-docs/getting-started/receive-status-updates
## 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](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.
```jsx theme={null}
{
"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".
```jsx theme={null}
"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.
```jsx theme={null}
"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
```jsx theme={null}
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.
```json http theme={null}
{
"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}"
}
```
# 1. Start Here
Source: https://terminal49.com/docs/api-docs/getting-started/start-here
So you want to start tracking your ocean shipments and containers and you have a few BL numbers. Follow the guide.
Our API responses use [JSONAPI](https://jsonapi.org/) schema. There are [client libraries](https://jsonapi.org/implementations/#client-libraries) available in almost every language. Our API should work with these libs out of the box.
Our APIs can be used with any HTTP client; choose your favorite! We love Postman, it's a friendly graphical interface to a powerful cross-platform HTTP client. Best of all it has support for the OpenAPI specs that we publish with all our APIs. We have created a collection of requests for you to easily test the API endpoints with your API Key. Link to the collection below.
**Run in Postman**
***
## Get an API Key
Sign in to your Terminal49 account and go to your [developer portal](https://app.terminal49.com/developers/api-keys) page to get your API key.
### Authentication
When passing your API key it should be prefixed with `Token`. For example, if your API Key is 'ABC123' then your Authorization header would look like:
```
"Authorization": "Token ABC123"
```
# 2. Tracking Shipments & Containers
Source: https://terminal49.com/docs/api-docs/getting-started/tracking-shipments-and-containers
Submitting a tracking request is how you tell Terminal49 to track a shipment for you.
## What is a Tracking Request?
Your tracking request includes two pieces of data:
* Your Bill of Lading, Booking number, or container number from the carrier.
* The SCAC code for that carrier.
**Don't know the SCAC?** Use the [Auto-Detect
Carrier](/api-docs/in-depth-guides/auto-detect-carrier) endpoint to
automatically identify the shipping line from your tracking number.
You can see a complete list of supported SCACs in row 2 of the Carrier Data Matrix.
## What sort of numbers can I track?
**Supported numbers**
1. Master Bill of Lading from the carrier (recommended)
2. Booking number from the carrier
3. Container number
* Container number tracking support across ocean carriers is sometimes more limited. Please refer to the Carrier Data Matrix to see which SCACs are compatible with Container number tracking.
**Unsupported numbers**
* House Bill of Lading numbers (HBOL)
* Customs entry numbers
* Seal numbers
* Internally generated numbers, for example PO numbers or customer reference numbers.
## How do I use Tracking Requests?
Terminal49 is an event-based API, which means that the API can be used asynchronously. In general the data flow is:
1. You send a tracking request to the API with your Bill of Lading number and SCAC.
2. The API will respond that it has successfully received your Tracking Request and return the Shipment's data that is available at that time.
3. After you have submitted a tracking request, the shipment and all of the shipments containers are tracked automatically by Terminal49.
4. You will be updated when anything changes or more data becomes available. Terminal49 sends updates relating to your shipment via posts to the webhook you have registered. Generally speaking, updates occur when containers reach milestones. ETA updates can happen at any time. As the ship approaches port, you will begin to receive Terminal Availability data, Last Free day, and so forth.
5. At any time, you can directly request a list of shipments and containers from Terminal49, and the API will return current statuses and information. This is covered in a different guide.
## How do you send me the data relating to the tracking request?
You have two options. First, you can poll for updates. This is the way we'll show you first.
You can poll the `GET /tracking_request/{id}` endpoint to see the status of your request. You just need to track the ID of your tracking request, which is returned to you by the API.
Second option is that you can register a webhook and the API will post updates when they happen. This is more efficient and therefore preferred. But it also requires some work to set up.
A Webhook is another name for a web-based callback URL, or a HTTP Push API. Webhooks 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.
When we successfully lookup the Bill of Lading with the Carrier's SCAC, we will create a shipment, and send the event `tracking_request.succeeded` to your webhook endpoint with the associated record.
If we encounter a problem we'll send the event `tracking_request.failed`.
## Authentication
The API uses Bearer Token style authentication. This means you send your API Key as your token in every request.
To get your API token to Terminal49 and go to your [account API settings](https://app.terminal49.com/settings/api)
The token should be sent with each API request in the Authentication header:
Support [dev@terminal49.com](dev@terminal49.com)
```
Authorization: Token YOUR_API_KEY
```
## How to Create a Tracking Request
Here is javascript code that demonstates sending a tracking request
```json theme={null}
fetch("https://api.terminal49.com/v2/tracking_requests", {
"method": "POST",
"headers": {
"content-type": "application/vnd.api+json",
"authorization": "Token YOUR_API_KEY"
},
"body": {
"data": {
"attributes": {
"request_type": "bill_of_lading",
"request_number": "",
"scac": ""
},
"type": "tracking_request"
}
}
})
.then(response => {
console.log(response);
})
.catch(err => {
console.error(err);
});
```
## Anatomy of a Tracking Request Response
Here's what you'll see in a Response to a tracking request.
```json theme={null}
{
"data": {
"id": "478cd7c4-a603-4bdf-84d5-3341c37c43a3",
"type": "tracking_request",
"attributes": {
"request_number": "xxxxxx",
"request_type": "bill_of_lading",
"scac": "MAEU",
"ref_numbers": [],
"created_at": "2020-09-17T16:13:30Z",
"updated_at": "2020-09-17T17:13:30Z",
"status": "pending",
"failed_reason": null,
"is_retrying": false,
"retry_count": null
},
"relationships": {
"tracked_object": {
"data": null
}
},
"links": {
"self": "/v2/tracking_requests/478cd7c4-a603-4bdf-84d5-3341c37c43a3"
}
}
}
```
Note that if you try to track the same shipment, you will receive an error like this:
```json theme={null}
{
"errors": [
{
"status": "422",
"source": {
"pointer": "/data/attributes/request_number"
},
"title": "Unprocessable Entity",
"detail": "Request number 'xxxxxxx' with scac 'MAEU' already exists in a tracking_request with a pending or created status",
"code": "duplicate"
}
]
}
```
**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.
## Try It: Make a Tracking Request
Try it using the request maker below!
1. Enter your API token in the autorization header value.
2. Enter a value for the `request_number` and `scac`. The request number has to be a shipping line booking or master bill of lading number. The SCAC has to be a shipping line scac (see data sources to get a list of valid SCACs)
Note that you can also access sample code in multiple languages by clicking the "Code Generation" below.
**Tracking Request Troubleshooting**
The most common issue people encounter is that they are entering the wrong number.
Please check that you are entering the Bill of Lading number, booking number, or container number and not internal reference at your company or by your frieght forwarder. You can the number you are supplying by going to a carrier's website and using their tools to track your shipment using the request number. If this works, and if the SCAC is supported by T49, you should able to track it with us.
If you're unsure of the correct SCAC, try the [Auto-Detect Carrier](/api-docs/api-reference/tracking-requests/auto-detect-carrier) endpoint first.
It is entirely possible that's neither us nor you but the shipping line is giving us a headache. Temporary network problems, not populated manifest and other things happen! You can read on how are we handling them in the [Tracking Request Retrying](/api-docs/useful-info/tracking-request-retrying) section.
Rate limiting: You can create up to 100 tracking requests per minute.
You can always email us at [support@terminal49.com](mailto:support@terminal49.com) if you have persistent
issues.
```json theme={null}
{
"method": "post",
"url": "https://api.terminal49.com/v2/tracking_requests",
"headers": {
"Content-Type": "application/vnd.api+json",
"Authorization": "Token YOUR_API_KEY"
},
"body": "{\r\n \"data\": {\r\n \"attributes\": {\r\n \"request_type\": \"bill_of_lading\",\r\n \"request_number\": \"\",\r\n \"scac\": \"\"\r\n },\r\n \"type\": \"tracking_request\"\r\n }\r\n}"
}
```
## Try It: List Your Active Tracking Requests
We have not yet set up a webook to receive status updates from the Terminal49 API, so we will need to manually poll to check if the Tracking Request has succeeded or failed.
**Try it below. Click "Headers" and replace `` with your API key.**
```json theme={null}
{
"method": "get",
"url": "https://api.terminal49.com/v2/tracking_requests",
"headers": {
"Content-Type": "application/vnd.api+json",
"Authorization": "Token YOUR_API_KEY"
}
}
```
## Next Up: Get your Shipments
Now that you've made a tracking request, let's see how you can list your shipments and retrieve the relevant data.
Go to this
[page](https://help.terminal49.com/en/articles/8074102-how-to-initiate-shipment-tracking-on-terminal49)
to see different ways of initiating shipment tracking on Terminal49.
# How to add a Customer to a Tracking Request?
Source: https://terminal49.com/docs/api-docs/in-depth-guides/adding-customer
## Why you would want to add a party to a tracking request?
Adding a party to a tracking request allows you to associate customer information with the tracking request. The customer added to the tracking request will be assigned to the shipment when it is created, just like reference numbers and tags. This can help in organizing and managing your shipments more effectively.
## How to get the party ID?
You can either find an existing party or create a new one.
* To find an existing party, jump to [Listing all parties](#listing-all-parties) section.
* To create a new party, jump to [Adding party for a customer](#adding-party-for-a-customer) section.
## Listing all parties
You can list all parties associated with your account through the [API](/api-docs/api-reference/parties/list-parties).
Endpoint: **GET** - [https://api.terminal49.com/v2/parties](/api-docs/api-reference/parties/list-parties)
```json Response theme={null}
{
"data": [
{
"id": "PARTY_ID_1",
"type": "party",
"attributes": {
"company_name": "COMPANY NAME 1",
}
},
{
"id": "PARTY_ID_2",
"type": "party",
"attributes": {
"company_name": "COMPANY NAME 2",
}
}
],
"links": {
"last": "",
"next": "",
"prev": "",
"first": "",
"self": ""
},
"meta": {
"size": 2,
"total": 2
}
}
```
After you get all the parties you would filter the parties by `company_name` to find the correct ID, either by looking through the list manually or using code to automate the process.
## How to add party to tracking request if you have the party ID?
To add a customer to a tracking request, you need to add the party to the tracking request as a customer relationship while being created. **Note** that a party cannot be added to a tracking request that has already been created.
Endpoint: **POST** - [https://api.terminal49.com/v2/tracking\_requests](/api-docs/api-reference/tracking-requests/create-a-tracking-request)
```json Request theme={null}
{
"data": {
"type": "tracking_request",
"attributes": {
"request_type": "bill_of_lading",
"request_number": "MEDUFR030802",
"ref_numbers": [
"PO12345",
"HBL12345",
"CUSREF1234"
],
"shipment_tags": [
"camembert"
],
"scac": "MSCU"
},
"relationships": {
"customer": {
"data": {
"id": "PARTY_ID",
"type": "party"
}
}
}
}
}
```
After you send a **POST** request to create a tracking request, you will receive a response with the Tracking Request ID and customer relationship. You can use this tracking request ID to track the shipment.
```json Response theme={null}
{
"data": {
"id": "TRACKING_REQUEST_ID",
"type": "tracking_request",
"attributes": {
"request_type": "bill_of_lading",
"request_number": "MEDUFR030802",
"ref_numbers": [
"PO12345",
"HBL12345",
"CUSREF1234"
],
"shipment_tags": [
"camembert"
],
"scac": "MSCU"
},
"relationships": {
"tracked_object": {
"data": null
},
"customer": {
"data": {
"id": "PARTY_ID",
"type": "party"
}
}
},
"links": {
"self": "/v2/tracking_requests/TRACKING_REQUEST_ID"
}
}
}
```
## Adding party for a customer
For adding a customer to a tracking request, you need to create a party first. You can create a party through the [API](/api-docs/api-reference/parties/create-a-party).
Endpoint: **POST** - [https://api.terminal49.com/v2/parties](/api-docs/api-reference/parties/create-a-party)
```json Request theme={null}
{
"data": {
"type": "party",
"attributes": {
"company_name": "COMPANY NAME"
}
}
}
```
After you send a **POST** request to create a party, you will receive a response with the Party ID. You can use this Party ID to add the customer to a tracking request.
```json Response theme={null}
{
"data": {
"id": "PARTY_ID",
"type": "party",
"attributes": {
"company_name": "COMPANY NAME"
}
}
}
```
## Editing a party
You can update existing parties through the [API](/api-docs/api-reference/parties/edit-a-party).
Endpoint: **PATCH** - [https://api.terminal49.com/v2/parties/PARTY\_ID](/api-docs/api-reference/parties/edit-a-party)
## Reading a party
You can retrieve the details of an existing party through the [API](/api-docs/api-reference/parties/get-a-party).
Endpoint: **GET** - [https://api.terminal49.com/v2/parties/PARTY\_ID](/api-docs/api-reference/parties/get-a-party)
# Identify Your Carrier with the Infer API
Source: https://terminal49.com/docs/api-docs/in-depth-guides/auto-detect-carrier
Don't know the carrier SCAC for your tracking number? The Infer API identifies it automatically, helping you create successful tracking requests.
**Beta Feature** — This guide covers the [Infer Number
API](/api-docs/api-reference/tracking-requests/auto-detect-carrier), currently
in beta. The API is stable for production use, but features may expand based
on feedback.
Every tracking request requires two things: **your tracking number** and **the shipping line's (carrier's) SCAC code**.
But what if you don't know the SCAC? That's where the Infer API comes in.
You've seen this feature in action — when you enter a number, we auto-suggest
the carrier. Now this same intelligence is available via API.
## Why SCAC Matters
To track a shipment or container, Terminal49 needs to know **which shipping line to ask** (also called the vessel-operating common carrier (VOCC)).
The SCAC (Standard Carrier Alpha Code) we use here is the **shipping line SCAC for tracking** — i.e., the carrier operating the move we’re querying for events and shipment data.
| You Have | You Need | The Challenge |
| ------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| Bill of Lading: `MAEU123456789` | Shipping line SCAC (VOCC SCAC) | Many MBOLs **do not include a prefix**, and even when they do, it may not reliably identify the shipping line you need for tracking. |
| Container: `WHLU1234560` | Shipping line SCAC (VOCC SCAC) | The container owner code / leasing company is not always the carrier moving it, so the prefix alone is not enough. |
| Booking: `987654321` | Shipping line SCAC (VOCC SCAC) | Booking formats vary widely and often contain no carrier identifier. |
Without the correct **shipping line SCAC (VOCC SCAC)**, your tracking request can fail even if the
number is valid. The Infer API predicts the shipping line SCAC + number type to
increase the likelihood your tracking request succeeds.
## How the Infer API Helps
Submit any tracking number, and the API returns:
* **The predicted shipping line (SCAC)** — so you don't have to guess
* **The number type** — container, bill of lading, or booking
* **Validation results** — catches typos and invalid formats before you submit
Just the number — no need to specify the shipping line or type
Our system uses machine learning and historical data from millions of
shipments to predict the shipping line.
Use high-confidence results automatically, or prompt users to confirm
With the right SCAC, your tracking request is far more likely to succeed
## Examples by Number Type
Container numbers follow the ISO 6346 format. While the first three letters (owner code) often indicate the owner, the container might be moved by a different shipping line (VOCC).
Our system analyzes the number against tens of millions of historical records to predict which shipping line is moving the container.
**Example Input:** `MSCU1234567`
```json Request theme={null}
{
"number": "MSCU1234567"
}
```
```json Response theme={null}
{
"data": {
"attributes": {
"number_type": "container",
"validation": {
"is_valid": true,
"type": "container",
"check_digit_passed": true
},
"shipping_line": {
"decision": "auto_select",
"selected": {
"scac": "MSCU",
"name": "Mediterranean Shipping Company",
"confidence": 1.0
},
"candidates": [
{
"scac": "MSCU",
"name": "Mediterranean Shipping Company",
"confidence": 1.0
}
]
}
}
}
}
```
For container numbers, we use historical data to identify the shipping line with high accuracy (9/10 times).
Always check the `decision` field to know if you should ask the user for confirmation.
**What to do next:**
```bash theme={null}
# Create tracking request with the detected SCAC
POST /tracking_requests
{
"data": {
"type": "tracking_request",
"attributes": {
"request_number": "MSCU1234567",
"scac": "MSCU",
"request_type": "container"
}
}
}
```
Bill of lading numbers vary by carrier. Some contain prefixes, but others don't. The API uses machine learning to identify the carrier pattern.
**Example Input:** `MAEU123456789`
```json Request theme={null}
{
"number": "MAEU123456789"
}
```
```json Response theme={null}
{
"data": {
"attributes": {
"number_type": "bill_of_lading",
"validation": {
"is_valid": true,
"type": "shipment"
},
"shipping_line": {
"decision": "auto_select",
"selected": {
"scac": "MAEU",
"name": "Maersk",
"confidence": 0.98
},
"candidates": [
{
"scac": "MAEU",
"name": "Maersk",
"confidence": 0.98
}
]
}
}
}
}
```
Maersk BLs typically start with `MAEU`, but other carriers may not have prefixes. The API analyzes the full format.
**What to do next:**
```bash theme={null}
# Create tracking request with the detected SCAC
POST /tracking_requests
{
"data": {
"type": "tracking_request",
"attributes": {
"request_number": "MAEU123456789",
"scac": "MAEU",
"request_type": "bill_of_lading"
}
}
}
```
Booking numbers are the **hardest to identify** — they often don't contain carrier identifiers.
**Example Input:** `987654321`
```json Request theme={null}
{
"number": "987654321"
}
```
```json Response theme={null}
{
"data": {
"attributes": {
"number_type": "booking",
"validation": {
"is_valid": null,
"type": "shipment"
},
"shipping_line": {
"decision": "needs_confirmation",
"selected": {
"scac": "HLCU",
"name": "Hapag-Lloyd",
"confidence": 0.72
},
"candidates": [
{
"scac": "HLCU",
"name": "Hapag-Lloyd",
"confidence": 0.72
},
{
"scac": "ONE",
"name": "Ocean Network Express",
"confidence": 0.18
}
]
}
}
}
}
```
When `decision` is `needs_confirmation`, show the suggestion but **ask the user to verify**.
Display the `candidates` list as options.
**What to do next:**
```bash theme={null}
# Show user the suggested carrier and candidates
# After user confirms, create tracking request
POST /tracking_requests
{
"data": {
"type": "tracking_request",
"attributes": {
"request_number": "987654321",
"scac": "HLCU",
"request_type": "booking"
}
}
}
```
## Understanding the Response
The `decision` field tells you how confident the prediction is and what action to take:
| Decision | When it's used | What to do |
| -------------------- | ---------------------------- | ----------------------------------------------------- |
| `auto_select` | Confidence ≥ 95% | ✅ Safe to use automatically without user confirmation |
| `needs_confirmation` | Confidence 70-95% | ⚠️ Show suggestion, ask user to confirm |
| `no_prediction` | Confidence \< 70% or unknown | ❌ User must select carrier manually |
For the best user experience, always handle all three decision types. Even
when `no_prediction` is returned, you can still show the list of `candidates`
as suggestions.
The API validates numbers before returning predictions:
| Field | Description |
| -------------------- | ---------------------------------------------------------------- |
| `is_valid` | `true` if format is valid, `false` if invalid, `null` if unknown |
| `check_digit_passed` | For containers: ISO 6346 check digit verification |
| `reason` | If invalid, explains why (e.g., "Invalid check digit") |
Invalid numbers may still return a carrier prediction, but you should validate
the format before creating a tracking request.
| Field | Type | Description |
| -------------------------- | ------ | ---------------------------------------------------------- |
| `number_type` | string | Detected type: `container`, `bill_of_lading`, or `booking` |
| `shipping_line.decision` | string | Confidence level for the prediction |
| `shipping_line.selected` | object | Best match: `scac`, `name`, `confidence` |
| `shipping_line.candidates` | array | All possible matches, ranked by confidence |
See the [API Reference](/api-docs/api-reference/tracking-requests/auto-detect-carrier) for complete schema details.
## Integration Guide
For developers implementing this API, here are code examples in different languages:
```javascript theme={null}
async function getCarrierForNumber(trackingNumber, apiKey) {
const response = await fetch(
"https://api.terminal49.com/v2/tracking_requests/infer_number",
{
method: "POST",
headers: {
Authorization: `Token ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ number: trackingNumber }),
}
);
const { data } = await response.json();
const { decision, selected, candidates } = data.attributes.shipping_line;
return {
scac: selected?.scac,
carrier: selected?.name,
confidence: selected?.confidence,
autoSelect: decision === "auto_select",
needsConfirmation: decision === "needs_confirmation",
candidates: candidates,
};
}
// Usage
const result = await getCarrierForNumber("MSCU1234567", "YOUR_API_KEY");
if (result.autoSelect) {
// Auto-fill carrier dropdown
carrierDropdown.value = result.scac;
} else if (result.needsConfirmation) {
// Show suggestion with confirmation prompt
showCarrierSuggestion(result.carrier, result.candidates);
}
```
```python theme={null}
import requests
def get_carrier_for_number(tracking_number: str, api_key: str) -> dict:
"""Get carrier prediction for a tracking number."""
response = requests.post(
'https://api.terminal49.com/v2/tracking_requests/infer_number',
headers={
'Authorization': f'Token {api_key}',
'Content-Type': 'application/json'
},
json={'number': tracking_number}
)
result = response.json()
shipping_line = result['data']['attributes']['shipping_line']
return {
'scac': shipping_line['selected']['scac'] if shipping_line.get('selected') else None,
'carrier': shipping_line['selected']['name'] if shipping_line.get('selected') else None,
'confidence': shipping_line['selected']['confidence'] if shipping_line.get('selected') else None,
'auto_select': shipping_line['decision'] == 'auto_select',
'needs_confirmation': shipping_line['decision'] == 'needs_confirmation',
'candidates': shipping_line.get('candidates', [])
}
# Usage
result = get_carrier_for_number("MSCU1234567", "YOUR_API_KEY")
if result['auto_select']:
# Auto-fill carrier dropdown
carrier_dropdown.value = result['scac']
elif result['needs_confirmation']:
# Show suggestion with confirmation prompt
show_carrier_suggestion(result['carrier'], result['candidates'])
```
```bash theme={null}
curl -X POST https://api.terminal49.com/v2/tracking_requests/infer_number \
-H "Authorization: Token YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"number": "MSCU1234567"}'
```
## Rate Limits
| Setting | Value |
| ------------------- | ----------------------- |
| Requests per minute | 200 |
| Rate limit header | `Retry-After` (seconds) |
Rate limit errors return HTTP 429 with a `Retry-After` header. Respect this
header to avoid extended throttling.
## What's Next?
Full API specification with request/response schemas and try it in the
playground
Use the detected SCAC to start tracking your shipment
See which carriers we support and their data availability
Learn what happens after you submit a tracking request
# Container Statuses
Source: https://terminal49.com/docs/api-docs/in-depth-guides/container-statuses
The `current_status` attribute on container objects provides a high-level view of where a container is in its journey. This guide explains the different status values and their meanings.
## Available Status Values
The `current_status` field can have one of the following values:
### new
**Default state** - The container is tracked but we don't yet have enough information to determine its actual status. This is the initial state when tracking begins.
### on\_ship
**In transit by vessel** - The container is on a ship at any point prior to arrival at the Port of Discharge (POD). This status can apply at multiple stages:
* During the ocean voyage from origin to destination
* Before vessel departure from the Port of Lading
* Any time the container is loaded on a vessel
### available
**Ready for pickup** - The container has arrived at the POD or inland destination and is confirmed available for pickup. You can proceed with arranging pickup when you see this status.
### not\_available
**Arrived but not ready** - The container is at the POD or inland destination but is not yet available for pickup. This could be due to:
* Customs holds
* Terminal holds
* Documentation requirements
* Other restrictions
### grounded
**Availability unknown** - The container is physically at the POD, but we don't currently know whether it's available for pickup or not. This typically means the terminal isn't providing real-time availability data.
### awaiting\_inland\_transfer
**Moving inland** - The container is either:
* Still at the POD waiting to be loaded onto rail for an inland move
* In transit between the POD and a rail terminal
* At a rail terminal but not yet loaded onto a rail car
This status is specific to shipments with inland rail movements.
### on\_rail
**In transit by rail** - The container has been loaded onto a rail car and is being transported to its inland destination.
### picked\_up
**Out for delivery** - The container has been picked up from the terminal or facility for delivery.
### off\_dock
**At alternative facility** - The container has been moved to a different terminal or off-dock storage facility for pickup. You may need to go to this alternative location rather than the original POD terminal.
### delivered
**Delivery confirmed** - This status is only shown when delivery has been [manually marked as delivered](https://help.terminal49.com/articles/4713318249-how-to-mark-containers-as-delivered) through the Terminal49 dashboard.
### empty\_returned
**Container returned empty** - The container has been emptied and returned to the shipping line or designated return location.
### dropped
**Not currently used** - This status value is defined but not actively used in the system.
### loaded
**Not currently used** - This status value is defined but not actively used in the system.
## Important Considerations
### Status Accuracy
The logic to derive container statuses is complex and involves processing data from multiple sources including:
* Shipping line updates
* Terminal systems
* Rail carrier feeds
* Manual updates
**There can sometimes be errors in the reported `current_status`**. When making critical business decisions, we recommend:
* Cross-referencing with transport events
* Contacting the terminal directly for time-sensitive pickups
### Status Transitions
Containers don't always follow a linear path through these statuses. For example:
* A container may go from `on_ship` directly to `available` if terminal data arrives quickly
* A container might alternate between `available` and `not_available` if holds are placed and removed
* The status may remain as `new` for some time if data from the shipping line is delayed
### API Usage
To get the current status of a container, you can read the container's `current_status` attribute in your API responses:
```bash theme={null}
GET /v2/containers/{id}
```
The response will include:
```json theme={null}
{
"data": {
"id": "ff77a822-23a7-4ccd-95ca-g534c071baaf3",
"type": "container",
"attributes": {
"number": "KOCU4959010",
"current_status": "available",
...
}
}
}
```
# Event Timestamps
Source: https://terminal49.com/docs/api-docs/in-depth-guides/event-timestamps
Through the typical container lifecycle events occur across multiple timezones. Wheverever you see a timestamp for some kind of transporation event, there should be a corresponding [IANA tz](https://www.iana.org/time-zones).
Event timestamps are stored and returned in UTC. If you wish to present them in the local time you need to convert that UTC timestamp using the corresponding timezone.
### Example
If you receive a container model with the attributes
```
'pod_arrived_at': '2022-12-22T07:00:00Z',
'pod_timezone': 'America/Los_Angeles',
```
then the local time of the `pod_arrived_at` timestamp would be `2022-12-21T23:00:00 PST -08:00`
## When the corresponding timezone is null
When there is event that occurs where Terminal49 cannot determine the location (and therefore the timezone) of the event the system is unable to store the event in true UTC.
In this scenario we take timestamp as given from the source and parse it in UTC.
### Example
```
'pod_arrived_at': '2022-12-22T07:00:00Z',
'pod_timezone': null,
```
then the local time of the `pod_arrived_at` timestamp would be `2022-12-22T07:00:00` and the timezone is unknown. (Assuming the source was returning localized timestamps)
## System Timestamps
Timestamps representing changes within the Terminal49 system (e.g. `created_at`, `updated_at`, `terminal_checked_at`) are stored and represented in UTC and do not have a TimeZone.
# Including Resources
Source: https://terminal49.com/docs/api-docs/in-depth-guides/including-resources
Throughout the documentation you will notice that many of the endpoints include a `relationships` object inside of the `data` attribute.
For example, if you are [requesting a container](/api/4c6091811c4e0-get-a-container) the relationships will include `shipment`, and possibly `pod_terminal` and `transport_events`
If you want to load the `shipment` and `pod_terminal` without making any additional requests you can add the query parameter `include` and provide a comma delimited list of the related resources:
```
containers/{id}?include=shipment,pod_terminal
```
You can even traverse the relationships up or down. For example if you wanted to know the port of lading for the container you could get that with:
```
containers/{id}?include=shipment,shipment.port_of_lading
```
# Quick Start Guide
Source: https://terminal49.com/docs/api-docs/in-depth-guides/quickstart
## Before You Begin
You'll need a four things to get started.
1. **A Bill of Lading (BOL) number.** This is issued by your carrier. BOL numbers are found on your [bill of lading](https://en.wikipedia.org/wiki/Bill_of_lading) document. Ideally, this will be a shipment that is currently on the water or in terminal, but this is not necessary.
2. **The SCAC of the carrier that issued your bill of lading.** The Standard Carrier Alpha Code of your carrier is used to identify carriers in computer systems and in shipping documents. You can learn more about these [here](https://en.wikipedia.org/wiki/Standard_Carrier_Alpha_Code).
3. **A Terminal49 Account.** If you don't have one yet, [sign up here.](https://app.terminal49.com/register)
4. **An API key.** Sign in to your Terminal49 account and go to your [developer portal page](https://app.terminal49.com/developers) to get your API key.
Not sure which SCAC to use? The [Auto-Detect
Carrier](/api-docs/in-depth-guides/auto-detect-carrier) API can identify it
from your tracking number.
## Track a Shipment
You can try this using the embedded request maker below, or using Postman.
1. Try it below. Click "Headers" and replace YOUR\_API\_KEY with your API key. In the authorization header value.
2. Enter a value for the `request_number` and `scac`. The request number has to be a shipping line booking or master bill of lading number. The SCAC has to be a shipping line scac (see data sources to get a list of valid SCACs)
Note that you can also access sample code, include a cURL template, by clicking the "Code Generation" tab in the Request Maker.
```json http theme={null}
{
"method": "post",
"url": "https://api.terminal49.com/v2/tracking_requests",
"headers": {
"Content-Type": "application/vnd.api+json",
"Authorization": "Token YOUR_API_KEY"
},
"body": "{\r\n \"data\": {\r\n \"attributes\": {\r\n \"request_type\": \"bill_of_lading\",\r\n \"request_number\": \"\",\r\n \"scac\": \"\"\r\n },\r\n \"type\": \"tracking_request\"\r\n }\r\n}"
}
```
## Check Your Tracking Request Succeeded
We have not yet set up a webook to receive status updates from the Terminal49 API, so we will need to manually poll to check if the Tracking Request has succeeded or failed.
> ### Tracking Request Troubleshooting
>
> The most common issue people encounter is that they are entering the wrong number.
>
> Please check that you are entering the Bill of Lading number, booking number, or container number and not internal reference at your company or by your frieght forwarder. You can the number you are supplying by going to a carrier's website and using their tools to track your shipment using the request number. If this works, and if the SCAC is supported by T49, you should able to track it with us.
>
> You can always email us at [support@terminal49.com](mailto:support@terminal49.com) if you have persistent issues.
\*\* Try it below. Click "Headers" and replace `` with your API key.\*\*
```json http theme={null}
{
"method": "get",
"url": "https://api.terminal49.com/v2/tracking_requests",
"headers": {
"Content-Type": "application/vnd.api+json",
"Authorization": "Token YOUR_API_KEY"
}
}
```
## List your Tracked Shipments
If your tracking request was successful, you will now be able to list your tracked shipments.
**Try it below. Click "Headers" and replace YOUR\_API\_KEY with your API key.**
Sometimes it may take a while for the tracking request to show up, but usually no more than a few minutes.
If you had trouble adding your first shipment, try adding a few more.
```json http theme={null}
{
"method": "get",
"url": "https://api.terminal49.com/v2/shipments",
"headers": {
"Content-Type": "application/vnd.api+json",
"Authorization": "Token YOUR_API_KEY"
}
}
```
## List all your Tracked Containers
You can also list out all of your containers, if you'd like to track at that level.
Try it after replacing `` with your API key.
```json http theme={null}
{
"method": "get",
"url": "https://api.terminal49.com/v2/containers",
"headers": {
"Content-Type": "application/vnd.api+json",
"Authorization": "Token YOUR_API_KEY"
}
}
```
## Listening for Updates with Webhooks
The true power of Terminal49's API is that it is asynchronous. You can register a Webhook, which is essentially a callback URL that our systems HTTP Post to when there are updates.
To try this, you will need to first set up a URL on the open web to receive POST requests. Once you have done this, you'll be able to receive status updates from containers and shipments as they happen, which means you don't need to poll us for updates; we'll notify you.
\*\* Try it below. Click "Headers" and replace YOUR\_API\_KEY with your API key.\*\*
Once this is done, any changes to shipments and containers you're tracking in step 2 will now be sent to your webhook URL as Http POST Requests.
View the "Code Generation" button to see sample code.
```json http theme={null}
{
"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 \"*\"\r\n ]\r\n }\r\n }\r\n}"
}
```
# Integrate Rail Container Tracking Data
Source: https://terminal49.com/docs/api-docs/in-depth-guides/rail-integration-guide
This guide provides a comprehensive, step-by-step approach for integrating North American Class-1 rail container tracking data into your systems. Whether you are a shipper or a logistics service provider, this guide will help you track all your rail containers via a single API.
This is a technical article about rail data within Terminal49's API and DataSync.
For a broader overview, including the reasons why you'd want rail visibility and how to use it in the Terminal49 dashboard,
[read our announcement post](https://www.terminal49.com/blog/launching-north-american-intermodal-rail-visibility-on-terminal49/).
## Table of Contents
* [Supported Rail Carriers](#supported-rail-carriers)
* [Supported Rail Events and Data Attributes](#supported-rail-events-and-data-attributes)
* [Rail-specific Transport Events](#rail-specific-transport-events)
* [Webhook Notifications](#webhook-notifications)
* [Rail Container Attributes](#rail-container-attributes)
* [Integration Methods](#integration-methods)
* [Integration via API](#a-integration-via-api)
* [Integration via DataSync](#b-integration-via-datasync)
## Supported Rail Carriers
Terminal49 container tracking platform integrates with all North American Class-1 railroads that handle container shipping, providing comprehensive visibility into your rail container movements.
* BNSF Railway
* Canadian National Railway (CN)
* Canadian Pacific Railway (CP)
* CSX Transportation
* Norfolk Southern Railway (NS)
* Union Pacific Railroad (UP)
By integrating with these carriers, Terminal49 ensures that you have direct access to critical tracking data, enabling better decision-making and operational efficiency.
## Supported Rail Events and Data Attributes
Terminal49 seamlessly tracks your containers as they go from container ship, to ocean terminal, to rail carrier.
We provide a [set of Transport Events](#webhook-notifications) that let you track the status of your containers as they move through the rail system. You can be notified by webhook whenever these events occur.
We also provide a set of attributes [on the container model](/api-docs/api-reference/containers/get-a-container) that let you know the current status of your container at any given time, as well as useful information such as ETA, pickup facility, and availability information.
### Rail-Specific Transport Events
There are several core Transport Events that occur on most rail journeys. Some rail carriers do not share all events, but in general these are the key events for a container.
```mermaid theme={null}
graph LR
A[Rail Loaded] --> B[Rail Departed]
B --> C[Arrived at Inland Destination]
C --> D[Rail Unloaded]
D --> G[Available for Pickup]
G --> E[Full Out]
E --> F[Empty Return]
```
`Available for Pickup`, `Full Out` and `Empty Return` are not specific to rail, but are included here since they are a key part of the rail journey.
### Webhook Notifications
Terminal49 provides webhook notifications to keep you updated on key Transport Events in a container's rail journey. These notifications allow you to integrate near real-time tracking data directly into your applications.
Here's a list of the rail-specific events which support webhook notifications:
| Transport Event | Webhook Notification | Description | Example |
| ----------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Rail Loaded | `container.transport.rail_loaded` | The container is loaded onto a railcar. | Example |
| Rail Departed | `container.transport.rail_departed` | The container departs on the railcar (not always from port of discharge). | Example |
| Rail Arrived | `container.transport.rail_arrived` | The container arrives at a rail terminal (not always at the destination terminal). | Example |
| Arrived At Inland Destination | `container.transport.arrived_at_inland_destination` | The container arrives at the destination terminal. | Example |
| Rail Unloaded | `container.transport.rail_unloaded` | The container is unloaded from a railcar. | Example |
There's also a set of events that are triggered when the status of the container at the destination rail terminal changes. For containers without rail, they would have been triggered at the ocean terminal.
| Transport Event | Webhook Notification | Description | Example |
| --------------- | ------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| Full Out | `container.transport.full_out` | The full container leaves the rail terminal. | Example |
| Empty In | `container.transport.empty_in` | The empty container is returned to the terminal. | Example |
Finally, we have a webhook notifications for when the destination ETA changes.
| Transport Event | Webhook Notification | Description | Example |
| ----------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Estimated Destination Arrival | `container.transport.estimated.arrived_at_inland_destination` | Estimated time of arrival for the container at the destination rail terminal. | Example |
Integrate these notifications by subscribing to the webhooks and handling the incoming data to update your systems.
#### Rail Container Attributes
The following are new attributes that are specific to rail container tracking.
* **pod\_rail\_loaded\_at**: Time when the container is loaded onto a railcar at the POD.
* **pod\_rail\_departed\_at**: Time when the container departs from the POD.
* **ind\_eta\_at**: Estimated Time of Arrival at the inland destination.
* **ind\_ata\_at**: Actual Time of Arrival at the inland destination.
* **ind\_rail\_unloaded\_at**: Time when the container is unloaded from rail at the inland destination.
* **ind\_facility\_lfd\_on**: Last Free Day for demurrage charges at the inland destination terminal.
* **pod\_rail\_carrier\_scac**: SCAC code of the rail carrier that picks up the container from the POD (this could be different than the rail carrier that delivers to the inland destination).
* **ind\_rail\_carrier\_scac**: SCAC code of the rail carrier that delivers the container to the inland destination.
These attributes can be found on [container objects](/api-docs/api-reference/containers/get-a-container).
## Integration Methods
There are two methods to integrate Terminal49's rail tracking data programmatically: via API and DataSync.
### A. Integration via API
Terminal49 provides a robust API that allows you to programmatically access rail container tracking data and receive updates via webhooks. You will receive rail events and attributes alongside events and attributes from the ocean terminal and carrier.
[Here's a step-by-step guide to get started](/api-docs/getting-started/start-here)
### B. Integration via DataSync
Terminal49's DataSync service automatically syncs up-to-date tracking data with your system. The rail data will be in the same tables alongside the ocean terminal and carrier data.
[Learn more about DataSync](/datasync/overview)
# Rate Limiting
Source: https://terminal49.com/docs/api-docs/in-depth-guides/rate-limiting
## Overview
Terminal49 API implements rate limiting to ensure fair usage and maintain service quality for all users.
## Rate Limit Details
* **Limit**: 100 requests per minute per account
* **Scope**: Applied per API key/account
* **Window**: Rolling 60-second window
## Rate Limit Response
When you exceed the rate limit, the API will return:
**HTTP Status Code**: `429 Too Many Requests`
**Response Headers**:
* `Retry-After`: Number of seconds to wait before making another request
**Response Body**:
```json theme={null}
{
"errors": [
{
"status": "429",
"title": "Too Many Requests",
"detail": "Your account has exceeded its API rate limit. Please reduce request frequency or contact support to increase your limit. Consider using webhooks for real-time updates instead of polling."
}
]
}
```
## Best Practices
### 1. Use Webhooks Instead of Polling
The most effective way to avoid rate limits is to use **webhooks** for real-time updates instead of repeatedly polling the API:
* Configure webhooks to receive push notifications when shipment data changes
* Eliminates the need for frequent polling
* Provides instant updates without consuming your rate limit
* See the [Webhooks](/api-docs/api-reference/webhooks) section for setup instructions
### 2. Implement Exponential Backoff
If you receive a `429` response:
1. Check the `Retry-After` header
2. Wait for the specified number of seconds
3. Implement exponential backoff for subsequent failures
4. Don't retry immediately, as this will consume your limit further
### 3. Batch Your Requests
* Use list endpoints with filtering instead of multiple individual requests
* Leverage the [`include` parameter](/api-docs/in-depth-guides/including-resources) to fetch related resources in a single request
* Cache responses when appropriate to reduce redundant calls
### 4. Monitor Your Usage
* Track your request patterns
* Identify and optimize high-frequency operations
* Consider spreading requests over time rather than bursting
## Need a Higher Limit?
If your use case requires a higher rate limit:
1. **Evaluate webhook usage first** - Most polling use cases can be replaced with webhooks
2. **Contact support** at [support@terminal49.com](mailto:support@terminal49.com)
3. **Provide details** about your use case and expected request volume
4. **Our team will work with you** to find an appropriate solution
## Example: Handling Rate Limits
Here's an example of how to properly handle rate limit responses in Python:
```python theme={null}
import time
import requests
def make_request_with_retry(url, headers, max_retries=3):
"""
Make an API request with automatic retry on rate limit.
Args:
url: The API endpoint URL
headers: Request headers including Authorization
max_retries: Maximum number of retry attempts
Returns:
Response object if successful
Raises:
Exception: If max retries exceeded
"""
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 429:
# Get the retry-after value from header (default to 60 seconds)
retry_after = int(response.headers.get('Retry-After', 60))
print(f"Rate limited. Waiting {retry_after} seconds...")
time.sleep(retry_after)
continue
# Return response for any other status code
return response
raise Exception("Max retries exceeded")
# Example usage
headers = {
'Authorization': 'Token YOUR_API_KEY'
}
response = make_request_with_retry(
'https://api.terminal49.com/v2/shipments',
headers
)
```
## Tips for High-Volume Applications
If you're building a high-volume application:
* **Design for webhooks from the start**: Don't rely on polling for data updates
* **Implement request queuing**: Spread your requests evenly across the rate limit window
* **Use pagination efficiently**: Fetch larger pages less frequently rather than small pages frequently
* **Cache aggressively**: Store and reuse data that doesn't change frequently
* **Monitor rate limit headers**: Some APIs provide headers indicating remaining quota (check our response headers)
# Vessel and Container Route Data
Source: https://terminal49.com/docs/api-docs/in-depth-guides/routing
This guide explains how to access detailed container routes and vessel positions data (historical and future positions) using Terminal49 APIs.
This is a technical article describing how to use our Routing Data feature, using the map as an example.
Routing Data (Container Map GeoJSON API) is a paid feature. These APIs are subject to additional terms of usage and pricing. If you are interested in using these APIs, please contact [sales@terminal49.com](mailto:sales@terminal49.com).
## Table of Contents
* [Overview](#overview)
* [Getting Started](#getting-started)
* [Understanding the Response](#understanding-the-response)
* [GeoJSON FeatureCollection Structure](#geojson-featurecollection-structure)
* [Feature Types](#feature-types)
* [Port](#port)
* [Current Vessel](#current-vessel)
* [Past Vessel Locations](#past-vessel-locations)
* [Estimated Full Leg](#estimated-full-leg)
* [Estimated Partial Leg](#estimated-partial-leg)
* [Building Your Map](#building-your-map)
* [Use Cases](#use-cases)
* [Recommendations and Best Practices](#recommendations-and-best-practices)
* [Frequently Asked Questions](#frequently-asked-questions)
## Overview
The `GET /v2/containers/{id}/map_geojson` endpoint provides all the map-related data for a container in a single GeoJSON response.
The endpoint returns a GeoJSON FeatureCollection containing:
* **Port locations** (Point geometries): Port of lading (POL), port of discharge (POD), and transshipment ports (TS1, TS2, etc.)
* **Current vessel location** (Point geometry): The current position of the vessel if the container is currently at sea
* **Past vessel paths** (LineString geometries): Historical positions of vessels for completed and in-progress legs of the journey
* **Estimated future paths** (LineString geometries): Predicted vessel routes for upcoming or in-progress legs
## Getting Started
To retrieve the map data for a container, make a simple GET request to the endpoint:
```shell Request theme={null}
curl --request GET \
--url https://api.terminal49.com/v2/containers/{id}/map_geojson \
--header "Authorization: Token YOUR_API_TOKEN"
```
The response is a standard GeoJSON FeatureCollection that can be directly used with most mapping libraries (Leaflet, Mapbox GL, Google Maps, etc.).
```json theme={null}
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
100.896831042,
13.065302386
]
},
"properties": {
"feature_type": "port",
"ports_sequence": 1,
"ports_total": 3,
"label": "POL",
"name": "Laem Chabang",
// ... more properties
}
},
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[
100.868768333,
13.07306
],
[
100.839155,
13.079318333
],
// ... more coordinates
[
118.03862,
24.440998333
]
]
},
"properties": {
"feature_type": "past_vessel_locations",
"vessel_id": "87a12f43-766c-4078-89bc-ac6595082f7b",
// ... more path properties
}
},
// ... more features
]
}
```
## Understanding the Response
### GeoJSON FeatureCollection Structure
The response follows the [GeoJSON specification](https://geojson.org/) and contains:
* `type`: Always `"FeatureCollection"`
* `features`: An array of GeoJSON Feature objects, each representing a map element (port, vessel, or route path)
Each feature contains:
* `type`: Always `"Feature"`
* `geometry`: A GeoJSON geometry object (Point or LineString)
* `properties`: An object containing metadata specific to the feature type
### Feature Types
The `properties.feature_type` field identifies what each feature represents. The following feature types are available:
#### Port
Geometry Type: `Point`
Port features represent all ports in the container's route: the port of lading (POL), port of discharge (POD), and any transshipment ports (TS1, TS2, etc.).
```json theme={null}
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
100.896831042,
13.065302386
]
},
"properties": {
"feature_type": "port",
"ports_sequence": 1,
"ports_total": 3,
"location_id": "c5adae24-6fd4-4720-8813-976cf206feb1",
"location_type": "Port",
"name": "Laem Chabang",
"state_abbr": "20",
"state": null,
"country_code": "TH",
"country": "Thailand",
"time_zone": "Asia/Bangkok",
"inbound_eta_at": null,
"inbound_ata_at": null,
"outbound_etd_at": null,
"outbound_atd_at": "2025-11-08T00:44:52Z",
"label": "POL",
"updated_at": "2025-12-11T09:01:08Z"
}
}
```
| Property | Type | Description |
| ----------------- | -------------- | ------------------------------------------------------------------- |
| `feature_type` | string | Always `"port"` |
| `ports_sequence` | integer | The sequence number of this port in the route (1 = POL, last = POD) |
| `ports_total` | integer | Total number of ports in the route |
| `location_id` | string | Unique identifier for the port location |
| `location_type` | string | Always `"Port"` |
| `name` | string | Name of the port |
| `state_abbr` | string \| null | State abbreviation (if applicable) |
| `state` | string \| null | State name (if applicable) |
| `country_code` | string | ISO country code |
| `country` | string | Country name |
| `time_zone` | string | IANA timezone identifier |
| `label` | string | Port label: `"POL"`, `"POD"`, or `"TS1"`, `"TS2"`, etc. |
| `inbound_eta_at` | string \| null | Estimated time of arrival (ISO 8601) |
| `inbound_ata_at` | string \| null | Actual time of arrival (ISO 8601) |
| `outbound_etd_at` | string \| null | Estimated time of departure (ISO 8601) |
| `outbound_atd_at` | string \| null | Actual time of departure (ISO 8601) |
| `updated_at` | string \| null | Last update timestamp from the shipment (ISO 8601) |
#### Current Vessel
Geometry Type: `Point`
This feature is only present when the container is currently on a vessel at sea. It represents the vessel's current position.
```json theme={null}
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-131.128473333,
31.023033333
]
},
"properties": {
"feature_type": "current_vessel",
"ports_sequence": 2,
"vessel_id": "93fc5dce-4c7f-4089-bd28-f20cd9202ab0",
"vessel_name": "ZIM BANGKOK",
"vessel_imo": "9936525",
"voyage_number": "13E",
"vessel_location_timestamp": "2025-12-11T11:46:03Z",
"vessel_location_heading": 108,
"vessel_location_speed": 21,
"departure_port_id": "ed64d446-9098-420c-ab08-c127e62509fe",
"departure_port_name": "Xiamen",
"departure_port_state_abbr": "FJ",
"departure_port_state": null,
"departure_port_country_code": "CN",
"departure_port_country": "China",
"departure_port_label": "TS1",
"departure_port_atd": "2025-11-19T16:00:00Z",
"departure_port_time_zone": "Asia/Shanghai",
"arrival_port_id": "6129528d-846e-4571-ae16-b5328a4285ab",
"arrival_port_name": "Savannah",
"arrival_port_state_abbr": "GA",
"arrival_port_state": "Georgia",
"arrival_port_country_code": "US",
"arrival_port_country": "United States",
"arrival_port_label": "POD",
"arrival_port_eta": "2025-12-31T05:00:00Z",
"arrival_port_time_zone": "America/New_York"
}
}
```
| Property | Type | Description |
| ----------------------------- | -------------- | ----------------------------------------------------- |
| `feature_type` | string | Always `"current_vessel"` |
| `ports_sequence` | integer | Sequence number of the departure port for this leg |
| `vessel_id` | string | Unique identifier for the vessel |
| `vessel_name` | string | Name of the vessel |
| `vessel_imo` | string | IMO number of the vessel |
| `voyage_number` | string \| null | Voyage number for this leg |
| `vessel_location_timestamp` | string | Timestamp of the vessel position (ISO 8601) |
| `vessel_location_heading` | number \| null | Vessel heading in degrees (0-360) |
| `vessel_location_speed` | number \| null | Vessel speed in knots |
| `departure_port_id` | string | ID of the port the vessel departed from |
| `departure_port_name` | string | Name of the departure port |
| `departure_port_state_abbr` | string \| null | State abbreviation of departure port |
| `departure_port_state` | string \| null | State name of departure port |
| `departure_port_country_code` | string | Country code of departure port |
| `departure_port_country` | string | Country name of departure port |
| `departure_port_label` | string | Label of departure port (POL, POD, TS1, etc.) |
| `departure_port_atd` | string \| null | Actual time of departure from the port (ISO 8601) |
| `departure_port_time_zone` | string | Timezone of departure port |
| `arrival_port_id` | string \| null | ID of the next port the vessel is heading to |
| `arrival_port_name` | string \| null | Name of the arrival port |
| `arrival_port_state_abbr` | string \| null | State abbreviation of arrival port |
| `arrival_port_state` | string \| null | State name of arrival port |
| `arrival_port_country_code` | string \| null | Country code of arrival port |
| `arrival_port_country` | string \| null | Country name of arrival port |
| `arrival_port_label` | string \| null | Label of arrival port (POL, POD, TS1, etc.) |
| `arrival_port_eta` | string \| null | Estimated time of arrival at the next port (ISO 8601) |
| `arrival_port_time_zone` | string \| null | Timezone of arrival port |
#### Past Vessel Locations
Geometry Type: `LineString`
These features represent the actual historical paths taken by vessels for completed and in-progress legs of the journey. Each LineString contains a series of coordinates showing where the vessel traveled between two ports.
```json theme={null}
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[
100.868768333,
13.07306
],
[
100.839155,
13.079318333
],
// ... many more coordinates
[
118.03862,
24.440998333
]
]
},
"properties": {
"feature_type": "past_vessel_locations",
"ports_sequence": 1,
"vessel_id": "87a12f43-766c-4078-89bc-ac6595082f7b",
"start_time": "2025-11-08T00:44:52Z",
"end_time": "2025-11-15T16:00:00Z",
"point_count": 546,
"outbound_atd_at": "2025-11-08T00:44:52Z",
"inbound_ata_at": "2025-11-15T16:00:00Z",
"inbound_eta_at": null
}
}
```
| Property | Type | Description |
| ----------------- | -------------- | ------------------------------------------------------------ |
| `feature_type` | string | Always `"past_vessel_locations"` |
| `ports_sequence` | integer | Sequence number of the departure port for this leg |
| `vessel_id` | string | Unique identifier for the vessel that traveled this path |
| `start_time` | string | Start timestamp of the path (ISO 8601) |
| `end_time` | string | End timestamp of the path (ISO 8601) |
| `point_count` | integer | Number of coordinate points in the LineString |
| `outbound_atd_at` | string \| null | Actual time of departure from the origin port (ISO 8601) |
| `inbound_ata_at` | string \| null | Actual time of arrival at the destination port (ISO 8601) |
| `inbound_eta_at` | string \| null | Estimated time of arrival at the destination port (ISO 8601) |
#### Estimated Full Leg
Geometry Type: `LineString`
These features represent predicted vessel paths for future legs that have not yet started. The LineString shows the estimated route between two ports.
```json theme={null}
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[55.059917502, 24.987353081],
[55.234, 24.856],
[56.123, 24.567],
// ... intermediate estimated points
[79.851136851, 6.942742853]
]
},
"properties": {
"feature_type": "estimated_full_legs",
"ports_sequence": 2,
"previous_port_id": "94892d07-ef8f-4f76-a860-97a398c2c177",
"next_port_id": "818ef299-aed3-49c9-b3f7-7ee205f697f6",
"point_count": 87
}
}
```
| Property | Type | Description |
| ------------------ | ------- | -------------------------------------------------- |
| `feature_type` | string | Always `"estimated_full_legs"` |
| `ports_sequence` | integer | Sequence number of the departure port for this leg |
| `previous_port_id` | string | ID of the origin port |
| `next_port_id` | string | ID of the destination port |
| `point_count` | integer | Number of coordinate points in the LineString |
#### Estimated Partial Leg
Geometry Type: `LineString`
This feature represents the predicted path from the vessel's current position to the next port. It is only present when the container is currently on a vessel at sea.
```json theme={null}
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[
-131.128473333,
31.023033333
],
[
-130.9177,
30.67224
],
// ... many more coordinates
[
-80.91232,
32.03728
]
]
},
"properties": {
"feature_type": "estimated_partial_leg",
"ports_sequence": 2,
"current_port_id": "ed64d446-9098-420c-ab08-c127e62509fe",
"next_port_id": "6129528d-846e-4571-ae16-b5328a4285ab",
"point_count": 364
}
}
```
| Property | Type | Description |
| ----------------- | ------- | -------------------------------------------------- |
| `feature_type` | string | Always `"estimated_partial_leg"` |
| `ports_sequence` | integer | Sequence number of the departure port for this leg |
| `current_port_id` | string | ID of the port the vessel departed from |
| `next_port_id` | string | ID of the next port the vessel is heading to |
| `point_count` | integer | Number of coordinate points in the LineString |
**Note:** This feature is only present when there is a `current_vessel` feature. The LineString starts from the vessel's current position (which matches the `current_vessel` feature coordinates) and extends to the next port.
## Building Your Map
To visualize a container's journey using the GeoJSON response on your own map (similar to [the embeddable map](/api-docs/in-depth-guides/terminal49-map)):
1. **Load the GeoJSON data** into your mapping library (Leaflet, Mapbox GL, Google Maps, etc.)
2. **Filter features by type** to style them differently:
* **Ports**: Display as markers with labels (POL, POD, TS1, etc.)
* **Current vessel**: Display as a special marker (e.g., a ship icon) with vessel information
* **Past vessel locations**: Display as solid lines (representing completed journeys)
* **Estimated partial leg** and **Estimated full legs**: Display as dashed lines (representing future predictions)
3. **Use the properties** to add interactivity:
* Show port details (name, country, timestamps) on click/hover
* Display vessel information (name, IMO, speed, heading) for the current vessel
* Show leg information (departure/arrival times, vessel ID) for path segments
```javascript theme={null}
// Fetch the GeoJSON data
fetch('https://api.terminal49.com/v2/containers/{id}/map_geojson', {
headers: {
'Authorization': 'Token YOUR_API_TOKEN'
}
})
.then(response => response.json())
.then(geojson => {
// Create a map
const map = L.map('map').setView([20, 70], 3);
// Add base layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// Process each feature
geojson.features.forEach(feature => {
const props = feature.properties;
if (props.feature_type === 'port') {
// Add port marker
const marker = L.marker([feature.geometry.coordinates[1], feature.geometry.coordinates[0]])
.addTo(map)
.bindPopup(`${props.label} ${props.name}`);
} else if (props.feature_type === 'current_vessel') {
// Add current vessel marker
const vesselIcon = L.icon({
iconUrl: 'vessel-icon.png',
iconSize: [32, 32]
});
L.marker([feature.geometry.coordinates[1], feature.geometry.coordinates[0]], {icon: vesselIcon})
.addTo(map)
.bindPopup(`${props.vessel_name} Speed: ${props.vessel_location_speed} knots`);
} else if (props.feature_type === 'past_vessel_locations') {
// Add past path as solid line
const coordinates = feature.geometry.coordinates.map(coord => [coord[1], coord[0]]);
L.polyline(coordinates, {color: 'green', weight: 3})
.addTo(map);
} else if (props.feature_type === 'estimated_full_legs' || props.feature_type === 'estimated_partial_leg') {
// Add estimated path as dashed line
const coordinates = feature.geometry.coordinates.map(coord => [coord[1], coord[0]]);
L.polyline(coordinates, {color: 'blue', weight: 2, dashArray: '10, 10'})
.addTo(map);
}
});
});
```
## Use Cases
Integrating Terminal49's Vessel and Container Route APIs enables a variety of advanced capabilities:
* **Track Complete Shipment Journeys Visually:** Monitor shipments across multiple legs on a map, from the port of lading to the port of discharge, including all transshipment points.
* **Identify Transshipment Details Geographically:** Clearly see where transshipments occur and the routes taken between them.
* **Correlate Timestamps with Locations:** Visually connect ETDs, ETAs, ATDs, and ATAs for every leg with their geographical points on the map for precise planning and exception management.
* **Improve Internal Logistics Dashboards:** Offer your operations team a clear visual overview of all ongoing shipments and their current locations.
## Recommendations and Best Practices
* **Polling Intervals**: For active containers (currently at sea), we recommend refreshing the map data up to once per hour to get updated vessel positions. For containers that have completed their journey, you can cache the data as it won't change.
* **Error Handling**: Implement proper error handling for API requests. If a container doesn't have route data yet, the endpoint will return an empty FeatureCollection (`{"type": "FeatureCollection", "features": []}`).
If you decide to create your own map:
* **Data Layering:** Consider layering information on your map. Start with basic port markers and paths, then add details like vessel names, ETAs, or status on hover or click.
* **Map Library Integration:** Use a robust mapping library (e.g., Leaflet, Mapbox GL, Google Maps, OpenLayers) to handle the rendering of markers, lines, and map interactivity.
* **Styling Guidelines**:
* Use distinct colors/styles for different feature types (ports, current vessel, past paths, estimated paths)
* Consider using dashed lines for estimated paths and solid lines for completed paths
* Add labels to port markers showing POL, POD, TS1, etc.
* Display vessel information in popups or info panels
* **Data Interpretation**:
* The `ports_sequence` property helps you understand the order of ports in the journey
* Use `inbound_ata_at` and `outbound_atd_at` to determine which legs are completed
* The presence of a `current_vessel` feature indicates the container is currently at sea
* **Handling Antimeridian Crossings**: When container routes cross the International Date Line (antimeridian at ±180° longitude), standard map projections can display routes incorrectly, showing lines that wrap around the entire globe. For mapping libraries that don't natively handle antimeridian crossings, the recommended approach is to: (1) detect and split crossing LineStrings into separate segments, and (2) render map features across multiple world views (standard, East, and West) as needed. Below are more details:
* **Detection**: Identify LineString features (past vessel locations or estimated paths) that cross the antimeridian by checking if consecutive coordinates have a longitude difference greater than 180°.
* **Single Crossing Solution**: When one antimeridian crossing is detected:
* Split the route into two segments: features before the crossing (based on `ports_sequence`) are drawn in the standard world view
* Features after the crossing are drawn in an extended world view (East or West, depending on crossing direction)
* Split the crossing LineString into two separate lines: one ending at the antimeridian in the standard view, and one starting from the antimeridian in the extended view
* **Multiple Crossings**: For routes with more than one antimeridian crossing (rare but possible), render all features across three world views (standard, East, and West) with duplicated features. Split all crossing lines to prevent lines from wrapping across the globe.
* **No Crossings**: If no antimeridian crossings are detected, render all features in the standard world view without any special handling.
## Frequently Asked Questions
**Q: How up-to-date is the vessel position data?**
A: Vessel location data is updated every 15 minutes, although that does not guarantee there will be a new position every 15 minutes due to factors like whether the vessel is transmitting or within range of a satellite or base station.
**Q: How accurate are the future predictions?**
A: Predicted future positions are based on algorithms and historical data. Their accuracy can vary based on many factors such as temporary deviations, weather conditions, seasonality, or how frequently the shipping lane is used.
**Q: What if a vessel deviates from the predicted path?**
A: Predicted paths are estimates. The historical path (once available as a `past_vessel_locations` feature) will show the actual route taken. Regularly refreshing data for active shipments is key to getting the most accurate information.
**Q: Why don't I see a `current_vessel` feature for my container?**
A: The `current_vessel` feature is only present when:
* The container is currently on a vessel at sea
* The vessel has departed from a port (`outbound_atd_at` is present)
* The next port hasn't been reached yet (`inbound_ata_at` is not present)
* A valid vessel location can be retrieved
**Q: Can I get map data for multiple containers at once?**
A: Currently, the endpoint returns data for a single container. You'll need to make separate API calls for each container you want to display on your map.
**Q: What coordinate system is used?**
A: All coordinates follow the GeoJSON standard: `[longitude, latitude]` in WGS84 (EPSG:4326) format.
**Q: Are the LineString coordinates simplified?**
A: The endpoint applies simplification to reduce the number of points in LineStrings for better performance. The simplification tolerance can vary, but the paths remain accurate for visualization purposes.
# Terminal49 Map Embed Guide
Source: https://terminal49.com/docs/api-docs/in-depth-guides/terminal49-map
The Terminal49 Map allows you to embed real-time visualized container tracking on your website with just a few lines of code.
### Prerequisites
* A Terminal49 account.
* A Publishable API key, you can get one by reaching out to us at [support@terminal49.com](mailto:support@terminal49.com).
* Familiarity with our [Shipments API](/api-docs/api-reference/shipments/list-shipments) and [Containers API](/api-docs/api-reference/containers/list-containers).
In the following examples we'll be passing a `containerId` and `shipmentId` variables to the embedded map.
They relate to `id` attributes of the container and shipment objects that are returned by the API.
### How do I embed the map on my website?
Once you have the API Key, you can embed the map on your website.
1. Copy and paste the code below and insert it on your website.
Once loaded, this will make the map code available through the global `window` object.
Just before the closing `` tag, add the following link tag to load the map styles.
```html theme={null}
Document
```
Just before the closing `