opendart-api
OpenDART API Skill
This skill helps you build Python scripts, bots, and analysis pipelines that consume the OpenDART (Digital Analysis & Retrieval of Transcripts) API operated by Korea's Financial Supervisory Service (FSS).
OpenDART provides electronic disclosure data for Korean public companies (including KOSPI, KOSDAQ, and KONEX listed firms). The English API base URL is:
https://engopendart.fss.or.kr/engapi/
All endpoints require a 40-character API authentication key (crtfc_key).
1. Installation
Via skills.sh (Recommended)
Install this skill into your AI agent with one command:
npx skills add dwk601/opendart-api-skill
This makes the skill available to Claude Code, Cursor, Codex, Gemini CLI, GitHub Copilot, OpenCode, and 50+ other agents that support the skills ecosystem.
Discover more: https://skills.sh/dwk601/opendart-api-skill/opendart-api
Manual Install
git clone https://github.com/dwk601/opendart-api-skill.git
cd opendart-api-skill
pip install aiohttp pandas
Then import the bundled client from scripts/opendart_client.py.
2. Authentication
Getting an API Key
- Register at https://engopendart.fss.or.kr
- Navigate to Request an Authentication Key
- The issued key is a 40-character string
Best Practice: Use Environment Variables
import os
API_KEY = os.environ.get("OPENDART_API_KEY")
if not API_KEY:
raise ValueError("Set OPENDART_API_KEY environment variable")
3. Core 3-Step Workflow
Almost every OpenDART workflow follows this pattern:
Step 1: Download Corporation Codes
OpenDART uses an 8-digit corp_code (not stock ticker). Download the master
mapping once and cache it.
from opendart_client import OpenDartClient
import asyncio
async def main():
client = OpenDartClient(api_key="YOUR_KEY")
df_corps = await client.get_corp_codes()
# Cache to avoid re-downloading
df_corps.to_parquet("corp_codes.parquet")
asyncio.run(main())
Step 2: Resolve corp_code
# Find Samsung Electronics by stock code or name
samsung = df_corps[df_corps["stock_code"] == "005930"]
corp_code = samsung.iloc[0]["corp_code"] # e.g., "00126380"
Step 3: Call the Target Endpoint
fin = await client.get_financials_single(
corp_code=corp_code,
bsns_year="2023",
reprt_code="11011" # Annual report
)
print(fin.head())
4. Async-First Patterns
OpenDART allows ~20,000 requests/day. For financial analysis pipelines that touch hundreds of companies, asyncio + aiohttp is essential.
Basic Async Session
import asyncio
from opendart_client import OpenDartClient
async def fetch_many():
client = OpenDartClient(api_key="YOUR_KEY", max_concurrent=10)
async with client:
results = await asyncio.gather(
client.get_financials_single("00126380", "2023", "11011"),
client.get_financials_single("00126380", "2022", "11011"),
client.get_financials_single("00126380", "2021", "11011"),
)
return results
Semaphore-Controlled Concurrency
The bundled client uses asyncio.Semaphore(max_concurrent) to stay within
rate limits. Default is 10 concurrent requests.
client = OpenDartClient(api_key="YOUR_KEY", max_concurrent=20)
Sequential with Backoff
For polling bots, sequential requests with asyncio.sleep() are simpler and
safer:
async def poll_loop(client, interval=86400):
while True:
filings = await client.search_filings(bgn_de="20240101")
await process(filings)
await asyncio.sleep(interval)
5. Bot Patterns for Financial Analysis
Pattern A: Polling Bot (New Filings)
Checks for new disclosures since a date and stores results.
import asyncio
from datetime import datetime, timedelta
from opendart_client import OpenDartClient
async def polling_bot(api_key, check_interval=86400):
client = OpenDartClient(api_key=api_key)
async with client:
since = (datetime.now() - timedelta(days=1)).strftime("%Y%m%d")
while True:
filings = await client.search_filings(
bgn_de=since,
pblntf_ty="A", # Periodic disclosures
page_count=100
)
if not filings.empty:
filings.to_parquet(f"filings_{since}.parquet")
since = filings["rcept_dt"].max()
await asyncio.sleep(check_interval)
# asyncio.run(polling_bot("YOUR_KEY"))
Pattern B: Batch Financial Pipeline
Downloads financials for many companies/years with controlled concurrency.
import asyncio
import pandas as pd
from opendart_client import OpenDartClient
async def batch_pipeline(api_key, corp_codes, years, reprt_code="11011"):
client = OpenDartClient(api_key=api_key, max_concurrent=10)
all_data = []
async with client:
tasks = [
client.get_financials_single(corp_code, year, reprt_code)
for corp_code in corp_codes
for year in years
]
results = await asyncio.gather(*tasks, return_exceptions=True)
for df in results:
if isinstance(df, Exception):
print(f"Error: {df}")
continue
if not df.empty:
all_data.append(df)
return pd.concat(all_data, ignore_index=True) if all_data else pd.DataFrame()
Pattern C: Multi-Company Monitor
Watch specific companies for new filings.
async def monitor_companies(api_key, corp_codes, callback, interval=3600):
client = OpenDartClient(api_key=api_key)
seen = set()
async with client:
while True:
tasks = [client.search_filings(corp_code=c) for c in corp_codes]
results = await asyncio.gather(*tasks)
for corp_code, filings in zip(corp_codes, results):
if filings.empty:
continue
for _, row in filings.iterrows():
key = row["rcept_no"]
if key not in seen:
seen.add(key)
await callback(corp_code, row)
await asyncio.sleep(interval)
6. DataFrame Output Conventions
The bundled client normalizes API responses into tidy pandas DataFrames.
List Endpoints (e.g., search_filings)
The list array in the JSON becomes a DataFrame with one row per item.
Metadata (page_no, total_count) is stored as DataFrame attributes:
filings = await client.search_filings(corp_code="00126380")
print(filings.attrs.get("total_count"))
Single-Row Endpoints (e.g., get_corp_info)
Returns a pd.Series for easy field access:
info = await client.get_corp_info("00126380")
print(info["corp_name"], info["ceo_nm"])
Financial Statements
get_financials_all returns a DataFrame where each row is an account item:
| account_nm | fs_div | sj_div | thstrm_nm | thstrm_amount | ... |
|---|---|---|---|---|---|
| Revenue | CFS | IS | Current Term | 3022314 | ... |
7. Pagination & Rate Limits
| Limit | Value |
|---|---|
| Daily requests | ~20,000 |
| Results per page | Max 100 |
| Companies per multi request | Max 100 |
Auto-Pagination
The client handles pagination automatically. If you call search_filings with
defaults, the client loops through all pages and concatenates them into a
single DataFrame.
To fetch a specific page manually:
page_1 = await client.search_filings(
corp_code="00126380", page_no=1, page_count=100
)
Rate Limit Tuning
If you hit 020 (call limit exceeded), reduce max_concurrent or add
request_delay:
client = OpenDartClient(
api_key="YOUR_KEY",
max_concurrent=5,
request_delay=0.2 # seconds between requests
)
8. Error Handling
Retry Strategy
The client retries automatically on transient errors with exponential backoff:
- Retry:
020(rate limit),800(maintenance), network timeouts - No retry:
010(invalid key),013(no data),021(too many companies)
Common Errors in Financial Analysis Bots
| Error | Cause | Fix |
|---|---|---|
013 No data viewed |
Company has no filing for that year/report | Skip or try adjacent year |
021 Exceeded company limit |
Multi request has >100 corp_codes | Batch into chunks of 100 |
100 Field value invalid |
reprt_code or bsns_year wrong |
Check references/disclosure-types.md |
020 Call limit exceeded |
Too many requests | Reduce concurrency, add delay |
Graceful Degradation
Always use return_exceptions=True in asyncio.gather for batch pipelines:
results = await asyncio.gather(*tasks, return_exceptions=True)
for r in results:
if isinstance(r, Exception):
log_error(r)
continue
process(r)
9. Quick Reference: Most-Used Endpoints
| Task | Method | Key Params |
|---|---|---|
| Search disclosures | search_filings |
bgn_de, end_de, corp_code, pblntf_ty |
| Company overview | get_corp_info |
corp_code |
| Single company financials | get_financials_single |
corp_code, bsns_year, reprt_code |
| Multi-company comparison | get_financials_multi |
corp_code (comma-separated up to 100), bsns_year, reprt_code |
| Full financial statements | get_financials_all |
corp_code, bsns_year, reprt_code, fs_div (CFS/OFS) |
| Major shareholders | get_major_shareholders |
corp_code, bsns_year, reprt_code |
| Executives | get_executives |
corp_code, bsns_year, reprt_code |
| Dividends | get_dividends |
corp_code, bsns_year, reprt_code |
| Download document | download_document |
rcept_no, save path |
Report Type Codes (reprt_code)
| Code | Report |
|---|---|
11013 |
1st Quarter Report |
11012 |
Semi-annual Report |
11014 |
3rd Quarter Report |
11011 |
Annual Report (Business Report) |
Financial Statement Division (fs_div)
| Code | Meaning |
|---|---|
CFS |
Consolidated Financial Statements (연결재무제표) |
OFS |
Separate Financial Statements (재무제표) |
10. Bundled Resources
scripts/opendart_client.py
A full runnable async client. Import and use directly:
from opendart_client import OpenDartClient
references/endpoints.md
Complete catalog of all API endpoints with parameters and response structures. Read this when you need details on a specific endpoint.
references/error-codes.md
Full error code table with handling recommendations.
references/disclosure-types.md
Lookup tables for pblntf_ty, pblntf_detail_ty, corp_cls, reprt_code, etc.
11. Example: Complete Financial Analysis Script
import asyncio
import os
import pandas as pd
from opendart_client import OpenDartClient
async def analyze_kospi_financials():
api_key = os.environ["OPENDART_API_KEY"]
client = OpenDartClient(api_key=api_key, max_concurrent=10)
async with client:
# 1. Load or download corp codes
try:
df_corps = pd.read_parquet("corp_codes.parquet")
except FileNotFoundError:
df_corps = await client.get_corp_codes()
df_corps.to_parquet("corp_codes.parquet")
# 2. Filter to KOSPI companies (corp_cls == 'Y')
kospi = df_corps[df_corps["corp_cls"] == "Y"].head(20)
codes = kospi["corp_code"].tolist()
# 3. Batch fetch 2023 annual financials
tasks = [
client.get_financials_single(c, "2023", "11011")
for c in codes
]
results = await asyncio.gather(*tasks, return_exceptions=True)
# 4. Combine into analysis DataFrame
dfs = [r for r in results if isinstance(r, pd.DataFrame) and not r.empty]
if dfs:
combined = pd.concat(dfs, ignore_index=True)
combined.to_parquet("kospi_2023_financials.parquet")
print(f"Fetched {len(combined)} rows for {len(dfs)} companies")
else:
print("No data fetched")
if __name__ == "__main__":
asyncio.run(analyze_kospi_financials())
12. Tips for Financial Analysis Bots
- Cache corp codes: The ZIP file is large. Download once per week.
- Chunk multi requests:
get_financials_multiaccepts up to 100 codes. - Handle missing data: Not all companies file every report type.
- Use CFS for listed companies: Consolidated statements are standard for KOSPI/KOSDAQ.
- Store raw responses: Save parquet files before transformation for reproducibility.
- Monitor API usage: The client logs requests; track against the 20K/day limit.
When the user asks for OpenDART code, always default to the async client
pattern, pandas DataFrames, and the async with context manager. Point them
to the bundled scripts/opendart_client.py for the full implementation.