You need to track a cruise ship: where it is right now, where it’s headed, when it will arrive, which port it visited last, and how the fleet is performing today. By the end of this guide you will query a live AIS-backed API, parse positions and routes, show expected arrivals to a cruise terminal, and roll up a multi-ship fleet view—using nothing but HTTP requests and a single API key.
Why this approach works for cruise tracking
vessels-api.com exposes 18 REST endpoints for vessel search, live AIS tracking, fleet operations, port intelligence, and IMO CII emissions. Everything shares one base URL and one header for auth, and every response comes in the same JSON envelope: {status, success, message, data}. For cruise operations, that consistency translates into faster dashboards, fewer integration branches, and predictable error handling while you iterate on turnarounds and ETAs.
- Single base URL: https://vessels-api.com/api/V1
- Authentication: X-API-Key header (no OAuth flow)
- Coverage: Global AIS with near real-time refresh
- Common use cases: live cruise tracking, fleet ops rollups, port arrival boards, ESG/CII reporting
Endpoints you’ll use for a cruise ship
We’ll focus on five endpoints that map directly to cruise-ship workflows:
- GET /vessels/track — live position, last ports, route, predicted ETA
- GET /vessels/search — find a ship by name fragment, IMO, or MMSI
- POST /vessels/fleet — batch positions and routes for a cruise line’s fleet
- GET /ports/data and GET /port/expected-arrivals — show live port counts and upcoming cruise calls
- GET /vessels/green — compute IMO CII for ESG/compliance views
Live cruise tracking with /vessels/track
Use this to get the ship’s current AIS position, up to 168 hours of history, active route, predicted ETA, and recent port calls. You can identify a vessel by IMO or MMSI. For the example below we’ll use the documented MMSI track fixture.
Official cURL (copy-paste)
curl -H "X-API-Key: YOUR_API_KEY" "https://vessels-api.com/api/V1/vessels/track?mmsi=258785000&hours=48"
Official JSON (verbatim)
{
"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:16+00:00",
"age_minutes": 5119040,
"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
}
}
What matters for a cruise use case:
- current_position.timestamp_utc is ISO 8601 in UTC. Use this to detect stale AIS (age_minutes).
- speed_knots and course_degrees are your map overlay state; show “At anchor” if speed_knots is near zero and navigational_status indicates restricted movement.
- route.eta represents the voyage plan ETA. If predicted_eta is present, prefer it for dynamic updates.
- last_port_visits lets you show “Last port: EMDEN, Arrived 2026-07-30 13:00Z”.
Python example: fetch and render a ship’s live card
import os
import requests
from datetime import datetime, timezone
API_KEY = os.getenv("VESSELS_API_KEY", "YOUR_API_KEY")
BASE_URL = "https://vessels-api.com/api/V1"
def track_cruise_ship(mmsi: str, hours: int = 48):
url = f"{BASE_URL}/vessels/track"
params = {"mmsi": mmsi, "hours": hours}
resp = requests.get(url, headers={"X-API-Key": API_KEY}, params=params, timeout=20)
resp.raise_for_status()
payload = resp.json()
if not payload.get("success"):
raise RuntimeError(f"API error: {payload.get('message')}")
data = payload["data"]
vessel = data.get("vessel", {})
pos = data.get("current_position", {}) or {}
route = data.get("route", {}) or {}
last_ports = data.get("last_port_visits", []) or []
# Normalize nullable fields
lat = pos.get("latitude")
lon = pos.get("longitude")
speed = pos.get("speed_knots", 0)
ts = pos.get("timestamp_utc")
eta = (data.get("predicted_eta") or pos.get("eta") or route.get("eta"))
# Convert timestamp for display
ts_display = ts
if ts:
# Keep as UTC; front-end can localize if needed
ts_display = datetime.fromisoformat(ts.replace("Z", "+00:00")).strftime("%Y-%m-%d %H:%M UTC")
card = {
"mmsi": vessel.get("mmsi"),
"imo": vessel.get("imo"),
"name": vessel.get("name") or "Unknown",
"position": {"lat": lat, "lon": lon, "speed_knots": speed, "course_deg": pos.get("course_degrees")},
"status": pos.get("navigational_status"),
"as_of": ts_display,
"voyage": {
"from": route.get("departure_port"),
"to": route.get("destination_port"),
"eta": eta
},
"last_port": (last_ports[0]["port_name"] if last_ports else None)
}
return card
if __name__ == "__main__":
ship = track_cruise_ship("258785000", hours=48)
print(ship)
Notes:
- hours accepts up to 168 (7 days). Omit to default to 24 hours.
- include_route, include_predicted_eta, and include_weather are optional flags when you want to explicitly request additional data.
- Nulls are normal in AIS. Always guard fields like heading_degrees, destination, and predicted_eta.
Find the cruise ship when you only know a fragment
Cruise guests and ops teams often have only a partial name. Use /vessels/search with query plus filters like ship_type to narrow to passenger vessels.
Search by name with a type filter
Response shape:
Key behaviors:
- Fuzzy name search: partials and common misspellings work.
- Paginate with page and per_page (max 100) to stream search into a typeahead.
- For a cruise-specific result, filter ship_type=Passenger.
One request for the entire cruise fleet
For control rooms and operations dashboards, use POST /vessels/fleet to fetch positions and routes for multiple ships at once. This reduces client-side fan-out and makes your refresh loop simpler.
Batch positions for a cruise line
Response shape:
Tips:
- Send a mix of IMO/MMSI in vessels[].
- Toggle include_positions/include_routes to trim payload size.
- Use the rollup counts (vessels_at_sea, vessels_in_port) for KPI tiles.
Operational context at the terminal: port intel endpoints
Port calls are central to cruise. You’ll want three API views to support terminal operations and shore-side guest services:
- GET /ports/data — static details plus live counts for vessels_in_port and vessels_expected.
- GET /port/expected-arrivals — the arrival board for a port (MMSI/IMO, vessel_type, ETA, and origin).
- GET /ports/congestion — a snapshot of anchorage/berth congestion and wait-time statistics.
Get live stats for a cruise terminal
This returns the port’s identifiers, timezone, and live vessel counts. Use vessels_expected to pre-allocate gangways and shore power for large cruise calls.
Build a passenger-facing arrival board
Render the returned expected_arrivals list with vessel name (if present), MMSI/IMO, vessel_type, eta (UTC), and departure_port. Filter vessel_type=Passenger in the UI for cruise-only boards.
Monitor port congestion to adjust ETAs
Use the snapshot (vessels_in_anchorage, vessels_at_berth) as real-time context and the statistics block (e.g., avg_wait_time_hours_last_7d) to decide whether to pad ETAs for tendering vs. berthing. All timestamps are UTC; convert to the port’s timezone when displaying to guests.
Implementation detail: Do not hard-code port IDs. Use GET /ports to cache the catalog (port_id, name, country, latitude, longitude, timezone) once a day and key lookups by UNLOCODE where applicable.
Add ESG to the cruise dashboard with CII
To power sustainability views in the same app, call /vessels/green for each ship and surface the IMO CII rating derived from MEPC.339(76). This is practical for sustainability and compliance teams tracking fleet performance across 24h, 7d, 30d, or 1y.
Get a cruise ship’s CII
Response shape includes distance_nm, estimated_emissions.co2_tons, and cii.score with a rating from A (best) to E (worst). Keep the period consistent across a dashboard row to avoid mixed comparisons.
ETA logic that won’t page you at 03:00
ETAs can be present in multiple places: current_position.eta (from AIS), route.eta (voyage plan), and predicted_eta (API-generated, when requested). A resilient approach:
- If predicted_eta is present, use it.
- Else use current_position.eta when it’s fresh (low age_minutes).
- Else fall back to route.eta and mark it as planned in the UI.
Overlay port congestion as an adjustment factor client-side. Keep both the raw UTC ETA and a localized display time to avoid DST edge cases around turnarounds.
Mapping the fields to a cruise map and timeline
- Position pin: latitude, longitude, heading_degrees (if available), and course_degrees arrow.
- Status banner: navigational_status and speed_knots (e.g., “Underway at 17.1 kn”).
- Route ribbon: route.departure_port → route.destination_port, distance_nm (if present), avg_speed_knots.
- Timeline: last_port_visits[i].arrival_time → departure_time, with duration_hours if present.
Error handling, pagination, and caching
- HTTP status: 200 OK means success; 400 invalid parameter; 401 missing/invalid key; 404 not found; 422 out of range; 429 rate limit exceeded; 500 server error.
- Always check success and message in the JSON envelope before reading data.
- Search pagination: honor per_page ≤ 100 and use page to bring in more results.
- Caching: short-cache static catalogs (GET /ports) daily; cache search results by query for 1–5 minutes; do not cache live positions longer than your refresh cadence.
- Timestamps: all in UTC ISO 8601. Convert for display only; keep UTC for logic and storage.
JavaScript snippet for a cruise widget
async function fetchTrack(mmsi, hours = 48) {
const url = new URL("https://vessels-api.com/api/V1/vessels/track");
url.searchParams.set("mmsi", mmsi);
url.searchParams.set("hours", hours);
const resp = await fetch(url.toString(), {
headers: { "X-API-Key": "YOUR_API_KEY" }
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const json = await resp.json();
if (!json.success) throw new Error(json.message);
const { vessel, current_position, route, last_port_visits, predicted_eta } = json.data;
const eta = predicted_eta || current_position.eta || (route && route.eta) || null;
return {
id: vessel.mmsi || vessel.imo,
name: vessel.name || "Unknown",
lat: current_position.latitude,
lon: current_position.longitude,
speed: current_position.speed_knots,
course: current_position.course_degrees,
asOf: current_position.timestamp_utc,
from: route && route.departure_port,
to: route && route.destination_port,
eta,
lastPort: (last_port_visits && last_port_visits[0] && last_port_visits[0].port_name) || null
};
}
// Example usage:
fetchTrack("258785000").then(console.log).catch(console.error);
Putting it together: a minimal cruise tracking flow
- Discover the vessel once using GET /vessels/search (query, ship_type=Passenger). Store IMO and MMSI.
- Every 30–60 seconds, poll POST /vessels/fleet with all active ships (include_positions=true, include_routes=true).
- For a selected ship, fetch GET /vessels/track with hours=48 and include_predicted_eta=true to populate the detail drawer.
- For the destination port, call GET /ports/data and GET /port/expected-arrivals for guest services and ops coordination.
- Nightly, call GET /vessels/green for each ship with period=30d to refresh ESG tiles.
Security and production readiness
- Send X-API-Key over HTTPS only; never embed the key in client code shipped to browsers without a proxy.
- Prefer server-side calls for POST /vessels/fleet to centralize auth and simplify rate management.
- Validate parameters (hours ≤ 168, per_page ≤ 100) to avoid 422 errors.
- Implement retry with jitter only on idempotent GETs; do not blindly retry POST /vessels/fleet without backoff.
Developer FAQs
How often should I refresh positions for a cruise dashboard?
Most cruise UIs refresh every 30–60 seconds for the selected ship and 60–120 seconds for the fleet list. Respect the API’s HTTP responses and back off if you hit 429.
Should I key ships by IMO or MMSI?
Store both. MMSI is commonly used for AIS tracking; IMO is persistent across radio changes. The API accepts either where noted.
What units and timezones should I expect?
Speeds are knots, distances are nautical miles (nm), timestamps are UTC ISO 8601. Localize display at the edge but keep UTC for logic.
What if fields like heading_degrees or predicted_eta are null?
Nulls are normal in AIS. Guard all optional fields and fall back sensibly (e.g., use route.eta when predicted_eta is null).
How much history can I request with /vessels/track?
Use hours up to 168 (7 days). If you omit it, the default is 24 hours.
Next steps
Spin up your cruise tracker with a single key and consistent JSON across live tracking, port intelligence, fleet ops, and emissions. Start coding with the Documentation, provision your key via Register, and explore tools in the MCP. Build the map, wire up ETAs, and ship the dashboard your cruise ops team actually uses.




