EDINET API の使い方: API キーの取得から Python で有報と財務データ (XBRL) を取るまで
EDINET API v2 で有価証券報告書を取得し、XBRL から売上高や営業利益を取り出すまでを、動くコードつきで解説します。キーが無効でも 200 が返る罠、単体決算・IFRS・銀行の「経常収益」など、8,480 本を取り込んで分かった XBRL の癖も。
本記事にはプロモーション (アフィリエイト広告) が含まれます。
「EDINET API の使い方」「Python で財務データを取りたい」で調べている人向けに、先に結論を置きます。
EDINET API は無料で、API キーを 1 つ取れば、上場会社の有価証券報告書を XBRL ごと取得できます。 呼び方は「日付を指定して書類一覧を取る」→「書類 ID で ZIP を取る」の 2 段です。売上高や営業利益を 1 社分出すだけなら、Python 80 行ほどで動きます。記事の 5 節に、そのままコピーして動くコードを置いています。
難しいのは取得ではなく、XBRL の読み方です。私は自宅のサーバーで毎晩 EDINET から有報を取り込んでいて、2025 年 1 月以降の分で ZIP 8,480 本、財務の数値 610 万行になりました。その間に踏んだ罠を「つまずいた点」にまとめています。キーが無効でもエラーにならない、単体決算の会社は何も取れない、銀行の「経常収益」を利益と取り違える、の 3 つは、最初に知っておくと同じところで止まらずに済みます。
この記事は、決算データを自動で集めて AI に読ませる「投資判断システム」を自宅サーバーで作る話の 1 本目です。
EDINET API とは
EDINET は、金融庁が運営する「金融商品取引法に基づく開示書類」の電子開示システムです。有価証券報告書 (有報)、半期報告書、大量保有報告書などが、提出された日にここに載ります。
EDINET API は、その書類をプログラムから取るための API です。今の版は v2 で、利用には API キーが要ります。料金はかかりません。
似たものに TDnet (東証の適時開示) と J-Quants (JPX の株価・財務データの API) があります。決算短信は TDnet、有報は EDINET、と覚えておくと混乱しません。
この記事で使う環境
- データを取る側: 手元の Mac (開発) と、自宅のラズパイ (毎晩の本番バッチ)。私のバッチは Python 3.13。この記事のコードは 3.11 以上で動きます
- ライブラリ:
httpx(HTTP)、lxml(XML)。XBRL 専用のライブラリ (Arelle など) は使っていません - 取り込み先: PostgreSQL。この記事のコードは DB を使わず、画面に出すだけにしています
毎晩のバッチは、ラズパイでなくても、常時動いている Linux があれば動きます。自宅にサーバーを置けないなら VPS が選択肢です。
1. API キーを取る
API キーは、アカウントを作ってログインすると発行されます。手順は金融庁の EDINET API 仕様書 (Version 2) の「2-3 アカウントの作成と API キーの発行について」に、画面つきで載っています (2026 年 6 月版で確認)。流れはこうです。
- ブラウザのポップアップを許可する。
https://api.edinet-fsa.go.jpをポップアップ許可サイトに追加します。キーの発行画面はポップアップで開くので、これをしないと画面が出ません - アカウントを作る。EDINET の閲覧サイトで「ログイン」→「今すぐサインアップ」。メールアドレスに確認コードが届くので入力し、パスワードを決めます。パスワードは 12 文字以上で、小文字・大文字・数字・記号のうち 3 種類以上が必要です
- 多要素認証を登録する。電話番号を入れ、SMS か自動音声で本人確認します
- 連絡先を登録すると、キーが表示される。氏名と電話番号 (ハイフンなし) を入れて「連絡先登録」を押すと、同じ画面に API キーが出ます。コピーして保存します
2 回目以降にログインすると、同じ画面に今のキーと「API キー再発行」ボタンが出ます。キーが漏れたときは、ここで作り直せます。
「ログインできない」ときは、仕様書に書かれている次の 3 つを確かめてください。
- ポップアップがブロックされている。サインインは通っても、キーの画面が開きません
- 確認コードのメールが迷惑メールに入っている。送信元は
@microsoftonline.comです。EDINET のログインは Microsoft のアカウント基盤を使っているので、金融庁ではなく Microsoft の名前でメールが来ます - 多要素認証の電話番号を変えた。キーの画面の「多要素認証クリア」で登録した番号を消すと、次のサインインで登録し直せます
キーの発行画面には、登録した氏名と電話番号、それに API キーがそのまま表示されます。スクリーンショットを人に見せるときや、ブログに貼るときは気をつけてください。
キーは 1 つで、複数のマシンで使ってかまいません。私は Mac とラズパイで同じキーを使っています。
キーはリクエストのヘッダー Ocp-Apim-Subscription-Key に入れて送ります。
HEADERS = {"Ocp-Apim-Subscription-Key": API_KEY}
URL のクエリに Subscription-Key=... と付けても通ります。しかし、それをやると URL ごとログに残ります。私のバッチは httpx のログを cron の出力に流しているので、クエリに載せるとキーがログファイルに平文で残ってしまいます。ヘッダーで送ってください。
2. 書類一覧を取る
ベースの URL は https://api.edinet-fsa.go.jp/api/v2 です。まず、ある日に提出された書類の一覧を取ります。
GET /documents.json?date=2025-06-27&type=2
type=2 で、書類一覧とメタデータの両方が返ります。一覧は提出日単位でしか引けません。「2025 年の有報を全部」のような指定はできないので、期間が欲しいときは 1 日ずつ回します。土日祝も呼べて、提出がなければ空の配列が返ります。
一覧の 1 件には、書類の種類や会社の情報が入っています。有報を拾うための条件はこうです。
| 項目 | 条件 | 意味 |
|---|---|---|
docTypeCode |
120 (有報) か 130 (訂正有報) |
半期報告書は 160 / 170 |
secCode |
空でない | 証券コード。5 桁で末尾が 0 (13010 → 1301、130A0 → 130A) |
xbrlFlag |
"1" |
XBRL がある。文字列の "1" / "0" |
withdrawalStatus |
"0" |
取り下げられていない |
提出日時の submitDateTime は "2025-06-27 15:00" の形で、秒もタイムゾーンもありません。
実際の件数の目安です (一覧の全件 / 有報 / 有報かつ証券コードあり)。
| 日 | 全書類 | 有報 | 上場会社の有報 |
|---|---|---|---|
| 2025-06-25 (1 年で一番多い日) | 1,807 | 489 | 414 |
| 2025-09-10 (閑散期) | 317 | 43 | 1 |
3 月決算の会社が多いので、有報は 6 月下旬に集中します。1 年分 (2025-04〜2026-03) の上場会社の有報を月別に数えると、6 月が 2,284 件、3 月が 543 件、ほかの月は 44〜221 件でした。
3. 書類を ZIP で取る
一覧の docID で、書類本体を取ります。
GET /documents/S100W7QE?type=1
type=1 で、XBRL 一式の ZIP が返ります。1 本の大きさは平均 0.8MB、最大で 5.6MB でした。取得には 1 本 2〜3 秒かかります。
ZIP の中身は、ある有報 (圧縮 690KB、展開すると 9.5MB、59 ファイル) でこうなっていました。
XBRL/PublicDoc/
├── jpcrp030000-asr-001_XXXXX-000_2025-03-31_01_2025-06-27.xbrl インスタンス。数値の本体 (4.0MB)
├── *_pre.xml 表示リンク。勘定科目の並び順と階層
├── *_lab.xml ラベル (会社が独自に足した科目の名前)
├── *_cal.xml 計算リンク (足し算の関係)
├── *_def.xml 定義リンク
├── *.xsd スキーマ
├── 0101010_honbun_*_ixbrl.htm 本文 (企業の概況)
├── 0102010_honbun_*_ixbrl.htm 本文 (事業の状況、事業等のリスク)
└── 0104010_honbun_*_ixbrl.htm 本文 (株式と大株主)
XBRL/AuditDoc/ 監査報告書。財務を読むときは見ない
fuzoku/*.gif 図
数字が欲しいなら .xbrl のインスタンス、文章が欲しいなら honbun の HTML です。
4. XBRL から売上高と営業利益を取り出す
XBRL は、1 つの数値の情報が 3 か所に分かれています。
- 値: インスタンスの要素の中身 (
<jppfs_cor:NetSales ...>4035492000</...>) - いつの、何の値か: 要素の
contextRef属性 (CurrentYearDuration= 当期・連結全体) - 表のどこに並ぶか: 表示リンク (
_pre.xml)
売上高や営業利益を 1 社分出すだけなら、インスタンスだけで足ります。要素の名前 (ローカル名) と contextRef で拾います。
# 当期・連結全体の売上高
if local == "NetSales" and el.get("contextRef") == "CurrentYearDuration":
sales = el.text
要素の名前空間の URI で引かないのがコツです。URI にはタクソノミの年度が入っていて (.../jppfs/2024-11-01/... のように)、年度が変わると固定の URI では外れます。ローカル名 (NetSales) で拾えば、年度をまたいでも動きます。
5. 動くコード
ここまでをまとめた、最小のコードです。指定した日に提出された有報の 1 件目を取って、売上高・営業利益・経常利益・純利益を出します。
# /// script
# requires-python = ">=3.11"
# dependencies = ["httpx", "lxml"]
# ///
"""EDINET API v2 で、ある日に提出された有報を 1 件落として売上高と営業利益を出す."""
import io
import os
import sys
import time
import zipfile
import httpx
from lxml import etree
BASE = "https://api.edinet-fsa.go.jp/api/v2"
API_KEY = os.environ["EDINET_API_KEY"]
HEADERS = {"Ocp-Apim-Subscription-Key": API_KEY} # クエリに載せるとログに残る
# 取り出したい要素 (ローカル名) とラベル。日本基準の連結 PL
TARGETS = {
"NetSales": "売上高",
"OperatingIncome": "営業利益",
"OrdinaryIncome": "経常利益",
"ProfitLossAttributableToOwnersOfParent": "親会社株主に帰属する当期純利益",
}
def list_documents(day: str) -> list[dict]:
r = httpx.get(f"{BASE}/documents.json", params={"date": day, "type": 2},
headers=HEADERS, timeout=30)
r.raise_for_status()
body = r.json()
# エラーでも HTTP 200 で返ってくる。本文を見る
if "StatusCode" in body:
sys.exit(f"API エラー: {body['StatusCode']} {body.get('message')}")
if body["metadata"]["status"] != "200":
sys.exit(f"API エラー: {body['metadata']}")
return body["results"] or []
def download_xbrl_zip(doc_id: str) -> bytes:
r = httpx.get(f"{BASE}/documents/{doc_id}", params={"type": 1},
headers=HEADERS, timeout=120)
r.raise_for_status()
return r.content
def read_facts(zip_bytes: bytes) -> dict[str, str]:
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as z:
# 本文の XBRL は XBRL/PublicDoc/ の下に 1 本。AuditDoc は監査報告書
name = next(n for n in z.namelist()
if n.startswith("XBRL/PublicDoc/") and n.endswith(".xbrl"))
root = etree.parse(z.open(name)).getroot()
found = {}
for el in root.iter():
if not isinstance(el.tag, str):
continue
local = etree.QName(el).localname
# CurrentYearDuration が当期・連結全体。_ が付くとセグメント別や単体になる
if local in TARGETS and el.get("contextRef") == "CurrentYearDuration":
found[TARGETS[local]] = el.text
return found
def main() -> None:
day = sys.argv[1] if len(sys.argv) > 1 else "2025-06-27"
docs = [d for d in list_documents(day)
if d["docTypeCode"] == "120" and d["secCode"] and d["xbrlFlag"] == "1"]
print(f"{day}: 上場会社の有報 {len(docs)} 件")
if not docs:
return
doc = docs[0]
time.sleep(1) # 連続で叩かない
facts = read_facts(download_xbrl_zip(doc["docID"]))
print(doc["docID"], doc["secCode"], doc["filerName"], doc["periodEnd"])
for label, value in facts.items():
print(f" {label}: {int(value):,} 円")
if __name__ == "__main__":
main()
先頭のコメント (# /// script) は、依存ライブラリをファイルの中に書く書式 (PEP 723) です。uv があれば、インストールなしで動きます。
EDINET_API_KEY=あなたのキー uv run edinet_min.py 2025-06-27
pip なら pip install httpx lxml してから python edinet_min.py 2025-06-27 です。
2025-06-27: 上場会社の有報 286 件
S100W7QE 17110 株式会社SDSホールディングス 2025-03-31
売上高: 4,035,492,000 円
営業利益: -14,691,000 円
経常利益: -97,208,000 円
親会社株主に帰属する当期純利益: -151,714,000 円
このコードが読めるのは、日本基準で連結決算をしている会社だけ です。IFRS の会社は要素名が違い (jpigp_cor_RevenueIFRS など)、単体決算の会社は contextRef が CurrentYearDuration_NonConsolidatedMember になるので、何も出ません。次の「つまずいた点」に、それぞれの直し方を書いています。
6. 毎晩取り込むときの工夫
1 社を取るだけなら上のコードで十分です。毎晩のバッチにするときは、次の点を入れています。
- リクエストの間に 0.25 秒空ける。公式のレート制限の値は見つけられませんでした。叩きすぎないための自衛です
- ZIP は一時ファイルに書いてから名前を変える。
.zip.partに書き、完了したら.zipにします。途中で落ちても、壊れた ZIP が残りません - 5 件続けて失敗したら止まる。ネットワークや API の障害で、全件失敗しながら走り切るのを防ぎます
- 最後に取り込んだ日をもう一度引く。同じ日に後から出た書類を拾うためです。書類 ID で upsert するので、同じ日を 2 回回しても壊れません
- 「取得済みの日」を記録しない。失敗した日を「済み」と書いてしまう事故の方が、空振りの一覧取得 (1 回 0.4 秒) より高くつきます。まだ落としていない書類を毎回拾い直す形にしています
閑散期の夜間バッチは、ラズパイで一覧の取得が 3〜4 秒、解析が 1〜2 秒です。6 月のピークは実測できていませんが、1 本 2.5 秒と待ち 0.25 秒から計算すると、400 件で 18 分ほどです。
つまずいた点
取り込みを続ける中で踏んだものです。上から順に、知らないと時間を失う順に並べました。
- キーが無効でも HTTP 200 が返る。キーが間違っていると、HTTP のステータスは 200 のまま、本文のトップレベルに
"StatusCode": 401が入ります。raise_for_status()では気づけず、「対象 0 件」で静かに正常終了します。上のコードのlist_documentsで本文を見ているのはこのためです - 単体決算の会社が 1 項目も取れなかった。連結子会社のない会社は、値に
NonConsolidatedMemberが付きます。「member なし」の条件で拾っていたので、765 書類から 1 つも取れていませんでした。DEI (書類の基本情報) のWhetherConsolidatedFinancialStatementsArePreparedDEIで連結か単体かを見て、contextRefを切り替えます - 銀行の「経常収益」を経常利益に入れかけた。
jpcrp_cor_OrdinaryIncomeSummaryOfBusinessResultsは、名前に Income と付いていますが、ラベルは「経常収益」です。銀行や保険の「売上」にあたります。これを経常利益に寄せると、みずほ FG の 9.0 兆円が利益の列に入ります。該当は 1,073 件で、すべて銀行と金融持株会社でした。保険のjppfs_cor_OperatingIncomeINSも同じ罠です - 売上高の要素名がばらばら。日本基準は
NetSales、IFRS はjpigp_cor_RevenueIFRS、鉄道はOperatingRevenueRWYです。業種ごとの派生は 20 以上 (RWY、HWY、ELC、SEC、INV…) ありました。売上だけは、日本語のラベルで拾う逃げ道を作りました - 値は円。有報の表は百万円で印刷されていますが、XBRL の中身は円です。前に使っていたシステムでは、列の説明に「百万円」と書いたまま、中身は円でした
- 営業利益は 5 年分取れない。「主要な経営指標等の推移」には売上高や経常利益が 5 期分載りますが、営業利益の要素がタクソノミにありません。営業利益だけ、損益計算書の当期と前期の 2 期しか取れません。IFRS には営業利益の定義がなく、そもそも開示しない会社もあります
contextRefを捨てると区別できなくなる。CurrentYearDurationの後ろに_ReportableSegmentsMemberなどが付くと、セグメント別の値です。営業利益の 7 割の書類で、セグメント別の値が同じ要素名で同居していました。取り込むときにcontextRefを落とすと、全社の値とセグメントの値を後から区別できません- 一覧に同じ書類が 2 回出ることがある。upsert が同じ行を 2 回更新しようとしてエラーになりました。書類 ID で重複を消してから入れます
- 訂正有報は差分ではなく全文。最初は「様式が違うので読めない」と決めつけて外していました。585 件を実際に試したら、全部同じコードで読めました。12.6% で数値が変わっていて、黒字から赤字に反転した会社もあります。訂正を元の有報とマージするとかえって誤るので、訂正は訂正として読みます
- REIT が 1 件も入っていなかった。「証券コードがない = 非上場」として落としていましたが、投資法人は上場していても一覧に証券コードが入りません。
ordinanceCodeが030のものがそれです - 遡れるのは直近 5 年まで。それより前が欲しいときは、有報の「主要な経営指標等の推移」に載る 5 期分を使います。2026 年の有報から 2022 年の売上高が取れ、これを重ねて 2014 年 3 月期まで埋めました
- 今の上場銘柄だけで絞ると、分析が甘くなる。上場廃止した会社の有報を捨てると、過去を振り返ったときに「生き残った会社」だけが残ります。取得では絞らず、分析のときに絞る形に直しました
取得から XBRL の読み取りまで、ここにある罠の多くは土日の 2 日で踏み終えました。訂正有報や REIT のように、後からデータを眺めていて気づいたものもあります。
次にやること
- 取り込んだ有報の本文を Claude Code に読ませて、前年から変わった点を拾う。続きの記事「有価証券報告書を AI (Claude Code) で分析する」に書きました
まとめ
- EDINET API は無料。API キーを取って、ヘッダー
Ocp-Apim-Subscription-Keyで送る。クエリに載せるとログに残る - 呼び方は 2 段。
/documents.json?date=...&type=2で一覧、/documents/{docID}?type=1で XBRL の ZIP - 一覧は提出日単位でしか引けない。有報は
docTypeCode=120、上場会社はsecCodeあり - 数値は
.xbrlのインスタンスから、要素のローカル名とcontextRefで拾う。CurrentYearDurationが当期・連結全体 - キーが無効でも 200 が返る、単体決算は
NonConsolidatedMember、銀行の「経常収益」は利益ではない。この 3 つを最初に押さえる - 値は円。営業利益は 5 期分取れない。訂正有報は全文