Working with time zones

Every date and time in the OpenTrack API is returned as an ISO 8601 timestamp in UTC — for example "2026-06-11T06:59:59.999Z". The trailing Z means Zulu / UTC.

We deliberately do not localize timestamps for you, because a single shipment moves through multiple time zones (origin port, discharge port, inland rail ramp). Instead, we give you the IANA time zone of each location so you can render every value in the time zone that is meaningful to your users.

💡

TL;DR

  • Timestamps are UTC (...Z). There is no per-field numeric offset.
  • Every location object includes a timezone field in IANA format (e.g. America/Los_Angeles).
  • To localize an event, use that event's own location.timezone.
  • To localize an ETA or Last Free Day, use the time zone of the facility that field refers to (tables below).
  • Last Free Dates are calendar-day deadlines encoded as end-of-day. Convert to the correct local zone before extracting the date.

Time zones are attached to locations

Each Location object in a container response carries a timezone field using the IANA time zone database name (also called an Olson name), such as America/Los_Angeles or Asia/Shanghai. IANA names — not fixed offsets like UTC-8 — are what you want, because they encode daylight-saving rules automatically.

Locations that expose a timezone include:

FieldDescription
originThe inland origin
originPortThe initial ocean port of loading
destinationPortThe final ocean port of discharge
finalDestinationThe container's final delivery destination
currentLocationThe last known facility that handled the container
history[].locationThe location of each observed milestone
timeline[].locationThe location of each observed or predicted milestone
{
  "destinationPort": {
    "name": "Los Angeles",
    "unlocode": "USLAX",
    "type": "port",
    "timezone": "America/Los_Angeles",
    "coordinates": { "latitude": 33.74, "longitude": -118.27 }
  }
}

Each event in history and timeline carries its own location — that's the time zone you use to localize that event's timestamp:

{
  "milestone": "DISCHARGED",
  "timestamp": "2026-06-10T18:00:00.000Z",
  "location": {
    "name": "Los Angeles",
    "unlocode": "USLAX",
    "type": "port",
    "timezone": "America/Los_Angeles",
    "coordinates": { "latitude": 33.74, "longitude": -118.27 }
  }
}
⚠️

timezone can on rare occasions be absent — typically for a plain inland locality we have not been able to geocode. Always handle a missing timezone gracefully (fall back to UTC or to a facility you do have a zone for, and label it clearly).

Converting a timestamp to local time

Pick the timestamp, pick the relevant location's timezone, and let a time zone-aware date library do the conversion.

🧭

"Local time" means the facility's local time — not your user's

Convert each timestamp into the time zone of the location it belongs to (the port, terminal, or rail ramp) — not your operator's local time or the viewer's browser time zone. A container discharged in Los Angeles should read in America/Los_Angeles, even if the person looking at it is sitting in New York. Localizing everything to a single viewer time zone misrepresents when events actually happened on the ground.

🚨

Do not add a fixed offset yourself (e.g. UTC-8) — you'll get it wrong across daylight-saving boundaries. Always pass the IANA timezone to a time zone-aware date library.

JavaScript (Luxon)

import { DateTime } from 'luxon';

const event = container.history[0];
const local = DateTime
  .fromISO(event.timestamp, { zone: 'utc' })
  .setZone(event.location.timezone);

console.log(local.toFormat('yyyy-MM-dd HH:mm ZZZZ'));
// e.g. "2026-06-01 23:00 PDT"

Python (standard library)

from datetime import datetime
from zoneinfo import ZoneInfo

event = container["history"][0]
utc = datetime.fromisoformat(event["timestamp"].replace("Z", "+00:00"))
local = utc.astimezone(ZoneInfo(event["location"]["timezone"]))

print(local.strftime("%Y-%m-%d %H:%M %Z"))
# e.g. "2026-06-01 23:00 PDT"

Which time zone applies to which field

Milestones in history and timeline carry their own location, so always localize each event with its own location.timezone. For the top-level date fields, use the time zone of the facility the field describes:

FieldLocalize with
history[].timestamp, timeline[].timestampthat event's location.timezone
etaAtTerminaldestinationPort.timezone
etaAtDestination, etnAtDestination (rail moves only)finalDestination.timezone
lastFreeDaydestinationPort.timezone
detentionLastFreeDayfinalDestination.timezone
lastFreeDayAtRailStationfinalDestination.timezone
lineLastFreeDaydepends on the move — see below

