31 endpoints · Market Data · Fundamentals · Institutional Flow · Other
https://api.blave.orgapi-key + secret-key headers on every requeststock_id — TWSE (上市) / TPEx (上櫃) code, e.g. 2330, 0050, 00878, 006208. Letters and digits (dots allowed), ≤ 8 chars, else 400 {"error": "Invalid stock_id"}.start / end — YYYY-MM-DD, inclusive. Unless a block says otherwise they are not format-validated (filtering is a string comparison), so always send YYYY-MM-DD.503 {"error": "Upstream rate limit. Please retry shortly."} — retry later.batch/<data_type> (≤ 50 ids per call); never loop a single-stock endpoint over a universe./studio/market/twstock/list None.
data = requests.get(f"{BASE_URL}/studio/market/twstock/list", headers=headers, timeout=60).json()["data"]# [{"stock_id": "00400A", "name": "主動國泰動能高息", "close": 14.72, "market": "TWSE",# "industry_code": None, "listing_date": None}, ...] # 2,392 rows in this response{"data": [...]}, ~2,400 rows (上市 + 上櫃, incl. ETFs; 興櫃 excluded).
| Field | Type | Description |
|---|---|---|
stock_id | string | Code |
name | string | Name |
close | float | null | Latest daily close (null when upstream had none, e.g. halted) |
market | string | TWSE (上市) or TPEx (上櫃) |
industry_code | string | null | TWSE/TPEx raw numeric 產業別 code, passthrough (not decoded). null for ETFs / non-company securities. Common codes: 15 航運業, 17 金融保險業, 22 生技醫療業, 24 半導體業, 25 電腦及週邊設備業, 26 光電業, 27 通信網路業, 28 電子零組件業, 29 電子通路業, 30 資訊服務業, 31 其他電子業 |
listing_date | string | null | YYYY-MM-DD; null for ETFs / non-company securities |
Shared errors only (500 if both markets fail).
[:N] truncation concentrates in a few sectors./studio/market/twstock/info/<stock_id> info = requests.get(f"{BASE_URL}/studio/market/twstock/info/2330", headers=headers, timeout=30).json()["data"]# {"stock_id": "2330", "name": "台積電", "close": 2410.0, "industry_code": "24", "listing_date": "1994-09-05"}{"stock_id": "<stock_id>", "data": {...}}; data has the same fields as one /list row.
| Status | Body | When |
|---|---|---|
| 400 | {"error": "Invalid stock_id"} | Format check failed |
| 404 | {"error": "Stock not found"} | Not a currently listed security |
/studio/market/twstock/price/<stock_id> response = requests.get(f"{BASE_URL}/studio/market/twstock/price/2330", headers=headers, timeout=60)data = response.json()["data"] # no start/end → full history, 8,069 rows# [{"date": "1994-10-01", "stock_id": "2330", "open": 171.0, "high": 172.0,# "low": 171.0, "close": 172.0, "spread": 1.0,# "volume": 1660506, "turnover_value": 284858008, "turnover_count": 684},# {"date": "1994-10-03", "stock_id": "2330", "open": 172.0, "high": 173.0,# "low": 171.0, "close": 172.0, "spread": 0.0,# "volume": 1560000, "turnover_value": 268084000, "turnover_count": 711}, ...]{"stock_id": "<stock_id>", "data": [ ... ]}, rows ascending by date.
| Field | Type | Unit | Description |
|---|---|---|---|
date | string | YYYY-MM-DD | Trading day |
stock_id | string | — | Stock code |
open / high / low / close | float | TWD | Unadjusted prices |
spread | float | TWD | Change vs the previous close |
volume | int | shares (股) | Trading volume |
turnover_value | int | TWD | Trading value |
turnover_count | int | trades (筆) | Number of trades |
| Status | Body | When |
|---|---|---|
| 400 | {"error": "Invalid stock_id"} | stock_id fails the format check |
| 404 | {"error": "No data found for stock_id: <stock_id>"} | Unknown code / no data upstream |
| 503 | {"error": "Upstream rate limit. Please retry shortly."} | Upstream quota exhausted — retry later |
start / end are not format-validated: a malformed value filters by string comparison instead of returning 400, so always send YYYY-MM-DD.price_adj for backtesting total return; this endpoint is unadjusted.batch/price (≤ 50 ids per call) instead of looping this endpoint./studio/market/twstock/price_adj/<stock_id> Same as /price/<stock_id>.
params = {"start": "2020-01-01", "end": "2024-12-31"}data = requests.get(f"{BASE_URL}/studio/market/twstock/price_adj/2330", headers=headers, params=params, timeout=60).json()["data"]Same shape and fields as /price/<stock_id>; open / high / low / close are adjusted, volume / turnover_* are identical to the raw series.
Same as /price/<stock_id>.
/studio/market/twstock/quote/<stock_id> data = requests.get(f"{BASE_URL}/studio/market/twstock/quote/2330", headers=headers, timeout=30).json()["data"]# {"open": 2415.0, "high": 2465.0, "low": 2415.0, "close": 2445.0,# "change_price": -20.0, "change_rate": -0.81, "average_price": 2432.58,# "volume": 4245, "total_volume": 26403, "amount": 10379025000, "total_amount": 64227410000,# "yesterday_volume": 27390, "buy_price": 2445.0, "buy_volume": 17,# "sell_price": 2450.0, "sell_volume": 11, "volume_ratio": 0.96,# "quote_time": "2026-07-03 14:30:00", "stock_id": "2330", "tick_type": 2}{"stock_id": "<stock_id>", "data": {...}}; data is a flat object.
| Field | Type | Description |
|---|---|---|
open / high / low / close | float | Today's OHLC so far |
change_price / change_rate | float | Change vs previous close (TWD / %) |
average_price | float | Average traded price |
volume | int | Latest tick's volume |
total_volume | int | Cumulative volume today |
amount / total_amount | float | Latest tick / cumulative traded value |
yesterday_volume | int | Previous day's volume |
buy_price / buy_volume | float / int | Best bid |
sell_price / sell_volume | float / int | Best ask |
volume_ratio | float | Volume ratio |
quote_time | string | YYYY-MM-DD HH:MM:SS — a full timestamp, unlike other endpoints' bare date |
stock_id | string | Code |
tick_type | int | 0 indeterminate, 1 sell-initiated (賣盤成交), 2 buy-initiated (買盤成交) |
| Status | Body | When |
|---|---|---|
| 400 | {"error": "Invalid stock_id"} | Format check failed |
| 404 | {"error": "No data found for stock_id: <stock_id>"} | No quote |
| 503 | {"error": "Upstream rate limit. Please retry shortly."} | Upstream quota exhausted |
/studio/market/twstock/quote data = requests.get(f"{BASE_URL}/studio/market/twstock/quote", headers=headers, params={"stock_ids": "2330,2317"}, timeout=30).json()["data"]# {"2330": {...}, "2317": {...}}{"data": {"<stock_id>": {...quote fields...}, ...}}; fields as /quote/<stock_id>. Ids with no quote are absent.
| Status | Body | When |
|---|---|---|
| 400 | {"error": "stock_ids parameter required"} | Missing |
| 400 | {"error": "stock_ids exceeds max 50 per request"} | Too many |
| 400 | {"error": "Invalid stock_id in list"} | A code fails the format check |
| 503 | {"error": "Upstream rate limit. Please retry shortly."} | Upstream quota exhausted |
/studio/market/twstock/quote/all None.
rows = requests.get(f"{BASE_URL}/studio/market/twstock/quote/all", headers=headers, timeout=30).json()["data"]{"data": [{...quote fields...}, ...]} (~2,800 rows); fields as /quote/<stock_id>.
| Status | Body | When |
|---|---|---|
| 503 | {"error": "Upstream rate limit. Please retry shortly."} | Upstream quota exhausted |
/studio/market/twstock/minute/ohlcv/<stock_id>/<schema> 1 = forward-adjusted (後復權) OHLC, same factors as /price_adj; volume is never adjustedMax range per request (end − start): 1m 31 d · 5m 62 d · 15m 93 d · 30m 186 d · 60m 365 d · 1d 3650 d.
body = requests.get(f"{BASE_URL}/studio/market/twstock/minute/ohlcv/2330/1m", headers=headers, params={"start": "2026-08-06", "end": "2026-08-06"}, timeout=60).json()# {"stock_id": "2330", "schema": "1m", "data": [# {"close": 2385.0, "high": 2395.0, "low": 2385.0, "open": 2395.0, "ts": "2026-08-06 01:00:00+00:00", "volume": 2540}, ...]}{"stock_id", "schema", "data": [...]}.
| Field | Type | Unit | Description |
|---|---|---|---|
ts | string | UTC ISO | Bar open time (minute-start label); the 13:30 Taipei bar is the closing auction |
open / high / low / close | float | TWD | Prices |
volume | int | lots (張), not shares | Volume. TAIEX bars always carry volume: 0 at every schema — the index has no volume, so none is served; do not read it as a turnover figure |
| Status | Body | When |
|---|---|---|
| 400 | {"error": "Invalid stock_id (must be a listed TWSE/TPEx security)"} | Not listed |
| 400 | {"error": "Invalid schema. Allowed: [...]"} | Bad schema |
| 400 | {"error": "Invalid adjust (use 0 or 1)"} | Bad adjust |
| 400 | {"error": "Invalid date format. Use YYYY-MM-DD"} | Malformed date |
| 400 | {"error": "date_range_too_large", "message": "...", "max_days": <n>} | Range over the cap |
| 503 | {"error": "market data temporarily unavailable"} | Store read failure |
| 503 | {"error": "adjust factors temporarily unavailable"} | adjust=1 and factors unavailable — never silently served unadjusted |
minute/ohlcv/symbols before requesting years of history.200 with []./quote/<stock_id> for the live price./studio/market/twstock/minute/ohlcv/symbols None.
covered = requests.get(f"{BASE_URL}/studio/market/twstock/minute/ohlcv/symbols", headers=headers, timeout=30).json()["data"]{"data": ["2330", "TAIEX", ...]} — ids that already have minute-line data on disk (currently listed stocks plus TAIEX), sorted.
Shared errors only.
/studio/market/twstock/kbar/<stock_id> end − start ≤ 31 daysdata = requests.get(f"{BASE_URL}/studio/market/twstock/kbar/2330", headers=headers, params={"start": "2026-08-03", "end": "2026-08-07"}, timeout=60).json()["data"]{"stock_id", "data": [...]}, ascending.
| Field | Type | Description |
|---|---|---|
date | string | YYYY-MM-DD (Taipei) |
minute | string | HH:MM:SS Taipei, bar start (09:00–13:30) |
stock_id | string | Code |
open / high / low / close | float | TWD |
volume | int | Lots (張) |
| Status | Body | When |
|---|---|---|
| 400 | {"error": "Invalid stock_id (must be a listed TWSE/TPEx security)"} | Not listed |
| 400 | {"error": "Date range exceeds 31 days"} | Range too long, or malformed date |
minute/ohlcv/<stock_id>/1m. A listed stock with no minute data returns [] (this endpoint does not trigger seeding)./studio/market/twstock/market_value/<stock_id> data = requests.get(f"{BASE_URL}/studio/market/twstock/market_value/2330", headers=headers, params={"start": "2026-01-01"}, timeout=60).json()["data"]{"stock_id", "data": [{"date", "stock_id", "market_value"}]}; market_value in TWD.
400 invalid stock_id, 404 no data, 503 upstream quota.
/studio/market/twstock/market_value/all body = requests.get(f"{BASE_URL}/studio/market/twstock/market_value/all", headers=headers, params={"top": 10}, timeout=60).json()# {"date": "2026-09-17", "twse_ex_etf_market_value": 150952470507848,# "data": [{"is_etf": false, "market": "TWSE", "market_value": 62885997412475,# "name": "台積電", "rank": 1, "stock_id": "2330"},# ...,# {"is_etf": true, "market": "TWSE", "market_value": 2383312875000,# "name": "元大台灣50", "rank": 6, "stock_id": "0050"}, ...]}# Index weight of a 上市 non-ETF stock (works with any `top`, the denominator is whole-market)row = body["data"][0] # 2330 — market "TWSE", is_etf falseshare = row["market_value"] / body["twse_ex_etf_market_value"]# ETFs rank alongside stocks (0050 is rank 6) — filter them with `is_etf`. Never by code prefix,# and never by re-fetching a classification: `is_etf` already IS that classification.rows = requests.get(f"{BASE_URL}/studio/market/twstock/market_value/all", headers=headers, params={"top": 100}, timeout=60).json()["data"]top50 = [r["stock_id"] for r in rows if not r["is_etf"]][:50] # ETF-free market-cap top 50tpex_only = [r["stock_id"] for r in rows if r["market"] == "TPEx"] # board filter is exact{"date": "YYYY-MM-DD", "twse_ex_etf_market_value": int, "data": [...]}, sorted by market value descending.
| Field | Type | Description |
|---|---|---|
date | string | As-of day actually used (latest published; can lag today by a day) |
twse_ex_etf_market_value | int | TWD. Sum of the market == "TWSE" rows that are not ETFs, on the same as-of day. Whole-market denominator — not reduced by top |
data[].rank | int | 1-based rank |
data[].stock_id | string | Code |
data[].name | string | Name |
data[].market | string | "TWSE" (上市) or "TPEx" (上櫃), from the TWSE / TPEx listing rosters |
data[].market_value | int | TWD |
data[].is_etf | bool | true for the ETFs in the ranking (359 of 2,370 rows on 2026-09-17). false means "not in the ETF set", not "confirmed not an ETF": a security FinMind publishes no industry_category for — e.g. REIT 01010T — is false, and it stays inside twse_ex_etf_market_value |
| Status | Body | When |
|---|---|---|
| 400 | {"error": "top must be an integer between 1 and 3000"} | Bad top |
| 404 | {"error": "No recent market value data available"} (or a listing-incomplete message) | No recent data |
| 503 | {"error": "Upstream rate limit. Please retry shortly."} | Upstream quota exhausted |
/market_value/<stock_id> for "top N by market cap" questions.rank and twse_ex_etf_market_value are different universes. rank is whole-market — 上市 + 上櫃, ETFs included — and did not change when the denominator was added. The denominator is 上市 only and excludes ETFs (FinMind industry_category in {ETF, 上櫃ETF, 上櫃指數股票型基金(ETF)}); REITs and preferred shares are not excluded from it. So market_value / twse_ex_etf_market_value is an index weight only for a row whose market is "TWSE" and which is not an ETF — a TPEx or ETF row over this denominator is not a weight — while rank stays a whole-market rank. Don't read the two as one ranking.market is a listing-board tag, not an ETF flag — is_etf is the ETF flag. Use it for an ETF-free universe. Don't infer ETFs from the stock_id prefix (00 is a market convention rather than a contract, and it misses REITs 01xxxT) and don't re-fetch a classification from FinMind. is_etf is exactly the criterion twse_ex_etf_market_value is computed on, so an is_etf filter and the denominator can never disagree./studio/market/twstock/per/<stock_id> data = requests.get(f"{BASE_URL}/studio/market/twstock/per/2330", headers=headers, params={"start": "2026-01-01", "end": "2026-07-22"}, timeout=60).json()["data"]# [{"date": "2026-07-21", "stock_id": "2330", "dividend_yield": 0.95, "PER": 34.87, "PBR": 11.05}, ...]{"stock_id", "data": [...]}.
| Field | Type | Description |
|---|---|---|
date | string | Trading day |
stock_id | string | Code |
dividend_yield | float | Dividend yield (%) |
PER | float | Price / earnings |
PBR | float | Price / book |
400 invalid stock_id, 404 no data, 503 upstream quota.
batch/per./studio/market/twstock/financials/<stock_id> import pandas as pddata = requests.get(f"{BASE_URL}/studio/market/twstock/financials/2330", headers=headers, params={"start": "2022-01-01", "end": "2024-12-31"}, timeout=30).json()["data"]# [{"date": "2022-03-31", "stock_id": "2330", "type": "Revenue", "value": 491075000000.0, "origin_name": "營業收入"}, ...]wide = pd.DataFrame(data).pivot_table(index="date", columns="type", values="value", aggfunc="first"){"stock_id", "data": [...]}, long format (one row per item per quarter).
| Field | Type | Description |
|---|---|---|
date | string | Quarter end (03-31 / 06-30 / 09-30 / 12-31) |
stock_id | string | Code |
type | string | Item code, e.g. Revenue, GrossProfit, OperatingIncome, NetIncome, EPS, TAX, OtherComprehensiveIncome |
value | float | Value (TWD; EPS in TWD per share) |
origin_name | string | Chinese label — use it to identify unfamiliar type codes |
400 invalid stock_id, 404 no data, 503 upstream quota.
/studio/market/twstock/balance_sheet/<stock_id> Same as /financials/<stock_id>.
data = requests.get(f"{BASE_URL}/studio/market/twstock/balance_sheet/2330", headers=headers, params={"start": "2022-01-01"}, timeout=30).json()["data"]Same long format as /financials/<stock_id>. Key type codes: CashAndCashEquivalents, TotalAssets, TotalLiabilities, TotalEquity. Codes ending in _per are % of total assets.
400 invalid stock_id, 404 no data, 503 upstream quota.
/studio/market/twstock/cashflow/<stock_id> Same as /financials/<stock_id>.
import pandas as pddata = requests.get(f"{BASE_URL}/studio/market/twstock/cashflow/2330", headers=headers, params={"start": "2022-01-01"}, timeout=30).json()["data"]cfo = (pd.DataFrame(data).query("type == 'CashFlowsFromOperatingActivities'") .assign(date=lambda d: pd.to_datetime(d["date"])).set_index("date")["value"].sort_index())cfo_q = cfo - cfo.groupby(cfo.index.year).shift(1).fillna(0) # YTD → single quarterSame long format as /financials/<stock_id>. Main type codes (FinMind's own codes; the full set varies by company and year, so treat the response as the authoritative list and use origin_name for anything not below). Filter on type, not origin_name — the same code's label switches between full- and half-width brackets across years:
type | origin_name |
|---|---|
CashFlowsFromOperatingActivities | 營業活動之淨現金流入(流出) |
CashProvidedByInvestingActivities | 投資活動之淨現金流入(流出) |
CashFlowsProvidedFromFinancingActivities | 籌資活動之淨現金流入(流出) |
PropertyAndPlantAndEquipment | 取得不動產、廠房及設備 (capex, negative) |
Depreciation / AmortizationExpense | 折舊費用 / 攤銷費用 |
CashBalancesIncrease | 本期現金及約當現金增加(減少)數 |
CashBalancesBeginningOfPeriod / CashBalancesEndOfPeriod | 期初(年初)/ 期末現金及約當現金餘額 |
Flow items are year-to-date (YTD) cumulative, as filed in Taiwan: the 03-31 row is Q1 alone, 06-30 is Q1–Q2, 09-30 is Q1–Q3, 12-31 is the full year. For a single quarter subtract the previous quarter of the same year (Q1 needs no subtraction) — so fetch from a January start. CashBalancesEndOfPeriod is the balance at the quarter end; CashBalancesBeginningOfPeriod is the balance at the start of the year. This endpoint has no period parameter — it always returns YTD (the ?period=quarter single-quarter view exists only on the web chart route, not here).
400 invalid stock_id, 404 no data, 503 upstream quota.
/studio/market/twstock/monthly_revenue/<stock_id> import pandas as pddata = requests.get(f"{BASE_URL}/studio/market/twstock/monthly_revenue/2330", headers=headers, params={"start": "2024-01-01", "end": "2024-12-31"}, timeout=30).json()["data"]df = pd.DataFrame(data).sort_values(["revenue_year", "revenue_month"])df["yoy_pct"] = df["revenue"].pct_change(periods=12) * 100# [{"date": "2024-01-01", "stock_id": "2330", "country": "Taiwan", "create_time": "",# "revenue": 176299866000, "revenue_year": 2023, "revenue_month": 12},# {"date": "2024-02-01", "stock_id": "2330", "country": "Taiwan", "create_time": "",# "revenue": 215785127000, "revenue_year": 2024, "revenue_month": 1}, ...]{"stock_id", "data": [...]}, one row per month.
| Field | Type | Description |
|---|---|---|
date | string | First day of the month after the revenue month — 2024-01-01 carries revenue_year: 2023, revenue_month: 12. Never use it as the period |
stock_id | string | Code |
country | string | Market — observed value is Taiwan |
create_time | string | Observed empty ("") on every row |
revenue | int | Monthly revenue in TWD (元), not thousands — inferred from magnitude, not stated upstream: 2330 for revenue_year 2024, revenue_month 1 returned 215785127000, which is only a sane figure read as 元 (NT$215.8bn) |
revenue_month | int | Revenue month (1–12) — use this, not date, for the period |
revenue_year | int | Revenue year |
400 invalid stock_id, 404 no data, 503 upstream quota.
/studio/market/twstock/ratios/<stock_id> import pandas as pddata = requests.get(f"{BASE_URL}/studio/market/twstock/ratios/2330", headers=headers, params={"start": "2024-01-01"}, timeout=30).json()["data"]# [..., {"date": "2026-06-30", "stock_id": "2330",# "gross_margin_q": 67.7207, "gross_margin_ttm": 64.2302, "operating_margin_q": 60.3443, "operating_margin_ttm": 56.1009,# "net_margin_q": 55.6354, "net_margin_ttm": 50.3717, "roe_q": 11.3934, "roe_ttm": 40.3342, "roa_q": 7.8372, "roa_ttm": 27.3074,# "eps_q": 27.25, "eps_ttm": 86.28, "eps_yoy_q": 77.4089, "eps_yoy_ttm": 53.2232,# "revenue_yoy_q": 36.0453, "revenue_yoy_ttm": 30.5567, "revenue_qoq_q": 12.0163, "revenue_qoq_ttm": 8.2017,# "debt_ratio": 30.9438, "current_ratio": 245.7635, "quick_ratio": 225.0114, "equity_multiplier": 1.4481,# "interest_coverage_q": 280.5515, "interest_coverage_ttm": 227.0232,# "inventory_turnover_q": 1.1767, "inventory_turnover_ttm": 4.6058, "cfo_to_net_income_q": 1.1084, "cfo_to_net_income_ttm": 1.1779,# "fcf_q": 287363030000.0, "fcf_ttm": 1143556366000.0, "fcf_per_share_q": 11.0827, "fcf_per_share_ttm": 44.1036, ...}]df = pd.DataFrame(data).assign(date=lambda d: pd.to_datetime(d["date"])).set_index("date"){"stock_id", "data": [...]}, wide format: one row per quarter, oldest first, one column per ratio. Every ratio is a float rounded to 4 decimals, or null.
| Field | Type | Description |
|---|---|---|
date | string | Quarter end (03-31 / 06-30 / 09-30 / 12-31) — not the day the filing became public |
stock_id | string | Code |
<ratio>_q | float | null | The quarter alone |
<ratio>_ttm | float | null | Trailing four quarters (the four single quarters summed); null if any of the four is missing |
debt_ratio, current_ratio, quick_ratio, equity_multiplier | float | null | Period-end balance-sheet ratios — no suffix |
<ratio> (each has both _q and _ttm):
| Group | Ratios | Unit |
|---|---|---|
| Profitability | gross_margin, operating_margin, pretax_margin, net_margin, opex_ratio, roe, roa | % (already ×100) |
| Per share | eps, revenue_per_share, fcf_per_share | TWD per share |
| Growth | revenue_yoy, revenue_qoq, operating_income_yoy, net_income_yoy, eps_yoy | % (already ×100) |
| Structure | interest_coverage | times |
| Efficiency | receivable_turnover, inventory_turnover, asset_turnover, cfo_to_net_income | times |
| Cash flow | fcf | TWD, raw amount (not thousands) |
No-suffix columns: debt_ratio, current_ratio, quick_ratio are % (already ×100 — a current ratio of 2.46× comes back as 245.7635); equity_multiplier is times.
| Status | Body | When |
|---|---|---|
| 400 | {"error": "Invalid stock_id"} | Code fails the format check |
| 404 | {"error": "No data found for stock_id: <id>"} | No financial statements for the code (ETFs such as 0050 / 00878), or no row inside start–end |
| 503 | {"error": "Data temporarily unavailable. Please retry shortly."} | Ratio store not ready — retry later, never read it as "no data" |
null (no gross margin, current ratio, etc.).date is lookahead.null, never 0.per/<stock_id>) and the monthly-revenue ratios (compute YoY / MoM from monthly_revenue/<stock_id>).GET /studio/market/twstock/batch/ratios?stock_ids=... (≤ 50 ids per call)./studio/market/twstock/dividend/<stock_id> Range filtering uses a three-tier effective date: cash_ex_date if set, else stock_ex_date, else record_date — freshly announced events without an ex date still show up.
data = requests.get(f"{BASE_URL}/studio/market/twstock/dividend/2330", headers=headers, params={"start": "2025-01-01", "end": "2025-12-31"}, timeout=30).json()["data"]# [{"record_date": "2025-03-24", "period": "113年第3季", "announce_date": "2025-03-03",# "cash_ex_date": "2025-03-18", "stock_ex_date": "", "pay_date": "2025-04-10",# "cash": 4.50002042, "stock": 0.0, "stock_ratio": 0.0}, ...]{"stock_id", "data": [...]}, one row per announcement row.
| Field | Type | Description |
|---|---|---|
record_date | string | 權利分派基準日 YYYY-MM-DD |
period | string | 股利所屬期間 — an opaque label (114年第3季, 113, 不適用, …); compare as string, do not parse into a year |
announce_date | string | 公告日; "" when unknown |
cash_ex_date | string | 除息交易日; "" when none / not decided |
stock_ex_date | string | 除權交易日; "" when none |
pay_date | string | 現金股利發放日; "" when unknown |
cash | float | Cash dividend per share (TWD, 盈餘 + 公積) |
stock | float | Stock dividend per share (元面額, 盈餘 + 公積) |
stock_ratio | float | stock / 10 — split-style adjustment ratio |
| Status | Body | When |
|---|---|---|
| 400 | {"error": "Invalid stock_id"} | Format check failed |
| 400 | {"error": "Invalid start date, expected YYYY-MM-DD"} (or end) | Malformed date |
| 404 | {"error": "No data found for stock_id: <id>"} / {"error": "No dividend records for stock_id: <id>"} | Not listed, or no dividend history at all |
| 503 | {"error": "Upstream rate limit. Please retry shortly."} | Upstream quota exhausted |
cash == 0 and stock == 0) are kept — an announced no-distribution decision.2330 full-history call ran to a date a week ahead of the request. Filter by date yourself if you need past events only.200 + [] (distinct from 404).404./studio/market/twstock/news/<stock_id> end − start ≤ 31 daysdata = requests.get(f"{BASE_URL}/studio/market/twstock/news/2330", headers=headers, params={"start": "2026-08-01", "end": "2026-08-07"}, timeout=60).json()["data"]{"stock_id", "data": [...]}, several articles per day (weekends included).
| Field | Type | Description |
|---|---|---|
date | string | Publish datetime |
stock_id | string | Code |
title | string | Headline |
source | string | Publisher |
link | string | URL |
| Status | Body | When |
|---|---|---|
| 400 | {"error": "Date range exceeds 31 days"} or a date-parse message | Range too long / malformed date |
| 503 | {"error": "Upstream rate limit. Please retry shortly."} | Upstream quota exhausted |
/studio/market/twstock/institutional/<stock_id> data = requests.get(f"{BASE_URL}/studio/market/twstock/institutional/2330", headers=headers, params={"start": "2024-01-01", "end": "2024-12-31"}, timeout=60).json()["data"]# [{"date": "2024-01-02", "stock_id": "2330", "foreign_buy": 28464159, "foreign_sell": 47404324,# "trust_buy": 5553520, "trust_sell": 269712, "dealer_self_buy": 452000, "dealer_self_sell": 366190,# "dealer_hedge_buy": 942546, "dealer_hedge_sell": 780090,# "foreign_dealer_self_buy": 0, "foreign_dealer_self_sell": 0}, ...]{"stock_id", "data": [...]}, wide format, one row per day. All values in shares (股).
| Field | Description |
|---|---|
date, stock_id | Day, code |
foreign_buy / foreign_sell | 外資 (excl. foreign dealers) |
trust_buy / trust_sell | 投信 |
dealer_self_buy / dealer_self_sell | 自營商自行買賣 |
dealer_hedge_buy / dealer_hedge_sell | 自營商避險 |
foreign_dealer_self_buy / foreign_dealer_self_sell | 外資自營商 (mostly 0) |
400 invalid stock_id, 404 no data, 503 upstream quota.
*_buy − *_sell.GET /studio/market/twmarket/institutional) groups it differently — see that section./studio/market/twstock/margin/<stock_id> import pandas as pddata = requests.get(f"{BASE_URL}/studio/market/twstock/margin/2330", headers=headers, params={"start": "2024-01-01"}, timeout=60).json()["data"]df = pd.DataFrame(data)df["margin_util"] = df["margin_balance"] / df["margin_limit"] # 融資使用率{"stock_id", "data": [...]}, one row per day. Values are lots (張), not shares — verified against TWSE MI_MARGN: for 2026-09-15 2330 returned margin_balance: 29282 and margin_limit: 6483092, matching TWSE's own 今日餘額 29,282 and 次一營業日限額 6,483,092 for that date exactly. Every row carries its own date and stock_id; when reading full history (no start / end) take every figure from the same row — margin_limit is re-published per day and changes over time.
| Field | Description |
|---|---|
margin_buy / margin_sell | 融資買進 / 賣出 |
margin_cash_repay | 融資現金償還 |
margin_balance / margin_prev_balance | 融資今日 / 前日餘額 |
margin_limit | 融資限額 — TWSE's 次一營業日限額 as published on that row's date, i.e. the cap that applies to the NEXT business day (2330 on 2026-09-15: 6,483,092 lots) |
short_buy / short_sell | 融券買進 / 賣出 |
short_cash_repay | 融券現金償還 |
short_balance / short_prev_balance | 融券今日 / 前日餘額 |
short_limit | 融券限額 |
offset_loan_short | 資券相抵 |
400 invalid stock_id, 404 no data, 503 upstream quota.
/studio/market/twstock/gov_bank/<stock_id> end − start ≤ 31 daysdata = requests.get(f"{BASE_URL}/studio/market/twstock/gov_bank/2330", headers=headers, params={"start": "2026-08-01", "end": "2026-08-07"}, timeout=60).json()["data"]{"stock_id", "data": [...]}, up to 8 rows per day (one per bank), sorted by date, bank.
| Field | Description |
|---|---|
date, stock_id | Day, code |
bank_name | Bank |
buy / sell | Shares bought / sold |
buy_amount / sell_amount | TWD bought / sold |
| Status | Body | When |
|---|---|---|
| 400 | {"error": "Date range exceeds 31 days"} or a date-parse message | Range too long / malformed date |
| 503 | {"error": "Upstream rate limit. Please retry shortly."} | Upstream quota exhausted |
/studio/market/twstock/lending/<stock_id> data = requests.get(f"{BASE_URL}/studio/market/twstock/lending/2330", headers=headers, params={"start": "2026-08-01"}, timeout=60).json()["data"]{"stock_id", "data": [...]}, several rows per day.
| Field | Description |
|---|---|
date, stock_id | Day, code |
transaction_type | 競價 / 議借 |
volume | Volume |
fee_rate | Lending fee rate |
close | Close price |
original_return_date | Original return date |
original_lending_period | Original lending period |
400 invalid stock_id, 404 no data, 503 upstream quota.
/studio/market/twstock/broker/search rows = requests.get(f"{BASE_URL}/studio/market/twstock/broker/search", headers=headers, params={"name": "松山"}, timeout=30).json()["data"]# [{"broker_id": "9217", "broker_name": "凱基-松山"}, ...]{"query": "<name>", "data": [{"broker_id", "broker_name"}]}.
| Status | Body | When |
|---|---|---|
| 400 | {"error": "name parameter required"} | Missing |
| 503 | {"error": "Upstream rate limit. Please retry shortly."} | Upstream quota exhausted |
/studio/market/twstock/broker/stock/<stock_id> Send date for one day, or start (+ end) for a range; start wins if both are sent.
rows = requests.get(f"{BASE_URL}/studio/market/twstock/broker/stock/2330", headers=headers, params={"start": "2026-08-03", "end": "2026-08-07"}, timeout=60).json()["data"]{"stock_id", "data": [...]}, one row per branch per day.
| Field | Type | Description |
|---|---|---|
date | string | Trading day |
stock_id | string | Code |
broker_id | string | Branch code (may be alphanumeric, e.g. 920A) |
broker_name | string | Branch name |
price | float | Volume-weighted average traded price |
buy / sell | int | Shares bought / sold |
| Status | Body | When |
|---|---|---|
| 400 | {"error": "Invalid stock_id"} | Format check failed |
| 400 | {"error": "Invalid date (YYYY-MM-DD)"} | Malformed date |
| 400 | {"error": "start/end range is <n> days; max 366 (inclusive) — split into smaller ranges"} | Range too long |
| 503 | {"error": "Upstream rate limit. Please retry shortly."} | Data temporarily unavailable — retry; do not read as "no trades" |
200 + [].start / end over 2026-09-06 → 2026-09-13 returned rows spanning five distinct trading days, for both broker/stock/<stock_id> and broker/trader/<trader_id>./studio/market/twstock/broker/trader/<trader_id> Same date / start / end rules as /broker/stock/<stock_id>.
/broker/searchrows = requests.get(f"{BASE_URL}/studio/market/twstock/broker/trader/9217", headers=headers, params={"date": "2026-08-07"}, timeout=60).json()["data"]{"trader_id", "data": [...]}, one row per stock per day; fields date, broker_id, broker_name, stock_id, price, buy, sell (as /broker/stock).
As /broker/stock/<stock_id>, with 400 {"error": "Invalid trader_id"} for a bad code.
/studio/market/twstock/alerts Give date, or start / end, or neither (= today, Taipei).
Range semantics: a disposal is returned when its period (start–end) overlaps the range, a notice when its announcement date is inside it. Without stock_id the range is at most 366 days inclusive — split longer whole-market ranges (one calendar year per call fits). Unlike the category default, start / end / date are strictly validated.
# disposals in force on one daydata = requests.get(f"{BASE_URL}/studio/market/twstock/alerts", headers=headers, params={"date": "2026-10-13", "kind": "disposal"}, timeout=30).json()["data"]# [..., {"date": "2026-10-12", "stock_id": "5228", "stock_name": "鈺鎧", "market": "tpex",# "kind": "disposal", "count": 2, "start": "2026-10-13", "end": "2026-10-19",# "condition": "連續3個營業日", "measure": "第一次處置", "match_minutes": 2,# "reason": "* 公布日期:115/10/12\n* 證券代號:5228\n…", "close": 59.6, "pe": 29.22},# {"date": "2026-10-12", "stock_id": "2492", "stock_name": "華新科", "market": "twse",# "kind": "disposal", "count": 8, "start": "2026-10-13", "end": "2026-10-19",# "condition": "連續三次", "measure": "第一次處置", "match_minutes": 2,# "reason": "1處置原因:該有價證券之交易,連續三個營業日達本公司「公布注意交易資訊」標準。\n2處置期間:…", "close": None, "pe": None}, ...] # 36 rows# notices announced on one daynotes = requests.get(f"{BASE_URL}/studio/market/twstock/alerts", headers=headers, params={"date": "2026-10-12", "kind": "notice"}, timeout=30).json()["data"]# [..., {"date": "2026-10-12", "stock_id": "5228", "stock_name": "鈺鎧", "market": "tpex",# "kind": "notice", "count": 15, "start": None, "end": None, "condition": "第一款、第三款、第四款",# "measure": None, "match_minutes": None,# "reason": "最近六個營業日(含當日)累積之最後成交價漲幅達32.63%(第一款)\n…", "close": 59.6, "pe": 29.22}, ...]# one stock's disposal history (no range cap with stock_id)hist = requests.get(f"{BASE_URL}/studio/market/twstock/alerts", headers=headers, params={"stock_id": "2492", "kind": "disposal", "start": "2001-01-01"}, timeout=30).json()["data"]# 10 rows: 2001-11-23~2001-11-29 收足五成款券 (match_minutes None), 2018-05-08~2018-05-21 第一次處置 (5), …,# 2026-10-13~2026-10-19 第一次處置 (2){"start", "end", "data": [...]}; start / end echo the resolved range (both equal date when date was sent). Rows sorted by date, market, kind, stock_id. One row per announcement.
| Field | Type | Description |
|---|---|---|
date | string | Announcement day YYYY-MM-DD (a disposal takes effect the next trading day) |
stock_id | string | Code — includes warrants, ETFs and convertible bonds (e.g. 037060, 45402) |
stock_name | string | null | Name as published |
market | string | twse (上市) or tpex (上櫃) |
kind | string | disposal or notice |
count | int | The exchange's cumulative count (累計 / 累計次數) |
start, end | string | null | Disposal period, both days included; null on notices |
condition | string | null | Disposal: the trigger as published (連續三次, 連續3個營業日, 最近十個營業日已有六次 …). Notice: the clauses cited (第一款、第五款); null if none |
measure | string | null | Disposal type: 第一次處置, 第二次處置, 再次處置, 人工管制撮合, 收足五成款券 …; null on notices and on early TPEx rows |
match_minutes | int | null | Disposal: N of 「約每N分鐘撮合一次」 read from the text; null when the text names none (every TWSE disposal before ~2011 — its text is blank — and non-matching measures) and on notices |
reason | string | null | Full announcement text; null where the exchange published none (every TWSE disposal before 2011-03) (disposal 處置內容, notice 注意交易資訊) |
close, pe | float | null | Close and P/E on the announcement; always null on TWSE disposals |
| Status | Body | When |
|---|---|---|
| 400 | {"error": "Invalid start date, expected YYYY-MM-DD"} (or end) | Malformed start / end |
| 400 | {"error": "start must not be after end"} | Reversed range |
| 400 | {"error": "Invalid date, expected YYYY-MM-DD"} | Malformed date |
| 400 | {"error": "use either date or start/end"} | date sent with start or end |
| 400 | {"error": "Invalid stock_id"} | Format check failed |
| 400 | {"error": "kind must be one of ['disposal', 'notice']"} / {"error": "market must be one of ['twse', 'tpex']"} | Unknown value |
| 400 | {"error": "start/end range is <n> days; max 366 (inclusive) without stock_id — split into smaller ranges"} | Range over 366 days without stock_id |
| 503 | {"error": "Data temporarily unavailable. Please retry shortly."} | A table you asked for is not servable (not refreshed for 48 h, or unreadable) — retry later, never read it as "no announcements". Only the requested kind / market tables are checked |
date is the announcement day, start the first day in force. To ask "under disposition on D", use date=D (or start=end=D), not the announcement date.start / end are the announced period; a market holiday or trading suspension inside it extends the real end, which the data does not record.match_minutes follows the rules of the day (TPEx 5 / 20 minutes → 2 from 2026-08-10) — read it per row, never assume a value./studio/market/twstock/batch/<data_type> payload = requests.get(f"{BASE_URL}/studio/market/twstock/batch/per", headers=headers, params={"stock_ids": "2330,6182", "start": "2026-08-04", "end": "2026-08-08"}, timeout=120).json()# {"data_type": "per",# "data": {"2330": [{"date": "2026-08-04", "dividend_yield": 0.95, "PER": 31.19, "PBR": 10.21, "stock_id": "2330"}, ...],# "6182": [...]},# "failed": []}{"data_type", "data": {"<stock_id>": [...rows...]}, "failed": [...]}. Rows are identical to the matching single-stock endpoint.
failed — ids whose server-side fetch failed (rate limit or upstream error); retry those.data and failed genuinely has no data.| Status | Body | When |
|---|---|---|
| 400 | {"error": "Unsupported data_type '<x>'. Supported: [...]"} | Bad data_type |
| 400 | {"error": "stock_ids parameter required"} | Missing |
| 400 | {"error": "stock_ids exceeds max 50 per request"} | Too many |
| 400 | {"error": "Invalid stock_id in list"} | A code fails the format check |
| 400 | {"error": "Invalid start date, expected YYYY-MM-DD"} | dividend only |
ratios (財務比率): each id's rows are the wide per-quarter rows of /ratios/<stock_id> ("data": {"2330": [{"date": "2026-06-30", "stock_id": "2330", "roe_q": 11.3934, ...}], "2317": [...]}). A code with no financial statements (ETFs) is absent from both data and failed (no 404); a ratio store that is not ready puts every id in failed (no 503).dividend differs from /dividend/<stock_id>: it reads stored data only (no refresh), an unknown id or a stock with no dividend history is silently absent (no 404), and an upstream quota problem shows up in failed, not as a 503.