Upstage Document Parse API を Python から叩いてみる
はじめに
前回は、Upstage Studio の GUI から Document Parse を試しました。
GUI で触ると、請求書や職務経歴シートのような表がある文書でも、ただのテキストではなく HTML や Markdown に近い形で見られました。ざっくり相性を見るには GUI が楽です。
GUI でだいたいの手応えは見えたので、次は API から使ってみます。画面で見えていた HTML や Markdown の結果が API ではどう返ってくるのか、使い勝手を確認してみましょう。
今回は、前回と同じサンプルの一部を使って、Document Parse API を Python から呼び、返ってきた JSON をそのまま眺めてみます。

API キーを用意する
API から使うには API キーが必要です。Upstage の Web Console にログインして、API Keys から作成できます。
試した時点では、初回 10 USD 分のクレジットや無料枠があるようで、クレジットカード登録なしで試せました。

画面を見ると Studio API Key と Secret key がありました。前回 Studio を触ったときに作られたのかもしれませんし、Studio 側からも作れるのかもしれません。このあたりは少し曖昧です。
作成したキーは、今回は環境変数 UPSTAGE_API_KEY に入れて使います。
export UPSTAGE_API_KEY="..."
まずは curl で叩く
Document parsing API reference を見ると、Document Parse API は https://api.upstage.ai/v1/document-digitization に multipart/form-data でファイルを投げる形になっています。API キーは Authorization: Bearer ... で渡します。
まずはドキュメントの内容に従って、curl でアクセスしてみます。
model には alias か、特定の snapshot model を指定できます。2026-04-12 に Document Parse のモデルページ を見た時点では、document-parse は document-parse-260128 を指していました。
| 名前 | メモ |
|---|---|
document-parse |
alias。現時点では document-parse-260128
|
document-parse-260128 |
2026-01-28 リリース。前回 GUI で選んだモデル |
document-parse-251217 |
2025-12-17 リリース |
document-parse-250618 |
2025-06-18 リリース |
document-parse-250508 |
2025-05-08 リリース |
document-parse-250404 |
2025-04-04 リリース |
document-parse-250116 |
2025-01-16 リリース |
document-parse-240910 |
2024-09-10 リリース |
前回 GUI で選んだモデルも document-parse-260128 でした。alias の document-parse が現時点では同じモデルを指しているので、今回は document-parse を指定します。
# sample_input/納品書.jpeg は実際のファイルパスに置き換える
curl -X POST https://api.upstage.ai/v1/document-digitization \
-H "Authorization: Bearer ${UPSTAGE_API_KEY}" \
-F "document=@sample_input/納品書.jpeg" \
-F "model=document-parse" \
-F "mode=standard" \
-F "ocr=force" \
-F "output_formats=['html','markdown']" \
-F "coordinates=true"
ocr は auto と force があります。画像ファイルだけなら auto でも OCR が走りますが、スキャン PDF なども混ざる前提なら、最初は force にして挙動をそろえておくほうが見やすいです。
output_formats は、今回は html と markdown を返すようにしました。前回 GUI で見ていた結果に近いものを、API からも確認したいためです。

