TWSE OpenAPI only returns today. How do I get history?
The official open endpoint returns a whole-market snapshot of the most recent trading day, and passing a date does not change what comes back — measured, adding a date parameter still returned the same 1,379 rows for the same single day. So history has exactly two sources: save it yourself from today onward, or query a set that was already accumulated. The first starts today; the second reaches back to 2004.
Confirm the snapshot behaviour yourself
The endpoint needs no registration and no key — a plain GET returns a JSON array. Measured, it returns 1,379 rows in which the Date field holds a single value: the whole market for one trading day. Adding a date parameter returns 200, and the same day.
import requests
rows = requests.get(
"https://openapi.twse.com.tw/v1/exchangeReport/STOCK_DAY_ALL",
timeout=30,
).json()
print("rows", len(rows))
print("distinct dates in the payload:", len({r["Date"] for r in rows}))
print("sample", rows[0]["Date"], rows[0]["Code"], rows[0]["ClosingPrice"])
older = requests.get(
"https://openapi.twse.com.tw/v1/exchangeReport/STOCK_DAY_ALL",
params={"date": "20260601"},
timeout=30,
).json()
print("dates after passing a date:", {r["Date"] for r in older})Route one: save a snapshot every day from now on
Schedule a job after the close and persist each snapshot. The catch is that this history begins the day you start — a job scheduled today cannot recover yesterday. You will also convert ROC-calendar dates and string values yourself, and you should make the job idempotent so a re-run does not write the same day twice.
import json, pathlib, requests
def roc_to_iso(roc: str) -> str:
"""ROC calendar 1150811 -> 2026-08-11."""
return f"{int(roc[:-4]) + 1911}-{roc[-4:-2]}-{roc[-2:]}"
rows = requests.get(
"https://openapi.twse.com.tw/v1/exchangeReport/STOCK_DAY_ALL", timeout=30
).json()
trade_day = roc_to_iso(rows[0]["Date"])
out = pathlib.Path(f"snapshots/{trade_day}.json")
out.parent.mkdir(exist_ok=True)
if out.exists():
print("already saved, skipping:", trade_day) # idempotent: re-runs do not duplicate
else:
out.write_text(json.dumps(rows, ensure_ascii=False))
print("saved", trade_day, len(rows), "rows")Route two: query history that already exists
If what you need is 2015 data now, accumulating cannot produce it. Listed daily prices here reach back to 2004-02-11, covering 1,687 stocks, with ISO dates, numeric types and a queryable date range.
r = requests.get( "https://api.twmarketdata.com/v2/datasets/twse-daily-price", headers={"X-API-Key": "sk_live_your_key"}, params={"symbol": "2330", "start_date": "2026-06-01", "end_date": "2026-07-31"}, timeout=20,)r.raise_for_status()payload = r.json() print("rows", payload["count"], "data as of", payload["data_as_of"])for row in payload["rows"][:3]: print(row["date"], row["open"], row["close"], row["volume_shares"])Choosing between them
- Only need data from now on, and happy to run the schedule and the cleaning? Route one is sufficient and free.
- Need existing history, or would rather not maintain a daily job and its backfills? Route two saves time, not data — the origin is the same official source either way.
- They combine well: take the existing history from route two and keep accumulating with route one.
Listed and OTC are separate on the official side
The listed and OTC markets are published by different bodies through different endpoints, with different field names. Whole-market analysis means consuming both and aligning them yourself. Here they share one schema with the market labelled, though you still need to check you are not counting a name twice before aggregating.
What we do not have
- No real-time quotes. This is historical and post-close data, and the official open endpoint is likewise a post-close snapshot.
- We cannot help with route one's blind spot: a day nobody saved is gone for everyone. That is a property of time, not of a data source.
- Coverage is whatever the dataset page states — those figures are generated from the database rather than typed in.
Common questions
1Why does the date parameter have no effect?
2How long until my own archive is deep enough?
3Will the numbers differ from the official ones?
4Why is the date formatted 1150811?
Want to query this data yourself? Create a free account.