請求書からデータを抽出するAPI
space-ocr の請求書データ抽出 API 開発者向けガイド。curl と Python での POST /ocr/fields、fields スキーマの宣言と autoFields、出所座標(box・quad)と review 契約までを解説します。
請求書から構造化データ(取引先、請求書番号、日付、明細の金額、税額)を取り出す処理は、ドキュメント自動化のなかでもっともよくある仕事のひとつであり、同時に手作業で組むのがもっとも面倒な仕事のひとつでもあります。OCRテキストに正規表現をかける方法は、取引先がレイアウトを変えた瞬間に壊れます。テンプレートマッチング系のツールは、仕入先ごとに枠を引かされます。本当に欲しいのは、どんなレイアウトでも読み取り、きれいに型付けされたフィールドを返し、しかも――ここが肝心ですが――各値がページ上のどこから来たのかを教えてくれて結果を信頼できる、そんな請求書からデータを抽出するAPIでしょう。
この最後の部分こそがすべてです。total: 2,045 を根拠なしで返してくるだけの抽出エンドポイントは、買掛金(AP)のパイプラインにおいてはむしろリスクになります。本ガイドでは、space-ocrの POST /ocr/fields エンドポイントを解説します。1枚の請求書画像を受け取り、宣言したフィールドスキーマ(あるいは autoFields による自動提案)を適用し、すべての値を出所座標と明示的な検証判定つきで返す、単一の同期呼び出しです。
コードを1行も書く前に、まず出力を確認する
以下は実際に解析したレシートです。任意のフィールドにカーソルを合わせると、画像上のボックスが光ります――そのボックスは、まさにその値が読み取られた場所であり、各値はそれぞれ固有の検証判定と根拠を伴います。請求書もまったく同じ挙動です。抽出したすべてのフィールドが、元になったピクセルの上に戻ってきます。

