# Container Tracking API, Terminal49 Shipments, ETAs and Events (`nabeelbaghoor/ocean-container-shipment-tracking-api`) Actor

Track ocean containers and shipments in your Terminal49 account: bill of lading status, carrier ETA and arrival, vessel, terminal holds, fees and last free day, transport and estimated events, route map, tracking requests, shipping lines, ports and vessels. Read only. Bring your own key.

- **URL**: https://apify.com/nabeelbaghoor/ocean-container-shipment-tracking-api.md
- **Developed by:** [Nabeel Hassan](https://apify.com/nabeelbaghoor) (community)
- **Categories:** Developer tools, Business, Travel
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 shipment returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Container Tracking API for Terminal49: Shipments, ETAs and Milestones

Read the ocean shipments and containers tracked in your Terminal49 account through the Terminal49 API with your own API key, as clean dataset rows: ETAs, arrivals, vessels, terminal holds, fees, last free days, milestones and route maps.

Terminal49 is a container tracking platform for importers, freight forwarders, customs brokers and drayage carriers. It tracks bills of lading, bookings and container numbers across the major ocean carriers (shipping lines / VOCCs), US terminals and North American Class I rail, and normalizes every milestone from empty out at origin to empty return at destination. This actor wraps the read only part of its documented API v2 on api.terminal49.com, so you can pull ETAs into Google Sheets, BI dashboards, ERP or TMS systems, demurrage and detention alerts, or AI agents without writing a JSON:API client. Bring your own API key.

BYOK, not live tested: every route, parameter and field comes from Terminal49's published OpenAPI definition (terminal49.com/docs/openapi.json) and API reference, checked on 2026-10-25. The routes were probed with an invalid key on the same day. The actor has not been run against a live Terminal49 key.

### What it does

- **Shipments**: one row per tracked shipment with bill of lading or booking number, shipping line SCAC and name, port of lading and port of discharge (UN/LOCODE and name), final destination, POD vessel name, IMO and voyage, ETD, ATD, current ETA, original ETA, ATA and whether tracking is still active. Filter by number, free text, port, arrival window, creation date, voyage status, tags and tracking state.
- **Containers**: one row per container with ISO 6346 number, current status (on ship, available, picked up, empty returned and more), equipment type and size, holds and fees at the POD terminal, availability for pickup, last free day and import deadlines (terminal, line and rail), yard location, rail and inland milestones, plus its shipment's ports, ETA, vessel and the POD terminal name. Filter by status, carrier, port, arrival, last free day and holds or fees.
- **Container transport events**: the normalized past milestones of each container (empty out, full in, vessel loaded, departed, transshipment, arrived, discharged, rail loaded and unloaded, full out, empty in, last free day changes) with time, timezone, UN/LOCODE, port name, vessel and terminal.
- **Container raw and estimated events**: the carrier's own event list, including future estimated milestones, with the original carrier wording.
- **Container route map**: each GeoJSON feature of the container's route, ports in sequence with ETAs and ATAs, the current vessel position, past vessel tracks and estimated future paths (needs Terminal49 Routing Data).
- **Tracking requests**: what your account asked Terminal49 to track, with status, failed reason and retries, to find numbers that never started tracking.
- **Shipping lines, Ports, Terminals, Vessels**: supported carriers with SCACs and tracking support, ports by UN/LOCODE with coordinates and time zone, terminals with FIRMS, SMDG and BIC codes, and vessels by IMO with their latest position.
- **Container numbers just work**: in the event and route modes, give container numbers and the actor finds their Terminal49 IDs first, without charging for that lookup.
- **Pay only for results**: unknown numbers, empty lists and refused lookups are recorded as a free row that says why, never charged.

### How to use it

1. In Terminal49, create an API key under Developers at app.terminal49.com/developers/api-keys. Reading data needs a paid API plan or the 7 day API trial; a free plan key can only create tracking requests.
2. Paste the key into the API key field (or set the DATA_API_KEY secret). It is sent only to api.terminal49.com in the `Authorization: Token` header and is removed from everything the actor logs.
3. Pick a mode, add filters, numbers or IDs, and start the run. Download the rows as JSON, CSV or Excel, or call the actor from your own code or a schedule.

Containers must already be tracked in your Terminal49 account (through its web app, its API or email). This actor never creates tracking requests, because each one starts paid tracking on your Terminal49 plan and a repeated run would file duplicates.

A missing or rejected key, a free plan key, or an account without the Routing Data feature ends the run cleanly with a status message that says what to change.

### Input

| Field | Modes | Description |
| --- | --- | --- |
| `mode` | all | `shipments` (default), `containers`, `transportEvents`, `rawEvents`, `containerRoute`, `trackingRequests`, `shippingLines`, `ports`, `terminals` or `vessels` |
| `shipmentNumbers`, `shipmentIds` | Shipments | Bill of lading or booking numbers (exact), or Terminal49 shipment UUIDs |
| `searchText` | Shipments, Tracking requests | Prefix search on shipment, reference and container numbers |
| `trackingState` | Shipments, Containers | Any, actively tracked or tracking stopped |
| `voyageStatus`, `tags`, `shipmentSort` | Shipments | Arrived or on ship, your shipment tags, sort order |
| `polCodes`, `podCodes` | Shipments, Containers | Port of lading and port of discharge UN/LOCODEs |
| `arrivalFrom`, `arrivalTo` | Shipments, Containers | POD arrival window, actual or estimated |
| `createdFrom`, `createdTo` | Shipments, Containers, Tracking requests | Creation date window |
| `containerNumbers`, `containerIds` | Containers, events, route | ISO 6346 container numbers or Terminal49 container UUIDs |
| `currentStatuses`, `carrierScacs`, `onlyHoldsOrFees`, `lfdFrom`, `lfdTo` | Containers | Status, carrier SCAC, holds or fees, last free day window |
| `requestStatus` | Tracking requests | Created, pending, failed or cancelled (`carrierScacs` takes one SCAC here) |
| `portCodes`, `terminalIds`, `vesselImos` | Ports, Terminals, Vessels | What to look up |
| `maxResults` | all | Most rows to return and charge for |
| `requestsPerMinute` | all | Pacing; Terminal49 allows 100 requests per minute per key |
| `apiKey` | all | Your Terminal49 API key (secret) |

### Output

Every row has flat columns that are easy to filter: `containerNumber`, `containerId`, `shipmentId`, `billOfLading`, `carrierScac`, `status`, `polLocode`, `podLocode`, `podEtaAt`, `podAtaAt`, `destinationLocode`, `vesselName`, `vesselImo`, `event`, `eventTime`, `locationLocode`, `locationName`, `terminalName`, `lastFreeDay`, `availableForPickup`, `featureType`, `latitude`, `longitude`, `name` and `code`. The full Terminal49 record is kept in `attributes`, and its relationships as IDs in `relationshipIds`. Times are UTC, as Terminal49 stores them, with the matching IANA timezone in `attributes`. A refused or empty lookup has `found` set to false and a `note` with Terminal49's own words.

Example container row (values from the Terminal49 API reference samples):

```json
{
    "mode": "containers",
    "modeLabel": "Containers",
    "recordType": "container",
    "query": "CAIU7432986",
    "found": true,
    "id": "55a700e4-7005-45a9-92fd-1ff38641dbd9",
    "containerNumber": "CAIU7432986",
    "containerId": "55a700e4-7005-45a9-92fd-1ff38641dbd9",
    "shipmentId": "02b1bd6f-407c-45bb-8645-06e7ee34e7e3",
    "billOfLading": "MEDUF5399896",
    "carrierScac": "MSCU",
    "status": "available",
    "polLocode": "FRLEH",
    "podLocode": "USNYC",
    "podEtaAt": null,
    "podAtaAt": null,
    "destinationLocode": "USIND",
    "vesselName": "MSC ANISHA R.",
    "vesselImo": "9227297",
    "lastFreeDay": "2024-07-07T04:00:00Z",
    "availableForPickup": true,
    "attributes": {
        "number": "CAIU7432986",
        "equipment_type": "dry",
        "equipment_length": 40,
        "equipment_height": "high_cube",
        "pod_discharged_at": "2024-06-22T04:00:00Z",
        "holds_at_pod_terminal": [],
        "fees_at_pod_terminal": [],
        "pod_rail_carrier_scac": "CSXT",
        "import_deadlines": { "pickup_lfd_terminal": "2024-07-07T04:00:00Z", "pickup_lfd_rail": null, "pickup_lfd_line": "2024-07-07T04:00:00Z" },
        "current_status": "available"
    },
    "relationshipIds": { "shipment": "02b1bd6f-407c-45bb-8645-06e7ee34e7e3", "pickup_facility": null, "pod_terminal": "b859f5c3-8515-41da-bf20-39c0a5ada887" },
    "retrievedAt": "2026-10-25T12:00:00.000Z",
    "note": null
}
```

Transport event rows carry `event` (such as `container.transport.vessel_loaded`), `eventTime`, `locationLocode`, `locationName`, `vesselName` and `terminalName`. Raw event rows carry the carrier's `original_event` in `attributes` and `status` set to `actual` or `estimated`. Route rows carry `featureType` (`port`, `current_vessel`, `past_vessel_locations` and estimated paths), the point coordinates and the full GeoJSON geometry in `attributes.geometry`.

### Pricing

| Event | Price | Charged for |
| --- | --- | --- |
| `shipment` | 0.01 USD | Each shipment |
| `container` | 0.01 USD | Each container, with its shipment and terminal |
| `tracking-event` | 0.005 USD | Each transport event or raw carrier event |
| `route-feature` | 0.005 USD | Each route map feature |
| `tracking-request` | 0.005 USD | Each tracking request |
| `reference-record` | 0.005 USD | Each shipping line, port or terminal |
| `vessel-position` | 0.01 USD | Each vessel with its latest position |

Unknown numbers, empty lists, refused lookups and the container number to ID lookup are never charged. Your Terminal49 plan is billed separately by Terminal49.

### FAQ

#### What is the Terminal49 API?

Terminal49 is a US container tracking company whose API v2 (api.terminal49.com/v2, JSON:API style) returns shipment and container data for bills of lading, bookings and container numbers across ocean carriers, US marine terminals and North American rail. Tracking starts with a tracking request; from then on Terminal49 keeps shipments, containers and their milestones up to date.

#### Do I need my own Terminal49 API key?

Yes. This is a bring your own key actor: it calls Terminal49 with your key, under your plan and its terms. Create the key under Developers in the Terminal49 web app. The full value is shown only once, so copy it when you create it.

#### Why does my key get refused even though it is valid?

Terminal49 free plan keys can only create tracking requests; every read endpoint answers 401 until the account has a paid API plan or the 7 day API trial. The run then ends with Terminal49's own message rather than failing.

#### Can this actor start tracking a new container?

No. Creating a tracking request is billable on your Terminal49 plan, so the actor stays read only. Track the container in Terminal49 first, then read it here by container number.

#### How do I get the ETA of a container or bill of lading?

Use Shipments with the bill of lading in Shipment numbers for the port of discharge ETA, original ETA and ATA, or Containers with the container number for the container's status plus its shipment's ETA. Raw and estimated events add the carrier's future estimated milestones.

#### How do I find containers about to hit their last free day?

Run Containers with a Last free day window (for example today to three days out), optionally Only containers with holds or fees, and the Available for pickup status. Each row carries `lastFreeDay`, the terminal, line and rail deadlines, holds and fees.

#### Where do vessel positions and route maps come from?

From Terminal49's Routing Data feature. Container route map returns ports, the current vessel position and past and predicted paths as GeoJSON; Vessels returns the latest AIS position by IMO for vessels on your tracked shipments. Without the feature Terminal49 answers 403 and the run ends with that message.

#### What are the rate limits?

Terminal49 allows 100 requests per minute per key on a rolling window. The actor paces itself at 60 per minute by default and honours Retry-After on a 429.

### Keyword map

container tracking API, ocean container tracking, shipment tracking API, bill of lading tracking, booking number tracking, container ETA API, vessel ETA, port of discharge arrival, container milestones, container status API, demurrage and detention, last free day API, terminal holds and fees, US port container availability, container route map GeoJSON, vessel position by IMO, shipping line SCAC list, UN/LOCODE port lookup, Terminal49 API, Terminal49 container tracking, Terminal49 shipments.

### Routes and verification

Wrapped GET routes: `/shipments`, `/shipments/{id}`, `/containers`, `/containers/{id}`, `/containers/{id}/transport_events`, `/containers/{id}/raw_events`, `/containers/{id}/map_geojson`, `/tracking_requests`, `/shipping_lines`, `/ports/{id}`, `/terminals/{id}`, `/vessels/{imo}`. All were probed on 2026-10-25 with a deliberately invalid key and answered Terminal49's 401 JSON:API envelope, while invented paths answered a bare 404, so every route above exists as documented. Response shapes, filters and fields are taken from the vendor's OpenAPI definition and reference examples, not from live data.

### Licensing and data use

This actor works only with your own Terminal49 account and API key, and Terminal49's terms apply to everything it returns. Those terms forbid reselling the data or using it to build a competing service; they set no limit on how long you may keep results. This actor is not affiliated with or endorsed by Terminal49.

# Actor input Schema

## `mode` (type: `string`):

Shipments: one row per tracked shipment with bill of lading, carrier, ports, ETA, ATA and vessel. Containers: one row per container with status, holds, fees, last free day and terminal. Container transport events (normalized past milestones), Container raw and estimated events (carrier wording, including future ETAs) and Container route map (ports, vessel position and paths; needs the Routing Data feature). Tracking requests lists what you asked Terminal49 to track. Shipping lines, Ports, Terminals and Vessels are reference lookups. Every call is a read only GET; this actor never creates tracking requests.

## `shipmentNumbers` (type: `array`):

Shipments only, optional. One bill of lading or booking number per line, matched exactly. Leave empty to list every shipment that matches the filters below.

## `shipmentIds` (type: `array`):

Shipments only, optional. One Terminal49 shipment UUID per line, read directly.

## `searchText` (type: `string`):

Shipments and Tracking requests, optional. Prefix search across shipment numbers, reference numbers and container numbers (shipments), or request and reference numbers (tracking requests).

## `trackingState` (type: `string`):

Shipments and Containers. Keeps shipments still being tracked, or those whose tracking was stopped.

## `voyageStatus` (type: `string`):

Shipments only, optional. Arrived means the actual arrival at the port of discharge is known; on ship means it is not yet.

## `tags` (type: `array`):

Shipments only, optional. Your Terminal49 shipment tags; a shipment with any of them matches.

## `shipmentSort` (type: `string`):

Shipments only, optional. Terminal49 lists newest first by default.

## `polCodes` (type: `array`):

Shipments and Containers, optional. Port of lading UN/LOCODEs; any of them matches.

## `podCodes` (type: `array`):

Shipments and Containers, optional. Port of discharge UN/LOCODEs; any of them matches.

## `arrivalFrom` (type: `string`):

Shipments and Containers, optional. YYYY-MM-DD; port of discharge arrival (actual, else estimated) on or after it.

## `arrivalTo` (type: `string`):

Shipments and Containers, optional. YYYY-MM-DD; port of discharge arrival (actual, else estimated) on or before it.

## `createdFrom` (type: `string`):

Shipments, Containers and Tracking requests, optional. YYYY-MM-DD; records created on or after it.

## `createdTo` (type: `string`):

Shipments, Containers and Tracking requests, optional. YYYY-MM-DD; records created on or before it.

## `containerNumbers` (type: `array`):

Containers, events and route modes. One ISO 6346 container number per line. It must already be tracked in your Terminal49 account; this actor never starts tracking.

## `containerIds` (type: `array`):

Containers, events and route modes, optional. One Terminal49 container UUID per line.

## `currentStatuses` (type: `array`):

Containers only, optional. Keeps containers in any of these statuses.

## `carrierScacs` (type: `array`):

Containers and Tracking requests, optional. Shipping line SCACs such as MSCU or MAEU (Tracking requests takes one).

## `onlyHoldsOrFees` (type: `boolean`):

Containers only. Keeps containers with terminal holds or fees reported.

## `lfdFrom` (type: `string`):

Containers only, optional. YYYY-MM-DD; pickup last free day on or after it.

## `lfdTo` (type: `string`):

Containers only, optional. YYYY-MM-DD; pickup last free day on or before it.

## `requestStatus` (type: `string`):

Tracking requests only, optional. Failed requests carry the failed reason.

## `portCodes` (type: `array`):

Ports only. One UN/LOCODE (or Terminal49 port ID) per line.

## `terminalIds` (type: `array`):

Terminals only. One Terminal49 terminal UUID per line, as the pod_terminal relationship of a shipment or container gives it.

## `vesselImos` (type: `array`):

Vessels only. One 7 digit IMO number per line. Needs the Routing Data feature; only vessels on your actively tracked shipments are returned.

## `maxResults` (type: `integer`):

The most rows this run returns and charges for.

## `requestsPerMinute` (type: `integer`):

Pacing for calls to the Terminal49 API, which allows 100 requests per minute per key.

## `apiKey` (type: `string`):

Your own Terminal49 API key from app.terminal49.com/developers/api-keys. Stored as a secret and sent only to api.terminal49.com in the Authorization header; it is never logged. You can instead set the DATA_API_KEY environment secret.

## Actor input object example

```json
{
  "mode": "shipments",
  "trackingState": "any",
  "onlyHoldsOrFees": false,
  "maxResults": 100,
  "requestsPerMinute": 60
}
```

# Actor output Schema

## `records` (type: `string`):

One row per shipment, container, event, route feature, tracking request, shipping line, port, terminal or vessel.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("nabeelbaghoor/ocean-container-shipment-tracking-api").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("nabeelbaghoor/ocean-container-shipment-tracking-api").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call nabeelbaghoor/ocean-container-shipment-tracking-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nabeelbaghoor/ocean-container-shipment-tracking-api"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/9PH4TYi3Cbq4jB9t9/builds/vBCGj8IEHHjRhQ1IX/openapi.json
