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?
Measured, the server returns 200 and the same latest trading day whether or not a date is supplied — the set of dates in the payload is identical either way. It is designed as a current snapshot, not a historical query interface.
2How long until my own archive is deep enough?
As long as you run it. Three years of history takes three years. There is no shortcut, which is exactly why a project needing backfill has to source it from somewhere that already has it.
3Will the numbers differ from the official ones?
The origin is the same. What differs is the packaging: ROC dates converted to ISO, string values converted to numbers, a queryable date range, and the listed and OTC markets under one schema.
4Why is the date formatted 1150811?
That is the ROC calendar: year 115, 11 August, which is 2026-08-11. Official open data commonly uses it, so a self-built pipeline converts it — there is runnable conversion code on the data cleaning page.

Want to query this data yourself? Create a free account.

Related articles