Last Free Dates: read these carefully

Last Free Dates (LFDs) are calendar-day deadlines, not precise instants. We encode each one as the end of the local day at the responsible facility. So a Last Free Day of "June 11 in Los Angeles" is returned as:

2026-06-11T06:59:59.999Z   ← 23:59:59.999 on June 11 in America/Los_Angeles
🚨

Convert before you slice the date

If you display the raw UTC value, or convert it to the wrong time zone, the calendar day can shift. Always convert to the field's local zone first, then take the date.

import { DateTime } from 'luxon';

// Terminal demurrage LFD at the discharge port
const lfdDate = DateTime
  .fromISO(container.lastFreeDay, { zone: 'utc' })
  .setZone(container.destinationPort.timezone)
  .toFormat('yyyy-MM-dd');   // "2026-06-11"

The LFD fields

FieldWhat it isLocalize with
lastFreeDayTerminal demurrage LFD at the final ocean discharge portdestinationPort.timezone
lineLastFreeDayDemurrage LFD issued by the steamship lineport or destination — see below
detentionLastFreeDayDetention / per-diem LFDfinalDestination.timezone
lastFreeDayAtRailStationDemurrage LFD at the final rail stationfinalDestination.timezone

Rail moves change where the deadline lives

When a container has an inland rail leg, isRailMove is true and the relevant deadline moves inland to the rail ramp. Handle these cases explicitly:

  • lastFreeDayAtRailStation is the rail-ramp demurrage deadline. Localize it with finalDestination.timezone (the rail facility's zone), not the ocean port zone.
  • lineLastFreeDay applies to either the ocean terminal or the final rail station depending on the move. Choose the zone by isRailMove:
const lineLfdZone = container.isRailMove
  ? container.finalDestination.timezone   // rail: deadline is at the ramp
  : container.destinationPort.timezone;   // ocean: deadline is at the port
  • etaAtDestination / etnAtDestination are only present on rail moves (isRailMove: true) and refer to the final rail facility, so localize them with finalDestination.timezone.
  • On an active rail move, the terminal-level lastFreeDay may be omitted once the container has moved off the ocean terminal onto rail — at that point the rail deadline (lastFreeDayAtRailStation) is the one that matters. Don't assume lastFreeDay is always present.

Worked example

{
  "containerId": "MRKU2642087",
  "isRailMove": true,
  "etaAtTerminal": "2026-06-10T18:00:00.000Z",
  "etaAtDestination": "2026-06-12T08:00:00.000Z",
  "lastFreeDay": "2026-06-11T06:59:59.999Z",
  "lineLastFreeDay": "2026-06-12T06:59:59.999Z",
  "lastFreeDayAtRailStation": "2026-06-14T05:59:59.999Z",
  "destinationPort": { "name": "Los Angeles", "timezone": "America/Los_Angeles" },
  "finalDestination": { "name": "Salt Lake City", "timezone": "America/Denver" }
}

Rendered for the user:

ValueLocalized
ETA at terminal (etaAtTerminal → LA)Jun 10, 2026, 11:00 AM PDT
ETA at rail ramp (etaAtDestination → Denver)Jun 12, 2026, 2:00 AM MDT
Terminal LFD (lastFreeDay → LA)Jun 11, 2026
Line LFD (lineLastFreeDay, rail → Denver)Jun 12, 2026
Rail ramp LFD (lastFreeDayAtRailStation → Denver)Jun 13, 2026
⚠️

Note the rail ramp LFD: 2026-06-14T05:59:59.999Z is 23:59:59.999 on June 13 in America/Denver. Reading it as UTC would have shown June 14 — a full day late.

Summary checklist

  • Treat every timestamp as UTC and parse it with a time zone-aware library.
  • Localize each history / timeline event with its own location.timezone.
  • Localize ETAs and LFDs with the facility zone from the tables above.
  • For LFDs, convert to local time before extracting the calendar date.
  • Branch on isRailMove for lineLastFreeDay, and use finalDestination.timezone for rail-ramp fields.
  • Handle a missing timezone gracefully.