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
timezonefield 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:
| Field | Description |
|---|---|
origin | The inland origin |
originPort | The initial ocean port of loading |
destinationPort | The final ocean port of discharge |
finalDestination | The container's final delivery destination |
currentLocation | The last known facility that handled the container |
history[].location | The location of each observed milestone |
timeline[].location | The 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 }
}
}
timezonecan on rare occasions be absent — typically for a plain inland locality we have not been able to geocode. Always handle a missingtimezonegracefully (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'sConvert 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 IANAtimezoneto 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:
| Field | Localize with |
|---|---|
history[].timestamp, timeline[].timestamp | that event's location.timezone |
etaAtTerminal | destinationPort.timezone |
etaAtDestination, etnAtDestination (rail moves only) | finalDestination.timezone |
lastFreeDay | destinationPort.timezone |
detentionLastFreeDay | finalDestination.timezone |
lastFreeDayAtRailStation | finalDestination.timezone |
lineLastFreeDay | depends 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 dateIf 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
| Field | What it is | Localize with |
|---|---|---|
lastFreeDay | Terminal demurrage LFD at the final ocean discharge port | destinationPort.timezone |
lineLastFreeDay | Demurrage LFD issued by the steamship line | port or destination — see below |
detentionLastFreeDay | Detention / per-diem LFD | finalDestination.timezone |
lastFreeDayAtRailStation | Demurrage LFD at the final rail station | finalDestination.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:
lastFreeDayAtRailStationis the rail-ramp demurrage deadline. Localize it withfinalDestination.timezone(the rail facility's zone), not the ocean port zone.lineLastFreeDayapplies to either the ocean terminal or the final rail station depending on the move. Choose the zone byisRailMove:
const lineLfdZone = container.isRailMove
? container.finalDestination.timezone // rail: deadline is at the ramp
: container.destinationPort.timezone; // ocean: deadline is at the portetaAtDestination/etnAtDestinationare only present on rail moves (isRailMove: true) and refer to the final rail facility, so localize them withfinalDestination.timezone.- On an active rail move, the terminal-level
lastFreeDaymay 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 assumelastFreeDayis 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:
| Value | Localized |
|---|---|
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.999Zis 23:59:59.999 on June 13 inAmerica/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/timelineevent with its ownlocation.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
isRailMoveforlineLastFreeDay, and usefinalDestination.timezonefor rail-ramp fields. - Handle a missing
timezonegracefully.
