Your container ship schedules are slipping because port queues change by the hour and ETAs drift as weather, drafts, and last-mile pilotage stack up. By the end of this guide you’ll query live AIS positions, compute practical ETAs, and monitor port congestion for your lanes—using a handful of REST endpoints you can ship to production today.
What you’ll build with Vessels API
Vessels API exposes 18 REST endpoints for vessel search, live tracking, fleet operations, port intelligence, and IMO CII scoring—powered by global AIS coverage. Every response returns a consistent JSON envelope {status, success, message, data}. You authenticate once via the X-API-Key header, no OAuth flows or per-endpoint differences.
- Track a container ship and pull route + predicted ETA.
- Surface nearby traffic for pilotage windows or anchorage risk.
- Check port congestion snapshots to plan berthing and landside ops.
- Batch-update a fleet dashboard with fresh positions and routes.
Core endpoints for container ship AIS, congestion, and ETA
- /vessels/track — live position, up to 168h history, route, predicted ETA, weather.
- /port/expected-arrivals — inbound queue with vessel ETAs and origin ports.
- /ports/congestion — snapshot and wait-time stats per port (UNLOCODE).
- /vessels/analytics — voyage aggregates (distance, speed, port call counts).
- /vessels/fleet — batch fetch for multi-ship dashboards.
Live AIS tracking and ETA for a container ship
Start with /vessels/track. Provide an MMSI or IMO and optionally tune historical depth (hours=) and whether to include route, ETA, and weather. Units are nautical miles and knots; timestamps are UTC ISO 8601.
Official cURL for a live track (48h history)
curl -H "X-API-Key: YOUR_API_KEY" "https://vessels-api.com/api/V1/vessels/track?mmsi=258785000&hours=48"
Official JSON response
{
"status": 200,
"success": true,
"message": "OK",
"data": {
"vessel": {
"imo": "9122556",
"mmsi": "258785000",
"name": null
},
"current_position": {
"latitude": 53.33708,
"longitude": 7.17993,
"speed_knots": 0,
"course_degrees": 212,
"heading_degrees": null,
"navigational_status": 5,
"timestamp_utc": "2017-01-24T04:07:24+00:00",
"age_minutes": 5097380,
"destination": null,
"eta": null
},
"predicted_eta": null,
"position_history": [],
"route": {
"departure_port": "HERACLIO",
"departure_time": "2026-04-27T21:35:36+00:00",
"destination_port": "HERACLIO",
"eta": "2026-04-30T09:00:00+00:00",
"distance_nm": null,
"avg_speed_knots": 17.1
},
"last_port_visits": [
{
"port_id": "156",
"port_name": "EMDEN",
"arrival_time": "2026-07-30T13:00:10+00:00",
"departure_time": null,
"duration_hours": null
},
{
"port_id": "10",
"port_name": "HERACLIO",
"arrival_time": "2026-04-20T12:00:00+00:00",
"departure_time": "2026-04-23T08:00:00+00:00",
"duration_hours": 68
}
],
"weather": null
}
}
Field notes you will actually use:
- data.current_position.{latitude, longitude, speed_knots, course_degrees} — current AIS fix; knots and degrees true.
- data.current_position.timestamp_utc — ISO 8601 UTC; check freshness before showing a “live” badge.
- data.route.{departure_port, destination_port, eta, avg_speed_knots} — route-level ETA useful when predicted_eta is null.
- data.predicted_eta — model-based ETA if available; otherwise fall back to route.eta.
- data.last_port_visits — for recently completed port calls and dwell estimates.
Python: calculate a safe ETA for operations
This snippet prefers predicted_eta when available, else route.eta, and annotates staleness if the AIS position is old. All timestamps are parsed as UTC.
import requests
from datetime import datetime, timezone
API_KEY = "YOUR_API_KEY"
TRACK_URL = "https://vessels-api.com/api/V1/vessels/track"
def parse_iso(ts):
return datetime.fromisoformat(ts.replace("Z", "+00:00")).astimezone(timezone.utc) if ts else None
params = {
"mmsi": "258785000",
"hours": "48", # up to 168
# "include_route": "true", # route is already included by default in the official sample
# "include_predicted_eta": "true",
}
resp = requests.get(TRACK_URL, headers={"X-API-Key": API_KEY}, params=params, timeout=30)
resp.raise_for_status()
payload = resp.json()["data"]
cur = payload["current_position"]
route = payload.get("route", {}) or {}
pred_eta = payload.get("predicted_eta")
# Determine operational ETA
eta_str = pred_eta or route.get("eta")
eta = parse_iso(eta_str)
# Check AIS freshness
fix_time = parse_iso(cur.get("timestamp_utc"))
age_minutes = cur.get("age_minutes") # already provided; fallback computed if needed
summary = {
"mmsi": payload["vessel"]["mmsi"],
"imo": payload["vessel"]["imo"],
"position": {
"lat": cur["latitude"],
"lon": cur["longitude"],
"speed_knots": cur["speed_knots"],
"course": cur["course_degrees"],
"fix_time_utc": cur["timestamp_utc"],
"age_minutes": age_minutes
},
"destination_port": route.get("destination_port"),
"eta_utc": eta_str,
"eta_source": "predicted_eta" if pred_eta else "route.eta" if eta_str else "unknown"
}
print(summary)
From AIS to port reality: congestion and the inbound queue
ETA on its own is not enough. To decide on berth windows and truck gates, combine the vessel route ETA with the port’s congestion snapshot and the list of expected arrivals.
Expected arrivals for a port
The expected-arrivals feed returns vessels inbound to a port with their ETA and origin port. Use this to see how your ship’s ETA aligns with inbound bunching.
Key fields (see the response under data):
- expected_arrivals[].{mmsi, imo, name, vessel_type, eta, departure_port}
- eta is UTC ISO 8601; compare against your tracked vessel’s ETA for bunching analysis.
Congestion snapshot
Use /ports/congestion to understand anchorage pressure and recent wait-time behavior. Provide a UNLOCODE (e.g., ARBUE, SGSIN, NLRTM) and an optional rolling period for stats.
Return fields include:
- snapshot.{vessels_in_anchorage, vessels_at_berth}
- statistics.{avg_wait_time_hours_last_7d, max_wait_time_hours_last_7d, avg_berth_time_hours_last_7d, port_calls_count}
Tip: never hardcode wait times. Query on demand or cache for a short TTL (e.g., 15–30 minutes) to keep dashboards fast while staying current.
Build a practical ETA model: combine route ETA, predicted ETA, and port load
Operational ETA blends ship-level timing with port-level capacity. A pragmatic approach:
- Pull /vessels/track and prefer data.predicted_eta when present; fall back to data.route.eta.
- Query /port/expected-arrivals for the destination to detect inbound bunching in the 12–24h window.
- Query /ports/congestion for anchorage levels and recent wait-time behavior.
- If congestion is high and inbound bunching is intense near your arrival window, add a buffer to ETA or alert ops to secure pilotage/berth priority.
This layered method is simple to implement and typically yields a more reliable arrival estimate for trucking appointments, rail slots, and yard planning.
Fleet dashboards: batch positions and routes
Container operators and logistics control towers often track dozens of ships across multiple strings. Use /vessels/fleet to fetch positions and routes in one request. This reduces API round-trips and helps you render a single map with ETA chips for every hull.
Helpful fields for dashboards:
- vessels[].position.{latitude, longitude, speed_knots, timestamp_utc}
- vessels[].route.{destination_port, eta, avg_speed_knots}
- fleet.{total_vessels, vessels_at_sea, vessels_in_port}
Best practice: refresh bulk data every 2–5 minutes for at-sea vessels; longer for vessels alongside. Respect HTTP 429 responses by backing off and staggering batch cohorts.
Context: voyage analytics for capacity and service health
To understand typical transit times and port dwell across your string, use /vessels/analytics with type=vessel or type=fleet.
You’ll get aggregates like total_distance_nm, avg_speed_knots, max_speed_knots, port_calls_count, and total_time_in_port_hours. These are helpful to put a single late ETA in the context of a vessel’s recent performance.
Port-aware ETAs in JavaScript (node/fetch)
The following example stitches together a vessel ETA and the inbound queue for a destination port. It illustrates how to merge multiple endpoints without overfetching.
import fetch from "node-fetch";
const API_KEY = "YOUR_API_KEY";
const BASE = "https://vessels-api.com/api/V1";
async function getJSON(path, params = {}) {
const url = new URL(`${BASE}${path}`);
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
const res = await fetch(url.toString(), { headers: { "X-API-Key": API_KEY } });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = await res.json();
if (!body.success) throw new Error(body.message || "API error");
return body.data;
}
async function vesselEtaWithInbound(mmsi) {
const track = await getJSON("/vessels/track", { mmsi, hours: "48" });
const route = track.route || {};
const eta = track.predicted_eta || route.eta || null;
const dest = route.destination_port || null;
let inbound = [];
if (dest) {
// Use the port's UNLOCODE or identifier if known. For demo, ARBUE is shown elsewhere in cURL.
// Replace with your destination port code when available.
const portId = dest; // if your integration maps port names -> port IDs, resolve here
try {
const arrivals = await getJSON("/port/expected-arrivals", { port: portId });
inbound = arrivals.expected_arrivals || [];
} catch (e) {
// Graceful degradation if the destination cannot be mapped
inbound = [];
}
}
return {
vessel: track.vessel,
current_position: track.current_position,
destination_port: dest,
eta_utc: eta,
inbound_count: inbound.length
};
}
vesselEtaWithInbound("258785000")
.then(console.log)
.catch(console.error);
Operational guardrails and implementation notes
- Authentication: send X-API-Key on every request. One key, one base URL.
- Time and units: timestamps are UTC ISO 8601; distances are nautical miles; speeds are knots; courses are degrees true.
- Freshness: check data.current_position.age_minutes before labeling a fix as “live.” Fall back to last_port_visits if a ship is in port.
- Pacing: live maps don’t need sub-minute polling. Most control towers refresh at 2–5 minute intervals for at-sea hulls.
- Pagination: /vessels/search supports page and per_page (max 100). Cache IDs from search results to avoid fuzzy matching repeatedly.
- Error handling: HTTP 400/422 indicate bad parameters; 401 indicates key issues; 429 indicates throttling—implement exponential backoff.
- Nulls are normal: predicted_eta and weather may be null. Always guard and fall back to route.eta.
- Port identity: /ports returns a catalog of 248 ports. Use it to build a local mapping for port names to identifiers.
Use cases tailored to container shipping
- Gate appointment planning: fuse a vessel’s ETA with /ports/congestion to avoid bunching import flows in a single shift.
- Transshipment alerts: if route.destination_port is a transshipment hub, watch /port/expected-arrivals to anticipate feeder load bursts.
- Anchorage risk: poll /vessels/nearby around approach lanes to understand pilotage queue density ahead of arrival.
- Service reliability KPI: use /vessels/analytics over 30d windows to benchmark avg_speed_knots, time-in-port, and port_calls_count.
- Fleet view: render a map using /vessels/fleet and color-code chips by ETA risk (based on congestion + inbound queue).
Why developers choose Vessels API for transportation workflows
- 18 REST endpoints spanning vessel intelligence, fleet ops, port intelligence, and IMO CII emissions.
- Global AIS coverage with near real-time refresh rates suitable for live dashboards.
- One header for auth and a consistent JSON envelope across endpoints, which simplifies client code.
- Scales from a single string to enterprise fleets; includes a premium real-time AIS feed.
- 7-day free trial on all plans to get your integration live quickly.
Quick links
FAQ
Q: How often should I refresh a live positions map for container ships?
A: For at-sea vessels, 2–5 minutes is a realistic balance of freshness and efficiency. For in-port vessels, 10–15 minutes is usually enough.
Q: What do I do if predicted_eta is null?
A: Fall back to data.route.eta from /vessels/track. If both are null, check /port/expected-arrivals; some ports publish ETAs for inbound vessels.
Q: How do I handle port identifiers?
A: Use /ports to build a local catalog (port_id, name, country). Store a mapping from your internal port aliases to the returned port_id values.
Q: Can I request historical tracks beyond 48 hours?
A: Yes, set hours up to 168 on /vessels/track to retrieve up to seven days of position history.
Q: What’s the best way to display congestion without fabricating numbers?
A: Query /ports/congestion with a short cache TTL and render the snapshot and statistics as-is, clearly labeled with the requested period.
Build it now
Hook up your strings, compute practical ETAs that reflect real port conditions, and ship a dashboard the ops team will actually use. Start with the track endpoint, layer in port expected arrivals and congestion, and batch your fleet updates for scale. Get your key and begin coding today: Register. Reference every field as you integrate: Documentation. Explore tooling and integrations on the MCP.




