Infoway API now covers the Korean equity market. Starting today, real-time data for KOSPI and KOSDAQ — trades, best bid/ask, OHLCV candles, and financial statements — is available on all plans, including the free trial. No separate contract, no Korea-local entity required.
You can enter your Infoway API key below to retrieve historical and real-time market data for Korean stocks:
Korean equities have been one of the harder markets to plug into for developers outside the region. KRX lists over 2,600 companies including Samsung Electronics, SK Hynix, NAVER, and Hyundai Motor, names that show up in global portfolios and quantitative strategies alike, yet the infrastructure to access their market data in a developer-friendly way has always lagged behind the US, Japan, or even Hong Kong. Institutional channels (Bloomberg, direct exchange feeds) are available but expensive and complex; lightweight API options have been thin on the ground.
This guide covers everything you need to get started: how KRX works, the market mechanics that affect how you write your code, and complete Python examples for each data type (REST and WebSocket). If you just want to make your first API call, skip to Section 3.
How KRX Works: What You Need to Know Before Writing Code
KOSPI and KOSDAQ Are Two Distinct Boards
Korea Exchange (KRX) was formed in 2005 through the merger of the Korea Stock Exchange, Korea Futures Exchange, and KOSDAQ. It runs two equity boards with meaningfully different characteristics:
| Board | Description | Listed Companies | Key Names |
|---|---|---|---|
| KOSPI | Main board; large-cap blue chips | ~940 | Samsung Electronics, SK Hynix, Hyundai Motor |
| KOSDAQ | Growth board; tech, biotech, small/mid-cap | ~1,700 | NAVER, Kakao, Celltrion |
KOSDAQ is sometimes compared to NASDAQ, same origin story of being a separate technology-focused market, but volatility on KOSDAQ is considerably higher, especially in the biotech space where clinical trial results can send stocks to their daily limits.
Some of the most widely tracked symbols:
| Symbol | Company | Board |
|---|---|---|
005930.KS | Samsung Electronics | KOSPI |
000660.KS | SK Hynix | KOSPI |
005380.KS | Hyundai Motor | KOSPI |
000270.KS | Kia | KOSPI |
005490.KS | POSCO Holdings | KOSPI |
066570.KS | LG Electronics | KOSPI |
035420.KS | NAVER | KOSPI |
035720.KS | Kakao | KOSPI |
068270.KS | Celltrion | KOSPI |
105560.KS | KB Financial Group | KOSPI |
All Korean symbols use the .KS suffix. Unlike Japan (.JP) or the US (.US), there is no suffix difference between KOSPI and KOSDAQ listings, both use .KS.
Trading Hours: Simple UTC+9, No Complications
KRX operates on Korea Standard Time (KST, UTC+9). Unlike the US, Korea does not observe daylight saving time, so the UTC offset never changes. Unlike Japan, there is no lunch break, trading runs continuously from open to close.
| Session | KST (Seoul) | UTC |
|---|---|---|
| Pre-market auction | 08:30 – 09:00 | 23:30 – 00:00 |
| Regular trading | 09:00 – 15:20 | 00:00 – 06:20 |
| Closing auction | 15:20 – 15:30 | 06:20 – 06:30 |
| After-hours | 15:40 – 18:00 | 06:40 – 09:00 |
The continuous session simplifies streaming logic considerably. You won’t see the intraday gaps in candle data that Japan’s lunch break creates, and a quiet stretch during regular hours almost certainly means a slow market rather than a disconnection.
A simple session checker:
from datetime import datetime
import pytz
KST = pytz.timezone("Asia/Seoul")
def is_krx_open(dt: datetime | None = None) -> bool:
now = (dt or datetime.now(KST)).astimezone(KST)
if now.weekday() >= 5:
return False
t = now.time()
from datetime import time
return time(9, 0) <= t < time(15, 30)The ±30% Daily Price Limit
Every stock on KRX has a hard daily price limit of ±30% from the previous close. This is more permissive than China’s A-share market (±10% on main boards, ±20% on STAR/ChiNext) but similar in concept.
In practice, ±30% is wide enough that large-cap KOSPI stocks almost never hit their limits. For KOSDAQ biotech and small-cap tech names, hitting the limit after a trial result or regulatory decision is routine. When a stock is locked at its upper limit (상한가, sang-hanga), you’ll see trades printing at the same price repeatedly with no upward movement, not because the market has lost interest, but because the exchange won’t allow the price to go higher.
If your code flags “price frozen for N minutes” as a stale data signal, you need to distinguish between a genuine data issue and a limit-locked stock.
The Short Selling Ban of 2023–2024
In November 2023, with KOSPI in a prolonged decline and evidence of illegal naked short selling by some foreign institutions, Korea’s Financial Services Commission imposed a complete ban on short selling across all listed stocks. The ban lasted over a year, with a phased reopening beginning in March 2025.
This is worth knowing for two reasons. First, it’s a reminder that KRX regulation is more interventionist than most Western markets, the ban was implemented quickly and applied to the entire market simultaneously. Second, if you’re building a strategy with short exposure to Korean stocks, you should have monitoring for regulatory-level short selling restrictions as part of your risk framework.
Accessing Korean Market Data with Infoway API
Infoway API provides four categories of data for Korean equities, all accessible with the same API key:
| Data Type | Endpoint Pattern | Notes |
|---|---|---|
| Real-time trades | GET /korea/batch_trade/{codes} | Last trade: price, volume, direction |
| Best bid/ask | GET /korea/batch_depth/{codes} | Best bid and ask with size |
| Candles (REST) | POST /korea/v2/batch_kline | 12 timeframes, up to 500 bars, pageable |
| Financial statements | GET /common/basic/financial/... | Income statement, balance sheet, ratios, dividends |
Free trial users can access all four. Register at the Infoway website to get an API key (No credit card required.) The key goes in a request header on every call:
apiKey: YOUR_API_KEY_HEREScenario 1: Monitoring the Semiconductor Pair Trade
Samsung Electronics and SK Hynix together control over 60% of global DRAM production. They’re deeply correlated but not perfectly so, supply allocation decisions, contract price cycles, and HBM (High Bandwidth Memory) exposure create divergences that traders watch closely. Tracking the intraday return spread between the two is a common starting point for Korea-focused quant strategies.
import time
import requests
API_KEY = "YOUR_API_KEY_HERE"
BASE_URL = "https://data.infoway.io"
HEADERS = {"apiKey": API_KEY}
SYMBOLS = "005930.KS,000660.KS" # Samsung, SK Hynix
def get_latest_trades() -> dict:
url = f"{BASE_URL}/korea/batch_trade/{SYMBOLS}"
resp = requests.get(url, headers=HEADERS).json()
return {item["s"]: item for item in resp["data"]}
def get_prev_close(symbol: str) -> float:
"""Fetch the two most recent daily bars and return the prior close."""
resp = requests.post(
f"{BASE_URL}/korea/v2/batch_kline",
json={"klineType": 8, "klineNum": 2, "codes": symbol},
headers={**HEADERS, "Content-Type": "application/json"}
).json()
bars = resp["data"][0]["respList"] # newest first
return float(bars[1]["c"]) # bars[1] = previous session
if __name__ == "__main__":
samsung_prev = get_prev_close("005930.KS")
skhynix_prev = get_prev_close("000660.KS")
print(f"Samsung prev close: ₩{samsung_prev:,.0f}")
print(f"SK Hynix prev close: ₩{skhynix_prev:,.0f}")
print("-" * 50)
while True:
trades = get_latest_trades()
samsung_chg = (float(trades["005930.KS"]["p"]) - samsung_prev) / samsung_prev * 100
skhynix_chg = (float(trades["000660.KS"]["p"]) - skhynix_prev) / skhynix_prev * 100
spread = samsung_chg - skhynix_chg
direction = {0: "—", 1: "↑", 2: "↓"}
print(
f"005930.KS {samsung_chg:+.2f}% {direction.get(trades['005930.KS']['td'], '?')}"
f" | "
f"000660.KS {skhynix_chg:+.2f}% {direction.get(trades['000660.KS']['td'], '?')}"
f" | spread: {spread:+.2f}%"
+ (" ⚠ DIVERGENCE" if abs(spread) > 2.0 else "")
)
time.sleep(5)Trade response fields:
| Field | Description |
|---|---|
s | Symbol (e.g., 005930.KS) |
t | Trade timestamp (Unix milliseconds, UTC+8) |
p | Last trade price (KRW) |
v | Volume (shares) |
vw | Notional value (KRW) |
td | Direction: 0 = default, 1 = buy, 2 = sell |
Scenario 2: Best Bid/Ask Snapshot
The depth endpoint returns the best bid and best ask with their respective sizes. This is useful for spread monitoring, execution cost estimation, and detecting limit-locked stocks (where bid and ask converge at the limit price).
Endpoint: GET https://data.infoway.io/korea/batch_depth/{codes}
import requests
url = "https://data.infoway.io/korea/batch_depth/005930.KS"
headers = {"apiKey": "YOUR_API_KEY_HERE"}
resp = requests.get(url, headers=headers).json()
book = resp["data"][0]
ask_px = book["a"][0][0]
ask_sz = book["a"][1][0]
bid_px = book["b"][0][0]
bid_sz = book["b"][1][0]
spread = float(ask_px) - float(bid_px)
print(f"{book['s']}")
print(f" Ask: ₩{float(ask_px):>10,.0f} x {float(ask_sz):,.0f} shares")
print(f" Bid: ₩{float(bid_px):>10,.0f} x {float(bid_sz):,.0f} shares")
print(f" Spread: ₩{spread:.2f}")Response structure:
{
"s": "005930.KS",
"t": 1773037524808,
"a": [["80400.00"], ["1724.0"]],
"b": [["80200.00"], ["3891.0"]]
}a[0] holds ask prices, a[1] holds ask sizes, matched by array index. Same structure for b (bid side). Best ask is a[0][0], best bid is b[0][0].
Scenario 3: Pulling Historical Candles
The candle endpoint supports 12 timeframes and allows backward pagination using a timestamp anchor, making it suitable for both live charting and bulk historical downloads.
Endpoint: POST https://data.infoway.io/korea/v2/batch_kline
klineType | Timeframe | klineType | Timeframe |
|---|---|---|---|
| 1 | 1-minute | 7 | 4-hour |
| 2 | 5-minute | 8 | Daily |
| 3 | 15-minute | 9 | Weekly |
| 4 | 30-minute | 10 | Monthly |
| 5 | 1-hour | 11 | Quarterly |
| 6 | 2-hour | 12 | Yearly |
Single-symbol queries support up to 500 bars per request. Multi-symbol queries (comma-separated codes, up to 100 symbols) are limited to the 2 most recent bars, useful for snapshot dashboards but not for historical downloads.
import requests
import time
API_KEY = "YOUR_API_KEY_HERE"
ENDPOINT = "https://data.infoway.io/korea/v2/batch_kline"
HEADERS = {
"apiKey": API_KEY,
"Content-Type": "application/json"
}
def fetch_candles(symbol: str, kline_type: int, count: int,
until_ts: int | None = None) -> list[dict]:
payload = {"klineType": kline_type, "klineNum": count, "codes": symbol}
if until_ts:
payload["timestamp"] = until_ts
resp = requests.post(ENDPOINT, json=payload, headers=HEADERS).json()
for item in resp.get("data", []):
if item["s"] == symbol:
return item.get("respList", [])
return []
def download_daily_history(symbol: str) -> list[dict]:
"""Walk backwards through all available daily bars."""
all_bars: list[dict] = []
cursor: int | None = None
while True:
batch = fetch_candles(symbol, kline_type=8, count=500, until_ts=cursor)
if not batch:
break
all_bars.extend(batch)
oldest = int(batch[-1]["t"])
if cursor and oldest >= cursor:
break
cursor = oldest
time.sleep(0.3)
all_bars.sort(key=lambda b: int(b["t"])) # ascending for charting
return all_bars
bars = download_daily_history("005930.KS")
print(f"Downloaded {len(bars)} daily bars for Samsung Electronics")
for b in bars[-3:]:
print(f" {b['t']} o=₩{b['o']} h=₩{b['h']} l=₩{b['l']} c=₩{b['c']} vol={b['v']} chg={b['pc']}")Candle response fields:
| Field | Description |
|---|---|
t | Bar open time (Unix seconds); results returned newest-first |
o / h / l / c | Open / High / Low / Close (KRW) |
v | Volume (shares) |
vw | Notional value (KRW) |
pc | % change vs. previous bar |
pca | Point change vs. previous bar |
Pagination note: pass
timestamponly for minute and hour candles. Daily bars and above are returned without a timestamp restriction, you’ll get the full available history in a single paginated walk.
Scenario 4: Financial Statements for Korea Discount Research
If you’re building a value screen or conducting fundamental analysis on Korean equities, Infoway’s financial statement endpoints give you structured access to income statements, balance sheets, cash flow statements, valuation ratios, and dividend history.
This is particularly useful for Korea Discount research. The classic Korea Discount trade involves finding companies with strong free cash flow, low P/B ratios, and a credible governance improvement story, exactly the inputs the Value-Up program targets.
Available financial endpoints (all use GET):
/common/basic/financial/income_statement # Revenue, operating income, net income
/common/basic/financial/balance_sheet # Assets, liabilities, equity
/common/basic/financial/cash_flow # Operating, investing, financing, FCF
/common/basic/financial/statistics # P/E, P/B, ROE, market cap (with live values)
/common/basic/financial/dividend # Dividend yield, payout ratio
/common/basic/financial/earnings # EPS and revenue actuals vs. consensusHere’s a practical example: pull P/B, ROE, and free cash flow for a basket of KOSPI names and flag candidates that fit the classic Korea Discount value profile (low P/B, decent ROE, real cash generation):
import requests
API_KEY = "YOUR_API_KEY_HERE"
HEADERS = {"apiKey": API_KEY}
BASE_URL = "https://data.infoway.io/common/basic/financial"
CANDIDATES = [
"005930.KS", # Samsung Electronics
"000660.KS", # SK Hynix
"005490.KS", # POSCO Holdings
"105560.KS", # KB Financial
"000270.KS", # Kia
]
def get_metric(symbol: str, endpoint: str, item_id: str,
period_type: str = "fq") -> float | None:
"""Fetch a single financial metric for a symbol."""
resp = requests.get(
f"{BASE_URL}/{endpoint}",
params={"symbol": symbol, "type": "STOCK_KR", "period_type": period_type},
headers=HEADERS
).json()
for item in resp.get("data", []):
if item.get("itemId") == item_id:
return item.get("currentValue") or item.get("itemValue")
return None
print(f"{'Symbol':<14} {'P/B':>6} {'ROE %':>8} {'FCF (B KRW)':>14}")
print("-" * 45)
for sym in CANDIDATES:
pb = get_metric(sym, "statistics", "price_book")
roe = get_metric(sym, "statistics", "roe")
fcf = get_metric(sym, "cash_flow", "free_cash_flow")
pb_str = f"{pb:.2f}x" if pb is not None else "N/A"
roe_str = f"{roe:.1f}%" if roe is not None else "N/A"
fcf_str = f"{fcf/1e9:,.1f}" if fcf is not None else "N/A"
flag = " ★" if (pb and pb < 1.5 and roe and roe > 10) else ""
print(f"{sym:<14} {pb_str:>6} {roe_str:>8} {fcf_str:>14}{flag}")The statistics endpoint returns a currentValue field for valuation ratios, this reflects the live P/E and P/B based on the current market price, not just the last reported period value. That matters for real-time dashboards where stale valuation multiples would be misleading.
Common financial metric IDs (itemId):
| ID | Metric | Endpoint |
|---|---|---|
net_income | Net income | income_statement |
oper_income | Operating income | income_statement |
revenue | Total revenue | income_statement |
free_cash_flow | Free cash flow | cash_flow |
total_assets | Total assets | balance_sheet |
total_equity | Total equity | balance_sheet |
price_earnings | P/E ratio | statistics |
price_book | P/B ratio | statistics |
roe | Return on equity | statistics |
dividend_yield | Dividend yield | dividend |
WebSocket: Real-Time Streaming with Auto-Reconnect
For applications that need sub-second latency, like live dashboards, execution monitoring, event-driven alerts, WebSocket is the right transport. The server pushes updates as they happen; you don’t poll.
Korea equity WebSocket endpoint:
wss://data.infoway.io/ws?business=korea&apikey=YOUR_API_KEY_HEREThe client below subscribes to trades, best bid/ask, and 1-minute candles for three symbols. It handles reconnection with exponential backoff and sends heartbeats every 30 seconds to keep the connection alive.
import asyncio
import json
import logging
import os
import uuid
from typing import Optional
import websockets
from websockets.exceptions import ConnectionClosed
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("krx-stream")
# --- Protocol codes ----------------------------------------------------------
REQ_TRADE = 10000 # Subscribe: real-time trades
REQ_DEPTH = 10003 # Subscribe: best bid/ask
REQ_KLINE = 10006 # Subscribe: candles
REQ_HEARTBEAT = 10010
PUSH_TRADE = 10002
PUSH_DEPTH = 10005
PUSH_KLINE = 10008
ACK_CODES = {10001, 10004, 10007}
SYMBOLS = "005930.KS,000660.KS,035420.KS" # Samsung, SK Hynix, NAVER
class KRXStreamClient:
"""Real-time Korea equity WebSocket client with reconnect and heartbeat."""
def __init__(self, api_key: str):
self.url = f"wss://data.infoway.io/ws?business=korea&apikey={api_key}"
self.ws: Optional[websockets.WebSocketClientProtocol] = None
self.running = True
self._heartbeat: Optional[asyncio.Task] = None
@staticmethod
def _trace() -> str:
return str(uuid.uuid4())
async def _send(self, msg: dict) -> None:
if self.ws:
await self.ws.send(json.dumps(msg))
async def _subscribe(self) -> None:
await self._send({"code": REQ_TRADE, "trace": self._trace(),
"data": {"codes": SYMBOLS}})
await self._send({"code": REQ_DEPTH, "trace": self._trace(),
"data": {"codes": SYMBOLS}})
await self._send({"code": REQ_KLINE, "trace": self._trace(),
"data": {"arr": [{"type": 1, "codes": SYMBOLS}]}})
logger.info("Subscribed to trades, depth, and 1m candles for: %s", SYMBOLS)
def _start_heartbeat(self) -> None:
self._stop_heartbeat()
async def beat():
try:
while True:
await asyncio.sleep(30)
if not self.ws or self.ws.close_code is not None:
break
await self._send({"code": REQ_HEARTBEAT, "trace": self._trace()})
except (ConnectionClosed, asyncio.CancelledError):
pass
self._heartbeat = asyncio.create_task(beat())
def _stop_heartbeat(self) -> None:
if self._heartbeat and not self._heartbeat.done():
self._heartbeat.cancel()
self._heartbeat = None
def _dispatch(self, raw: str) -> None:
try:
msg = json.loads(raw)
except json.JSONDecodeError:
return
code = msg.get("code")
data = msg.get("data", {})
if code == PUSH_TRADE:
direction = {0: "—", 1: "BUY ↑", 2: "SELL ↓"}.get(data.get("td", 0), "?")
logger.info("TRADE %-14s ₩%-10s vol=%-10s %s",
data.get("s"), data.get("p"), data.get("v"), direction)
elif code == PUSH_DEPTH:
best_ask = data.get("a", [[None]])[0][0]
best_bid = data.get("b", [[None]])[0][0]
if best_ask and best_bid:
spread = float(best_ask) - float(best_bid)
logger.info("DEPTH %-14s bid=₩%-10s ask=₩%-10s spread=₩%.0f",
data.get("s"), best_bid, best_ask, spread)
elif code == PUSH_KLINE:
logger.info("CANDLE %-14s o=%-8s h=%-8s l=%-8s c=%-8s %s",
data.get("s"), data.get("o"), data.get("h"),
data.get("l"), data.get("c"), data.get("pfr"))
elif code in ACK_CODES:
logger.info("Subscription confirmed (code=%s)", code)
elif code == 200:
logger.info("Connection established")
async def _connect_once(self) -> None:
async with websockets.connect(self.url) as ws:
self.ws = ws
await self._subscribe()
self._start_heartbeat()
try:
async for message in ws:
self._dispatch(message)
finally:
self._stop_heartbeat()
self.ws = None
async def start(self) -> None:
backoff = 5
while self.running:
try:
await self._connect_once()
backoff = 5
logger.warning("Connection closed by server")
except ConnectionClosed as e:
logger.warning("Connection closed: %s", e)
backoff = 5
except Exception as e:
logger.error("Connection error: %s", e)
if not self.running:
break
logger.info("Reconnecting in %ss...", backoff)
await asyncio.sleep(backoff)
backoff = min(backoff * 2, 60)
def stop(self) -> None:
self.running = False
async def main():
api_key = os.environ.get("INFOWAY_API_KEY", "YOUR_API_KEY_HERE")
client = KRXStreamClient(api_key=api_key)
await client.start()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Stopped")Install the dependency and run:
pip install websockets
export INFOWAY_API_KEY="your_key_here"
python krx_stream.pyWebSocket Protocol Reference
| Code | Direction | Meaning |
|---|---|---|
| 10000 | → Server | Subscribe to trades |
| 10002 | ← Server | Trade push: s, p, v, vw, t, td |
| 10003 | → Server | Subscribe to best bid/ask |
| 10005 | ← Server | Depth push: s, t, a[prices, sizes], b[prices, sizes] |
| 10006 | → Server | Subscribe to candles (send arr: [{type, codes}]) |
| 10008 | ← Server | Candle push: s, o, h, l, c, v, vw, t, ty, pca, pfr |
| 10010 | ↔ Both | Heartbeat, send every 30s; server drops connections silent for 60s |
| 11000 | → Server | Unsubscribe trades |
| 11001 | → Server | Unsubscribe depth |
| 11002 | → Server | Unsubscribe candles |
Important: Infoway’s WebSocket is stateless. If you disconnect and reconnect, your subscriptions do not carry over, you need to re-send all subscription requests on every new connection. The client above handles this in
_connect_once, which calls_subscribe()immediately after the connection is established.
Subscription Quotas by Plan
| Plan | Max symbols per connection |
|---|---|
| Free trial | 10 |
| Basic ($99/mo) | 200 |
| Pro ($199/mo) | 800 |
| Enterprise ($399/mo) | 5,000 |
The free plan’s 10-symbol limit is enough to stream the semiconductor pair (Samsung + SK Hynix) plus a handful of other names, which covers most ad-hoc research workflows.
Frequently Asked Questions
What symbol format do Korean stocks use?
Six-digit numeric ticker followed by .KS, for example, 005930.KS for Samsung Electronics. Both KOSPI and KOSDAQ listings use the .KS suffix, so there’s no suffix to indicate which board a stock is on. You can download the full symbol list from the Infoway dashboard after registering, or query it via the product list API.
Can I use the free trial for Korean data?
Yes. All four data types are accessible on the free trial. The free tier allows up to 10 symbols per WebSocket connection and applies rate limits on REST requests; the paid plans expand both.
How does the ±30% daily limit affect my data?
When a stock hits its upper or lower daily limit, trades continue printing at the limit price, but the price won’t move beyond it. If you’re using the pc (percent change) field from the candle response to detect limit situations, the threshold is ±30%, not the ±10% you’d use for China A-shares. Comparing float(current_price) against prev_close * 1.30 is the straightforward detection approach.
Does the financial data cover all KOSPI and KOSDAQ listings?
Coverage focuses on major listed companies, the universe covers the names where fundamental data is available and regularly updated. Samsung, SK Hynix, NAVER, Hyundai, and similar blue-chip names are fully covered. Very small KOSDAQ listings may have limited financial data availability.
How do I page backwards through candle history?
Take the t value (Unix seconds) from the oldest bar in your current response, subtract 1, and pass it as the timestamp parameter in your next request. The API returns candles older than that timestamp. Repeat until you get an empty response or until you’ve reached your desired lookback window. This parameter applies to minute and hour candles; daily bars and above return without timestamp restriction.
Does reconnecting the WebSocket automatically restore my subscriptions?
No. The server has no session memory, each new connection starts fresh. You need to re-send your subscription messages after every reconnect. The example client above does this by calling _subscribe() at the top of _connect_once(), so every new connection (including reconnections after a drop) sends fresh subscription requests before the message loop starts.
Infoway API documentation is available HERE. Register for a free API key to get started, no credit card required. For technical support, reach the team via the official Telegram or send us an email.