A market hours API answers a question that breaks more polling code than malformed JSON ever does: is this market actually trading right now A cron job that pulls the latest AAPL quote every five minutes runs without incident for weeks. Then a mid-week holiday arrives, the session never opens, and the job keeps polling anyway. The dashboard spends the entire day rendering yesterday's closing price as if it were live, and the first person to notice is a user asking why the number hasn't moved since Thursday.
The usual fix, a hardcoded holiday list, ages badly. It misses half days, it needs updating every year, and it multiplies painfully the moment you track a second market in another region. The better fix is to ask your data provider, which already has to know when every market it covers is trading.
What a market hours API returns#
SiftingIO publishes schedule data for 23 markets across North America, Latin America, Europe, and Asia-Pacific under /v1/fnd/markets. Four endpoints cover the whole problem:
# Which markets exist, and is each one open right now
curl -H "X-API-Key: $SIFTING_KEY" "https://api.sifting.io/v1/fnd/markets"
curl -H "X-API-Key: $SIFTING_KEY" "https://api.sifting.io/v1/fnd/markets/status"
# Weekly schedule and holiday calendar for one market
curl -H "X-API-Key: $SIFTING_KEY" "https://api.sifting.io/v1/fnd/markets/us_equities/hours"
curl -H "X-API-Key: $SIFTING_KEY" "https://api.sifting.io/v1/fnd/markets/us_equities/calendar"
The catalog endpoint lists every market with its slug: us_equities, forex, crypto, jp_equities, and so on. The status endpoints report whether a market is open at the moment of the call, either for a single market or as one snapshot covering all 23. The hours endpoint returns the weekly session schedule in venue-local time. The calendar endpoint returns holidays and half days for a range of up to 730 days, enough to prefetch two full years of schedule in a single request.
Wiring the check into a polling job#
The simplest integration is a guard at the top of the polling function:
import os
import requests
BASE = "https://api.sifting.io/v1"
HEADERS = {"X-API-Key": os.environ["SIFTING_KEY"]}
def market_open(market: str) -> bool:
r = requests.get(f"{BASE}/fnd/markets/{market}/status",
headers=HEADERS, timeout=5)
r.raise_for_status()
return bool(r.json().get("open")) # see /docs for the exact field names
def poll_quote(ticker: str):
if not market_open("us_equities"):
return None
r = requests.get(f"{BASE}/last/quote/stocks/{ticker}",
headers=HEADERS, timeout=5)
r.raise_for_status()
return r.json()
quote = poll_quote("AAPL")
For a job that fires every few minutes this works, but notice the cost: every poll now spends two API calls where it used to spend one. On the free tier, which meters 10,000 REST calls a month, guarding a frequent poll this way roughly doubles your consumption.
The cheaper pattern is to prefetch. Pull the calendar once a night, store the open and close times for the next trading day, and evaluate the guard locally against the clock. One API call a day replaces hundreds of status checks, and the polling loop only touches the network while the local schedule says the session is live. The live status endpoint then becomes a fallback for anything a calendar can't predict, like a schedule change published after your nightly fetch.
Markets don't share a clock#
The reason to parameterize the guard by market slug is that asset classes keep genuinely different hours. Crypto trades continuously, so its status check is nearly always true and the guard costs almost nothing. Forex runs around the clock during the week but goes quiet over the weekend, and that weekend gap is exactly where a naive poller accumulates hours of identical quotes. Equity markets keep defined sessions with holidays that differ by country and early closes before certain dates. A multi-asset app that treats all of them the same either polls dead markets all weekend or sits idle through a live session.
For a dashboard header that shows several markets at once, the all-markets snapshot at /v1/fnd/markets/status covers the whole strip in one call instead of one request per market.
Common pitfalls#
Half days look like full days. If the guard only asks whether today appears in the holiday calendar, early closes slip through: the calendar marks the day as a trading day, the session ends hours ahead of normal, and every poll after the early close returns the same last trade until midnight. Read the close time from the calendar entry itself rather than only checking whether the date is listed.
Venue-local hours meet a UTC server clock. The hours endpoint reports schedules in venue-local time, while live ticks carry UTC epoch milliseconds and your servers most likely run on UTC. A fixed-offset conversion works until daylight saving time shifts, and because regions change their clocks on different dates, an offset that was correct in January is wrong for a few weeks each spring and autumn. Either rely on the status endpoint, which resolves this server-side, or convert with a real timezone database rather than offset arithmetic.
A 503 stale_snapshot usually means the market is closed. Live endpoints under /v1/last return this error when the latest data is older than the freshness threshold, and the body includes last_t and server_now so you can see exactly how old the snapshot is. During a closed session that staleness is expected. A generic retry-with-backoff wrapper that treats every 503 as transient will hammer the API all night, and on a metered plan those retries count against your quota. Gate on market status first, and reserve alerting for a stale_snapshot that arrives during trading hours, because that one is a real problem.
Schedule-aware polling is a small amount of code, and it removes a whole class of silent staleness bugs. The market hours, status, and calendar endpoints are documented with the rest of the API. Read the docs