レスポンスを見ると、指定した html と markdown が返ってきています。GUI で見ていた出力も、API ではこの JSON の中から取り出せばよさそうです。
Python から呼ぶ
次に Python から呼びます。
今回は、input/ フォルダに置いた PDF や画像を読み、output/ フォルダに Markdown と HTML を書き出すスクリプトにしました。API キーは .env から読みます。
.env
input/
納品書.jpeg
output/
納品書.md
納品書.html
.env はこうしておきます。
UPSTAGE_API_KEY=...
スクリプトは parse_documents.py という1ファイルにしました。python-dotenv は使わず、.env をそのまま読んでいます。今回は uv で実行しました。
uv init --no-package --vcs none --no-readme --no-pin-python
uv add requests
from __future__ import annotations
import argparse
import os
from pathlib import Path
from typing import Any
import requests
ENDPOINT = "https://api.upstage.ai/v1/document-digitization"
BASE_DIR = Path(__file__).resolve().parent
SUPPORTED_SUFFIXES = {
".bmp",
".docx",
".heic",
".jpeg",
".jpg",
".pdf",
".png",
".pptx",
".tif",
".tiff",
".xlsx",
}
def load_api_key(env_file: Path) -> str:
if env_file.exists():
for line in env_file.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
if key.strip() == "UPSTAGE_API_KEY":
return value.strip().strip("'\"")
api_key = os.environ.get("UPSTAGE_API_KEY")
if api_key:
return api_key
raise SystemExit("UPSTAGE_API_KEY is not set in .env")
def iter_inputs(input_dir: Path) -> list[Path]:
if not input_dir.is_dir():
raise FileNotFoundError(input_dir)
return sorted(
child
for child in input_dir.iterdir()
if child.is_file() and child.suffix.lower() in SUPPORTED_SUFFIXES
)
def parse_document(
path: Path,
*,
api_key: str,
) -> dict[str, Any]:
headers = {"Authorization": f"Bearer {api_key}"}
data = {
"model": "document-parse",
"mode": "standard",
"ocr": "force",
"output_formats": "['html','markdown']",
"coordinates": "true",
}
with path.open("rb") as fp:
files = {"document": (path.name, fp)}
response = requests.post(
ENDPOINT,
headers=headers,
data=data,
files=files,
timeout=180,
)
response.raise_for_status()
return response.json()
def write_outputs(result: dict[str, Any], output_dir: Path, stem: str) -> None:
content = result.get("content")
if not isinstance(content, dict):
return
output_dir.mkdir(parents=True, exist_ok=True)
markdown = content.get("markdown")
if isinstance(markdown, str) and markdown.strip():
(output_dir / f"{stem}.md").write_text(markdown, encoding="utf-8")
html = content.get("html")
if isinstance(html, str) and html.strip():
(output_dir / f"{stem}.html").write_text(html, encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--env-file", type=Path, default=BASE_DIR / ".env")
parser.add_argument("--input-dir", type=Path, default=BASE_DIR / "input")
parser.add_argument("--output-dir", type=Path, default=BASE_DIR / "output")
args = parser.parse_args()
api_key = load_api_key(args.env_file)
inputs = iter_inputs(args.input_dir)
if not inputs:
raise SystemExit(f"No supported files found: {args.input_dir}")
for path in inputs:
print(f"parse: {path}")
result = parse_document(path, api_key=api_key)
write_outputs(result, args.output_dir, path.stem)
if __name__ == "__main__":
main()
全文は parse_documents.py に置いています。
今回は input/納品書.jpeg を置いてから実行しました。
uv run parse_documents.py
実行後、output/納品書.md と output/納品書.html ができました。

納品書の明細と金額の部分が、Markdown のテーブルとして残っています。
# 納品書
ご注文日 2026年4月7日 ご注文番号
発行日 2026年4月7日
| 数量商品名 | 種類 | 金額 (税込) |
| --- | --- | --- |
| 1 UGREEN LANケーブルCAT6A メッシュLAN ケーブルカテゴリー6Aコネクタ RJ45 高速 10Gbps/500MHzCAT6準拠 イーサネットケーブル イーサネットケーブル 爪折れ防止PVC素材 モデム ルータ PS3 PS4Xbox等に対応 20M BOF8MZ31KS,6941876251643,BOF8MZ31KS | その他 | ¥2,228 |
| 小計 | ¥2,228 |
| --- | --- |
| 配送料・手数料 | ¥200 |
| 合計 | ¥2,428 |
output/納品書.html をブラウザで開くと、前回 GUI で見ていたように表の形で確認できます。

同期 API と非同期 API
今回のサンプルでは同期 API を使いました。公式ドキュメントを見ると、同期 API は1リクエストあたり最大100ページまでで、100ページを超える文書では分割するか非同期 API を使う案内になっています。
今回使った職務経歴シートのような短い PDF なら、同期 API でそのまま結果を確認できました。
RAG 前処理に使うなら
RAG の前処理として見るなら、まず Markdown をそのままチャンク分割したくなります。
ただ、テーブルがある文書では、単純に文字数で切ると行や列のまとまりが崩れます。前回の職務経歴シートのように、列境界で一部の文字が隣に吸われるケースもあります。そういう文書を扱うなら、Markdown だけを見てすぐベクトルDBへ入れるより、HTML も見て、表をどうチャンク化するかを決めたほうがよさそうです。
今回は、まず次のような使い分けで考えています。
| 保存物 | 使いどころ |
|---|---|
| HTML | テーブルや見出し構造を目で確認する |
| Markdown | LLM に渡す本文候補にする |
このあたりは「Document Parse の出力が正しいか」だけではなく、後段で何をしたいかによって変わりそうです。ざっくり内容検索をしたいのか、請求書の明細を1行ずつ DB に入れたいのかで、見ておくべき出力も変わりそうですね。
まとめ
前回 GUI で見ていた Document Parse の結果を、今回は curl と Python のコードから取得しました。GUI で見えていた HTML や Markdown が、API のレスポンス JSON からそのまま取り出せます。
コードから呼べるようになると、ファイルをまとめて投げたり、返ってきた HTML / Markdown を保存したりするところを自分の処理に組み込めます。外部 API として呼び出すところまでは、かなり簡単に使えそうです。
今後は、実際の処理に組み込んだときにどうなるかも見てみたいです。たとえば RAG のチャンクに使ったり、既存の RAG 検索システムに PDF をそのまま入れるのではなく、Document Parse で処理した HTML や Markdown を入れることで精度が変わるのか、というあたりです。
Discussion