Each value with a box carries a verified on-page location — in data.cells[path], that is box + 4-point quad + evidence.match_ratio — on a 0–1000 normalized grid (0,0 top-left → 1000,1000 bottom-right), the same shape the live API returns. Hover a field to trace it back to the pixels it came from.
認証とベースURL
公開APIは単一のベース https://api.space-ocr.com で提供され、/v1 のようなパスバージョニングはありません。すべてのリクエストは、spocr_ で始まるキーを使ったHTTP Bearerトークンで認証します。
Authorization: Bearer spocr_xxxxxxxxxxxxxxxxキーが欠落している、または無効な場合は 401(error.code: "invalid_api_key")が返ります。403 はまったく別の意味で、キー自体は有効でもそのキーの権限外のリソース(たとえば別のキーが作成した job)に触れた場合です。すべてのレスポンスには X-Request-Id ヘッダー(形式 req_xxx)が付与されるので、サポート時のトレース用にログへ残しておきましょう。クライアントを自動生成したい場合は、GET /openapi.json でOpenAPI 3.1として完全な仕様が公開されています。
最もシンプルな呼び出し:明示的な請求書スキーマ
最短ルートは、欲しいフィールドを自分で名指しすることです。fields に FieldSpec オブジェクトの配列を渡すと、レスポンスはその宣言とまったく同じ形で返ります。テンプレートを選ぶ必要も、枠を引く必要もありません。imageType は必須パラメータで、image の渡し方を "url" か "base64" で明示します。
スカラー型の宣言は、意図の記述以上の働きをします。invoice_date を "date"、金額項目を "number" と宣言すると、素の読み取り値の隣に決定論的な data.normalized 層が加わります。invoice_no の required: true は、値が空または欠落したときに静かな空文字として通さず、検討リストへ載せるという意味です。pattern は自社の採番規則で、JSON Schema と同じ部分一致なので、値全体を見るなら ^…$ で囲みます。
宣言すべき項目がまだ分からない場合は、fields を省いて autoFields: true を送れば、モデルが文書そのものからスキーマを提案します。未知の取引先フォームを探索するにはこれが適した方法で、フィールド名が固まったら明示的な fields 配列へ移し、呼び出しごとにレスポンスの形が動かないようにします。
curl -X POST https://api.space-ocr.com/ocr/fields \
-H "Authorization: Bearer spocr_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"image": "https://example.com/invoices/inv-4471.jpg",
"imageType": "url",
"fields": [
{ "name": "vendor", "type": "string" },
{ "name": "invoice_no", "type": "string", "required": true,
"pattern": "^[A-Z0-9-]+$" },
{ "name": "invoice_date", "type": "date" },
{ "name": "subtotal", "type": "number" },
{ "name": "tax", "type": "number" },
{ "name": "total", "type": "number" },
{ "name": "line_items", "type": "array",
"children": [
{ "name": "description", "type": "string" },
{ "name": "qty", "type": "number" },
{ "name": "unit_price", "type": "number" }
]
}
]
}'正式な形式はキャメルケースです。 パラメータは imageType と autoFields です。従来のスネークケースの別名(image_type、auto_fields)も依然として動作しますが、非推奨(deprecated) です。新しいコードではキャメルケースの名前を使ってください。
レスポンスの構造
呼び出しが成功すると { status: "success", data: { ... } } が返ります。data は4つの部分に分かれ、それぞれ役割はひとつだけです。
data.values— 業務データそのもの。宣言したfieldsとまったく同じ形で、それ以外は入りません。data.cells— パス(total、line_items[0].unit_price)をキーとするフラットなマップです。各セルは 0〜1000に正規化されたグリッド上の軸並行な矩形box{ xmin, ymin, xmax, ymax }(0,0 = 左上、1000,1000 = 右下)と、ページの傾きに沿う4点のquad(傾いたスマホ撮影の請求書でもきれいに囲めます)、そしてverified・review・evidenceを持ちます。ピクセルへの換算はdata.image基準でpixel_x = box.xmin / 1000 × data.image.widthです。data.review— 集計です。unit: "field"、declared/returned/boxed/verifiedの各件数、flagged({ path, reasons }の配列)、およびby_reasonのヒストグラムが入ります。確認すべき件数はflagged.lengthで、別のカウンタはありません。by_reasonは項目ごとに1件ではなく理由すべてを数えるため、合計はflagged.length以上になります。data.normalized—valuesと同じ形の疎なツリーで、スカラー型またはpatternを宣言したフィールドの決定論的な解析結果だけが載ります。valuesが上書きされることはありません。
verified は文字一致のスコアではなく判定です。理由が何であれ review に理由が付いていれば false、照合が走って何も立たなければ true、そもそも照合する対象がなければ(行のユニオンなど)null になります。文字照合そのものは evidence.text_match にあり、だからこそ verified: false と text_match: true の同居は矛盾ではありません――文字は一致したが、宣言した規則のほうが引っ掛けた、という正常な組み合わせです。evidence にはほかに source(vision_symbol_match、token_id など)、match_ratio、そして printed_text(その座標でOCRが読んだ文字)が入ります。完全一致で突合したいときは printed_text と比べてください。
理由コードはAPI契約の語彙なので翻訳しません。text_mismatch、missing、pattern_mismatch、type_mismatch、out_of_range、low_ratio、overwide_box などです。reasons は常に配列で、ランク順に並び0番が代表です。扱うコードを対応づけ、未知のコードには汎用メッセージを出す作りにしておきましょう。
{
"status": "success",
"data": {
"values": {
"vendor": "Acme Supply Co.",
"invoice_no": "INV-4471",
"invoice_date": "2026/06/18",
"subtotal": "1,859",
"tax": "186",
"total": "2,045",
"line_items": [
{ "description": "Steel bracket 40mm", "qty": "12", "unit_price": "98" }
]
},
"cells": {
"total": {
"box": { "xmin": 595, "ymin": 974, "xmax": 781, "ymax": 1000 },
"quad": [
{ "x": 594, "y": 975 }, { "x": 781, "y": 972 },
{ "x": 781, "y": 998 }, { "x": 595, "y": 1000 }
],
"verified": true,
"review": null,
"evidence": {
"text_match": true,
"source": "vision_symbol_match",
"match_ratio": 0.93,
"printed_text": "2,045"
},
"normalized": { "value": 2045, "type": "number", "method": "deterministic" }
},
"line_items[0].unit_price": {
"box": { "xmin": 693, "ymin": 460, "xmax": 738, "ymax": 488 },
"quad": [
{ "x": 693, "y": 460 }, { "x": 738, "y": 460 },
{ "x": 738, "y": 488 }, { "x": 693, "y": 488 }
],
"verified": false,
"review": { "reasons": ["text_mismatch"] },
"evidence": {
"text_match": false,
"source": "vision_symbol_match",
"match_ratio": 0.62,
"printed_text": "9B"
}
}
},
"review": {
"unit": "field",
"declared": 9,
"returned": 9,
"boxed": 9,
"verified": 8,
"flagged": [
{ "path": "line_items[0].unit_price", "reasons": ["text_mismatch"] }
],
"by_reason": { "text_mismatch": 1 }
},
"normalized": {
"invoice_no": "INV-4471",
"invoice_date": "2026-06-18",
"subtotal": 1859,
"tax": 186,
"total": 2045,
"line_items": [ { "qty": 12, "unit_price": 98 } ]
},
"image": { "width": 1654, "height": 2339 }
}
}座標はモデルの言い分を鵜呑みにして決めていません。 言語モデルは各値のテキストと――どの単語トークンを使ったかのヒントを返しますが、ボックスそのものは決して返しません。エンジンはそのテキストを、ビジョンOCRがページ上で実際に検出したシンボルに対して文字単位で照合します。evidence.match_ratio はそのうちどれだけが見つかったかを表し、ボックスはそれらの文字が由来する実際のピクセルの上に置かれます。モデルのトークンヒントはノイズを含むことがあるため(繰り返し行の間で取り違えることがあります)、列・行の一貫性チェックでヒントを盲信せずに検証します。この比率はあくまで補助的な根拠であって判定ではありません――判定を担うのは verified と review です。詳しい仕組みはなぜバウンディングボックスがOCRを監査可能にするのかをご覧ください。
検討シグナルになる宣言
FieldSpec は、フィールドに名前を付けるだけでなく「良い値とはどういうものか」を宣言できます。そして宣言ひとつひとつに、対応する検討理由が結び付いています。required: true は値が空、あるいはまったく返らなかったときに missing を立てます。これは文字照合だけでは原理的に見えない唯一の失敗クラスです。pattern は pattern_mismatch、enum は集合の外に出たときに同じ理由、min / max は正規化した数値に対して out_of_range、スカラー type は解析できないときに type_mismatch を立てます。
これらはいずれもモデルには渡りません。宣言によって抽出精度が上がるわけではなく、値はどちらでも同じように返ります。宣言が決めるのは、どのパスが data.review.flagged に載るか、そして label の場合はエンジンがどこに座標を固定するか、です。読み取りと検査を独立に保つこと自体が狙いで、だからこそ両者の一致に意味が生まれます。
モデルを実際に誘導するのは description です。何をどう取得するかを平易な自然言語で記述します。そして type: "array" と children の組み合わせが、繰り返し現れる明細行を取り出す方法です――1つの子スキーマで多数の行を扱い、各行は line_items[0]、line_items[1] のようにパスで指定できます。(この点は請求書からの明細行抽出で詳しく掘り下げます。)
import requests, base64
with open("invoice.jpg", "rb") as f:
b64 = base64.b64encode(f.read()).decode()
resp = requests.post(
"https://api.space-ocr.com/ocr/fields",
headers={"Authorization": "Bearer spocr_xxxxxxxxxxxxxxxx"},
json={
"image": b64,
"imageType": "base64",
"fields": [
{"name": "vendor", "type": "string",
"description": "Supplier / billing company name"},
{"name": "invoice_no", "type": "string", "required": True,
"description": "Invoice number as printed"},
{"name": "invoice_date", "type": "date"},
{"name": "total", "type": "number",
"description": "Grand total"},
{"name": "line_items", "type": "array",
"description": "One row per line on the invoice",
"children": [
{"name": "description", "type": "string"},
{"name": "qty", "type": "number"},
{"name": "unit_price", "type": "number"},
]},
],
},
timeout=200,
)
data = resp.json()["data"]
# 印字どおりの値と、その隣にある決定論的な解析結果
print(data["values"]["total"], data["normalized"].get("total"))
# 検討キュー:確認が必要なパスごとに1件
for item in data["review"]["flagged"]:
cell = data["cells"].get(item["path"])
print(item["path"], item["reasons"][0], cell["box"] if cell else None)values は読み取り結果であって、バイト単位の複製ではありません。 7,855 と印字された合計は文字列 "7,855" として返ります――要約も言い換えもしないからこそ、値を座標に結び付けられます。とはいえこれはモデルが読んだ文字列であり、文字照合は全角・括弧・空白を畳んでから比べるため、(税抜) が (税抜) になる程度の書き直しは通ります。完全一致で突合したい場合は cells[path].evidence.printed_text(その座標でOCRが読んだ文字)と比べてください。ISO日付や区切りのない数値といった解析済みの形は data.normalized に載り、values を上書きすることはありません。WebのUIで見える ¥ は装飾であって、値の一部ではありません。エンジンが受け付けるのはラスター画像のみ――JPEG、PNG、GIF、BMP、TIFF、WebP――で、自動的にRGBへ変換されます。
非同期処理へ:バッチアップロード、ジョブ、Webhook
POST /ocr/fields は同期処理で、リクエスト/レスポンスのループで1枚の請求書を扱うのに最適です。読み取りのあいだ接続を保持し、処理時間の上限は 180秒 です。超えると 504 が error.code: "ocr_engine_timeout" とともに返り、この呼び出しは課金されません。上限に触れる原因は画素数よりも密度であることがほとんどなので、対処は1ページ1画像への分割か、下記の非同期ルートです。
請求書のフォルダーをまとめて処理したい場合は、POST /upload(multipartの files を繰り返し、1リクエストあたり最大20件)でシートに投入します。デフォルトでは即座にjobs配列を返します。
{ "path": "...", "jobs": [ { "uniqueKey": "...", "jobId": "...", "status": "pending" } ] }結果は2通りの方法で受け取れます。GET /jobs/{jobId} をポーリングするか、Webhookを登録するかです。Webhookはスペースごとに1つのURLで、X-Spaceocr-Signature ヘッダーによりHMAC-SHA256で署名されます。注目すべきイベントは upload.received、item.created、ocr.completed(data.result に values / cells / review / image と同じ構造で抽出結果が入ります)、そして ocr.failed です。ペイロードを信頼する前に、必ず署名を検証してください。
冪等性、リクエストトレース、レート制限
いくつかのヘッダーを使うことで、本番のパイプラインでも安全にリトライできるようになります。
| ヘッダー | 用途 |
|---|---|
Idempotency-Key | /ocr/fields、/create、/upload で受け付けます。同じキーによる再送はキャッシュ済みレスポンスを24時間リプレイします(X-Idempotent-Replay: true)――二重課金なしで安全にリトライできます。あくまで再送の安全装置であり、保管手段ではありません。 |
X-Request-Id | すべてのレスポンスで返されます(req_xxx)。サポート用にログへ残しましょう。 |
X-RateLimit-Remaining | そのキーに残る、この1分間の呼び出し可能数です。 |
レート制限はキーごとに60リクエスト/分、uidごとに600リクエスト/分です。超過すると error.code: "rate_limited" とともに 429 が返り、待機秒数は Retry-After ヘッダーに入ります。すぐ再送せず、この値でバックオフしてください。
キャパシティ設計の目安として、本番トラフィックで観測された所要時間は p50 が約7.2秒、p90 が約10.5秒です。これはSLAではなく観測された分布であり、フィールド数の多い密な請求書はこれを上回ります。
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded",
"requestId": "req_8fa2c1"
}
}抽出からクエリ可能なシートへ
請求書をシートに抽出してしまえば、読み返すたびにOCRを再実行する必要はありません。GET /view は、保存済みの行に対してサーバー側でクエリを実行します――where、sort、select、limit、offset を、課金なし・再抽出なしで使えます。各行は直接呼び出したときと同じ values / cells / review / image の構造で返り、ペイロードを軽くしたい場合は boxes=0 を付けて cells マップを落とせます。そこからCSVへエクスポートできます(UTF-8 BOM付きなので、ExcelでもCJKテキストでも文字化けせずに開けます)――詳しくはスキャン文書をCSVに変換するをご覧ください。
料金
POST /ocr/fields は1回の呼び出しにつき¥10、POST /upload は ¥10 × N枚(画像枚数)です。失敗時の課金はありません――400 invalid_image と 504 ocr_engine_timeout はそもそも課金に到達せず、502 のエンジンエラーや ocr.failed イベントは自動的に返金されます。読み取り専用のエンドポイント(GET /space、/view、/jobs、/amount、/health)は無料です。無料プランはクレジットカード不要で月100クレジット。有料プランは Starter が月額¥3,980、Pro が月額¥8,980 で、最新の一覧は料金ページをご覧ください。
APIで請求書からデータを抽出する手順
- APIキーを取得するサインアップして spocr_ で始まるAPIキーを作成します。すべてのリクエストは https://api.space-ocr.com に対し Authorization: Bearer ヘッダーで認証します。
- 請求書画像を用意するエンジンが読むのはラスター画像のみです(JPEG、PNG、GIF、BMP、TIFF、WebP)。公開URLまたは純粋なbase64として渡し、必須パラメータ imageType に 'url' または 'base64' を明示します。
- 欲しいフィールドを宣言するPOST /ocr/fields に fields[] 配列を渡します。vendor、invoice_no、invoice_date、金額項目、そして children を持つ配列としての line_items です。宣言すべき項目がまだ分からない場合は autoFields: true を送ります。
- 値と検討キューを読む業務データは data.values から取り、続けて data.review.flagged をたどります。各エントリはパスと理由の組で、そのパスを data.cells で引くと box・quad・evidence が得られます。
- 規模に応じて拡張する多数の請求書を扱う場合は POST /upload でシートに投入し、GET /jobs/{jobId} のポーリングまたは ocr.completed Webhookで結果を受け取ります。その後は GET /view でクエリするか、CSVへエクスポートします。