What timezone is market data in? Almost always UTC, and SiftingIO follows that convention across the whole API: full timestamps are RFC 3339 strings in UTC, live ticks carry an int64 Unix epoch timestamp in milliseconds, and date-only fields such as SEC filing dates are plain ISO 8601 (YYYY-MM-DD). Nothing arrives in the venue's local time or in your server's local time.
That one convention removes a whole category of ambiguity, but it doesn't remove the work. Trading sessions are still defined in venue-local time. Daylight saving time still moves those sessions around the UTC day twice a year. The forex week still opens on what is Sunday in one city and Monday in another. This post covers the timestamp formats the API actually returns, how session times relate to UTC, and how to convert in Python and JavaScript without the classic off-by-one-hour bugs.
The three timestamp formats you'll receive#
| Field type | Format | Example | Where it appears |
|---|---|---|---|
| Live ticks | int64 Unix epoch milliseconds, field t | 1778019852426 | WebSocket tick frames, /v1/last/* snapshots |
| Full timestamps | RFC 3339, UTC | 2026-07-31T13:00:00Z | Historical bars, meta.as_of, event times |
| Date-only fields | ISO 8601 date | 2026-07-31 | Filing dates, holiday calendars, XBRL periods |
Epoch milliseconds are the fastest to compare and the hardest to misread: 1778019852426 names the same instant on every machine on Earth. RFC 3339 strings are self-describing (the trailing Z marks UTC), sort lexicographically, and parse in one call in every mainstream language. Date-only fields are deliberately zone-free. A 10-K filed on 2026-07-31 was filed on that date; attaching a time and zone would add precision the source doesn't have.
Historical bars follow the same rule and align to UTC boundaries: an hourly EURUSD bar stamped 13:00Z covers 13:00:00 to 13:59:59.999 UTC in every season.
curl -H "X-API-Key: $SIFTING_KEY" \
-H "Accept-Encoding: gzip" --compressed \
"https://api.sifting.io/v1/hist/forex/EURUSD/bars?interval=1h"
The gzip header isn't optional here. Historical bar endpoints require compression and return 406 gzip_required without it.
Market data is UTC, trading sessions are local time#
US equities trade 09:30 to 16:00 in New York local time. In UTC that window is 13:30 to 20:00 for part of the year and 14:30 to 21:00 for the rest, because daylight saving time moves New York relative to UTC while the session definition stays put. Tokyo doesn't observe DST, so its UTC window never moves. Europe changes clocks on a different week than the United States, which means the relative alignment of the two regions drifts for a few days every March and again in late autumn.
The consequence: you can't hardcode a UTC session window for any DST-observing market. SiftingIO exposes the schedule as data instead. GET /v1/fnd/markets/{market}/hours returns a market's weekly schedule in venue-local time, GET /v1/fnd/markets/{market}/status says whether it's open right now, and GET /v1/fnd/markets/{market}/calendar covers holidays and half-days up to 730 days out. The browsable version, covering all 23 markets, is on the market hours pages.
Forex adds the weekend boundary problem. The FX week runs from Sunday evening to Friday evening in New York terms, which lands near 21:00 or 22:00 UTC depending on the season. "Monday's session" therefore begins on Sunday's UTC date, and code that buckets FX bars by calendar date will assign the first hours of the week to Sunday, then wonder why Monday looks short.
Economic releases carry their own timestamp subtleties: a CPI print is a scheduled instant, and which bar it lands in depends on your bar alignment. That topic has its own post on economic calendar timestamps and revisions.
Converting timestamps correctly in Python and JavaScript#
Two rules cover nearly everything. Store and compute in UTC. Convert to a named IANA zone only at the moment a human reads the value.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
# A live tick's "t" field: epoch milliseconds
t = 1778019852426
utc = datetime.fromtimestamp(t / 1000, tz=timezone.utc)
# An hourly bar timestamp: RFC 3339
bar_open = datetime.fromisoformat("2026-07-31T13:00:00+00:00")
# Display in a venue zone: IANA name, never a fixed offset
local = utc.astimezone(ZoneInfo("America/New_York"))
The one-line trap is the missing tz=. Called as datetime.fromtimestamp(t / 1000), Python interprets the epoch value in the machine's local zone and hands back a naive datetime that only looks correct when the machine happens to run in UTC. CI servers usually do; laptops usually don't. That's why this bug ships.
JavaScript's Date is epoch-milliseconds native, so tick timestamps need no parsing:
const utc = new Date(1778019852426);
const tokyo = new Intl.DateTimeFormat("en-GB", {
timeZone: "Asia/Tokyo",
dateStyle: "short",
timeStyle: "medium",
}).format(utc);
Date stores UTC internally. The local-time behavior it gets blamed for lives in methods like getHours() and in default formatting; format through Intl.DateTimeFormat with an explicit timeZone and the value stays unambiguous end to end.
Common mistakes, and what each one breaks#
| Mistake | What goes wrong | Fix |
|---|---|---|
Naive datetimes (fromtimestamp without tz=) | Every timestamp silently shifts by the machine's UTC offset; joins between two sources land hours apart | Pass tz=timezone.utc everywhere; treat any naive datetime as a bug |
| Hardcoded offsets (UTC-5 for New York) | Correct half the year, off by one hour after each DST transition | Use IANA zone names (America/New_York) via zoneinfo or Intl |
| Converting to local time at ingestion | DST makes one local hour per year ambiguous and another nonexistent; stored data can't be disambiguated later | Store UTC, convert only at display time |
| Bucketing FX by calendar date | The week's first bars carry Sunday's UTC date; daily aggregates misassign them | Bucket by session boundaries from the hours endpoint rather than by date string |
| Comparing sessions across regions during DST changeover | Regions switch on different weeks, so overlap windows (London and New York, for example) drift in March and autumn | Compute each session in its own IANA zone, then compare in UTC |
Two more gotchas surface in practice. The hours endpoint returns venue-local times by design, so converting this week's schedule to UTC once and caching it leaves a stale window after the next DST transition; recompute per day and check the calendar endpoint for half-days. And date-only strings bite in JavaScript: new Date("2026-07-31") parses as UTC midnight, so rendering it in a negative-offset zone displays July 30, which makes a filing list show every date one day early for users in the Americas.
UTC in, UTC through the pipeline, local only at the screen. Get that ordering right and DST transitions, weekend boundaries, and cross-market comparisons reduce to lookups against the hours and calendar endpoints. The full timestamp conventions, including pagination as_of fields and XBRL period codes, are in the API reference. Read the docs



