概要
RESTful、JSON、CORS 対応。可変長のバッチや非同期処理は Jobs / Webhooks セクションで案内します。
5分クイックスタート
① Developer → API Keys でキーを発行します(カード不要)。
② 右の curl をそのまま実行 — サンプル画像は実際にホスティングされているので、キーを差し替えるだけで動きます。
③ レスポンスの data.values の値と、data.cells の box / quad / verified、data.review.flagged(要確認リスト)を確認してください。フィールドに型や制約(number / date / pattern / enum / near)を宣言すると、解釈済みの値が data.normalized に、違反が review に載ります — 宣言できる一覧は POST /ocr/fields の fields を参照してください。
④ コードを書く前に試したいときは、MySpace コンソールがそのままプレイグラウンドになります。シートにファイルを置けば API と同じ結果が並び、セルをクリックすると元画像の座標まで確認できます。API からアップロードした文書も同じシートに現れるので、自動処理と目視確認を同じ場所で扱えます。
curl -X POST https://api.space-ocr.com/ocr/fields \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": "https://space-ocr.com/samples/two-receipts.jpg",
"imageType": "url",
"fields": [{ "name": "store_name" }, { "name": "total" }]
}'認証
キー形式は spocr_ で始まる文字列です。漏洩した場合は即時に失効してください。
curl https://api.space-ocr.com/amount \
-H "Authorization: Bearer YOUR_API_KEY"Base URL
# Production
https://api.space-ocr.com
# OpenAPI spec
https://api.space-ocr.com/openapi.jsonRate limits
応答には常に X-Request-Id (req_xxx) と X-RateLimit-Remaining(その分に残る呼び出し数)が含まれます。サポート連絡時には X-Request-Id を添えてください。
/ocr/fields・/create・/upload は Idempotency-Key ヘッダーをサポートします。同一キーの再送は 24h キャッシュされ、X-Idempotent-Replay: true で示されます。
画像サイズと応答時間
JSON ボディの上限は 28MB です。base64 はファイルの約 1.33 倍になるため、元画像で約 20MB 相当までを 1 リクエストで送れます。超えた場合は 413 を返し、details.limitBytes に許容サイズ、details.receivedBytes に受信サイズが入ります。
ただし 32MiB を超えるリクエストは、当社のコードに届く前に Google Cloud 側で遮断されます。このときの応答は JSON ではなく text/html(Google Frontend の 413 ページ)なので、応答を必ず JSON として解析する実装は例外で落ちます。上の 28MB を守っていればこの経路には到達しません。
大きな写真は、サーバー内部のエンコード結果によっては長辺 4000px に縮小してから読み取ります(縦横比は保持)。実際に読み取ったサイズは data.image の width / height に入るので、送った画像と異なることがあります。座標は 0–1000 の正規化値なので縮小されても意味は変わりません。クライアント側で縮小するなら長辺 4000px が目安です。
向きも同じです。EXIF の orientation は読み取る前にピクセルへ反映されるため、横向きに撮った写真は正立したページとして読まれ、値も座標もその向きで返ります。data.image の width と height が送信ファイルと入れ替わるのはこのときです(4000×3000 で送って 3000×4000)。枠を描く実装は、送信画像のサイズではなく data.image を基準にしてください。
上限を超える画像は imageType: "url" で URL を渡すか、/upload(非同期・ファイルあたり 20MB)+ /jobs ポーリングまたは webhook をご利用ください。同期呼び出しの処理が 180 秒を超えると ocr_engine_timeout になります — 原因は画素数より密度(小さな文字が詰まった多ページ書類)であることが多く、縮小ではなく 1 ページ 1 画像への分割か非同期経路をご検討ください。
結果の再現性
値の抽出はモデルを通ります。同じ画像でも、実行ごとに出力の構成が揺れることが実測されています(どこまで 1 つの値として返すか、明細の切り方など)。座標の文字照合と normalized のパースは決定論的(追加のモデル呼び出しなし)ですが、その入力になる抽出そのものはそうではありません。
会計・監査のように「あとで同じ数字を出せること」が要件になる用途では、応答 JSON をそのまま保管し、確認は保存した値に対して行ってください。読み直しは、内容が変わったときや、レビュー結果を破棄してよいときだけで十分です。
Idempotency-Key は 24 時間だけ同じ応答を返す再送の安全装置であって、保管の仕組みではありません(保管については データの取り扱い を参照)。
エラー
{
"error": {
"code": "validation_failed",
"message": "imageType is required",
"requestId": "req_xxx"
},
"details": {
/* optional, endpoint-specific context (e.g. /upload returns processable count) */
}
}
// error.code: validation_failed | bad_request | invalid_image | invalid_api_key
// | key_inactive | unauthorized | forbidden | not_found
// | insufficient_balance | rate_limited | ocr_engine_error
// | ocr_engine_timeout | storage_error | internal_errorHTTP ステータス
構造化 OCR
画像から名前付きフィールドを抽出します。fields で抽出スキーマを指定するか、autoFields で自動提案させます。
同期呼び出しなので、応答が返るまで接続を保つ必要があります。処理は 最長 180 秒、超えると 504 ocr_engine_timeout(課金されません)。実測では 1 枚あたり数秒〜数十秒に収まりますが、クライアント側のタイムアウトは余裕をもって設定してください。この上限に当たるのは小さな文字が詰まった多ページ書類がほとんどで、原因は画素数ではなく密度です — 縮小すると読めなくなるので、1 ページ 1 画像に分割するか、処理時間に余裕のある非同期 POST /upload をご利用ください。
ボディパラメータ
Base64 文字列または画像 URL。JSON ボディの上限は 28MB で、base64 はファイルの約 1.33 倍になるので、元画像で約 20MB 相当までです。超えると 413(details.limitBytes / receivedBytes)を返します。それより大きい場合は URL 指定か /upload(非同期・ファイルあたり 20MB)をご利用ください。
長辺 4000px を超える画像はサーバー側で自動縮小してから読みます — 座標も縮小後のページ(data.image)基準で返るので、上限に合わせて事前に画質を落として送る必要はありません。
抽出スキーマ配列。autoFields を使う場合は省略可。値はページに書かれている読みで返り、要約や言い換えはしません — だから座標に結び付けて検証できます。
ただしバイト単位の複製ではありません: values はモデルが読んだ文字列で、文字照合は全角・括弧・空白を畳んでから比べるため、(税抜)が (税抜) になるような書き直しは通ります。完全一致で 突合 する場合は cells[path].evidence.printed_text(その座標で OCR が読んだ文字)を使ってください。
既定は string。number / integer / date を宣言しても values は変わりません — 型がモデルに渡ることはありません(型を伝えると、その形の値を作ってしまいます)。宣言した型が生むのは normalized という第二の層で、同じ読みをその型に解釈した値が values と同じ形で並びます("¥13,220" → 13220、"令和8年8月16日" → "2026-08-16"、"3袋" → 3)。解釈は決定的で追加のモデル呼び出しはありません。解釈できなかった値は normalized で null になり、reason "type_mismatch" が付きます — 多くの場合それは誤読の合図です。
逆に、宣言しない方がよい項目もあります。数量欄の「一式」、支払期限の「翌月末払い」のように 値ではない書き方が正規に印字される 項目です。型を宣言すると、書類として正しいのに毎回 type_mismatch として要確認に上がります(error は conventional_token / relative_date と種類までは言いますが、リストには載ります)。常に数値・日付が入る項目にだけ型を宣言し、それ以外は string のままで受けて、そちらの業務ルールで解釈してください。
値の横に印字されているラベル(例: "合計")。同じ値がページに複数回印字されているとき、座標をそのラベルの隣の出現にアンカーします。ラベルがページに正確に 1 回だけ印字されているときのみ働き、見つからなければ通常の検索に戻ったうえで、その事実が review.notes に issue: "label_unresolved" として載ります(値は返るので、この知らせが無いと宣言が効いていないことに気づけません)。「消費税(8%)」「10%対象 小計」のように複数の語にまたがる表記もそのまま書けます。required と同じくモデルには渡りません — 抽出テキストは変わらず、座標のアンカーだけが変わります。候補が複数あるときは配列で(例: ["発行日", "発行年月日"])。
効くのはトップレベルの string / number / integer / date フィールドだけです。array・object 本体と、その children に付けた label は読まれずに無視されます(ページ全体で 1 回きりのラベルは、繰り返す行のどれを指すのか原理的に言えないため)。明細表の「数量」「金額」のように列見出しが全行で共有される場合は label を付ける必要がありません — children の座標は行ごとに解決されます。行内で位置を伝えたいときは description(例: 「単価の右」)を使ってください。
値の そば に印字されているはずの語彙です(例: 宛先の会社名なら ["御中", "様"]、発行元なら ["登録番号", "〒", "TEL"])。抽出後、宣言した語をページ上で探し、値がそのどれの近傍にも無ければ reason "near_mismatch" が立ちます。
これは「完璧に読めたのに 別の場所の値 を選んだ」クラスへの唯一の手立てです。帳票には会社名が 2 社印字されていて、反対側を選んでも文字照合は一致するので verified: true のまま通過します。enum も、どちらも正当なマスタ値なら区別できません。near は選択を正しくするのではなく、間違った選択を 見える ようにします。
判定は値の すべての出現 に対して行います(v85)。どの出現も語彙のそばに無ければ near_mismatch(どこにあっても違う値)、そばにある出現はあるのに座標が付いたのが別の複製なら near_ambiguous(どちらを指すか未定 — 値自体は正しいかもしれません)。同じ値が 2 か所に印字される帳票で、正しい値が座標の付き方だけで通ったり落ちたりしていたためです。判定の内訳は cells[path].evidence.near にそのまま入ります。
match で「語が印字の どこ に付くか」を指定できます: boundary(既定 — 語そのもの、または語の先頭/末尾)· suffix(御中・様・宛)· prefix(〒・TEL・登録番号)· standalone(くっつかない語のみ)· anywhere(v81 の動作)。v85 で既定が変わりました: v81 は長い語の内側ならどこでも認めたため、工事名「中野様邸増築工事」の 様 が、御中 を一度も印字しない書式で 宛先 判定の証人になっていました。v81 の動作が要る場合は { "match": "anywhere" } と明示してください。語が 単独の語として 印字されている通常のケースは、どのモードでも影響を受けません。
宣言した語がページのどこにも印字されていないときは判定を保留し、review.notes に issue: "near_unresolved" として載ります(御中を印字しない書式を罰しないため)。その書式こそ取り違えが起きる場所である場合は not_near の担当です。label と同じくモデルには渡りません。近傍の窓はエンジン規定で、セルの高さを単位に横 ±6 倍・縦 ±3 倍です。
near の鏡 — 値の そばにあってはならない 語彙です(宛先の会社名に ["登録番号", "TEL", "〒"])。値がそのどれかの近傍にあれば reason "near_conflict" が立ち、隣にあった語と距離が cells[path].evidence.not_near に載ります。形も match の指定も near と同じです(v85)。
なぜ両方が要るのか: near は識別の目印が 印字されているとき にしか話せません。ところが当事者が実際に取り違えられるのは 宛名行を持たない事務用フォーム で、そこには 御中 がそもそも印字されず、near は保留するしかありません。一方で発行元ブロックは必ず何かを印字します(登録番号 / TEL / 〒)。だから届く言い方は否定形になります — 発行元ブロックの中に座っている宛先は、発行元です。
語彙が印字されていないことは違反ではないので、near と違って保留せず、そのまま通ります(review.notes にも載りません)。モデルには渡りません。
curl -X POST https://api.space-ocr.com/ocr/fields \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": "https://example.com/receipt.jpg",
"imageType": "url",
"fields": [
{ "name": "store_name", "type": "string",
"description": "店舗名" },
{ "name": "date", "type": "string",
"description": "取引日" },
{ "name": "payment_method", "type": "string",
"enum": ["現金", "クレジット", "電子マネー"],
"description": "支払方法" },
{ "name": "invoice_no", "type": "string", "required": true,
"description": "伝票番号" },
{ "name": "items", "type": "array",
"description": "購入品目",
"children": [
{ "name": "name", "type": "string" },
{ "name": "qty", "type": "string" },
{ "name": "price", "type": "string" }
]
},
{ "name": "total", "type": "number", "required": true,
"label": "合計",
"description": "合計" }
]
}'レスポンスフィールド
リクエストのスキーマそのままの純粋なユーザーデータ。予約キーが混ざらないので、そのまま DB に保存できます。値はモデルがページから読んだ文字列で、文字照合で印字と突き合わせています — バイト単位の複製ではないので、完全一致で照合するなら cells[path].evidence.printed_text を使ってください。
値には半角 ¥(U+00A5)のような文字がそのまま入ります。cp932 / Shift_JIS でエンコードすると(CSV 出力も含め)その 1 文字だけで例外になり得るので、UTF-8 のまま扱ってください。
傾いたスキャンに追従する 4 点。box と常に両方付きます。画面に枠を描くならこちらを使ってください。基準は送信したファイルではなく data.image が示す読み取り後のページです — EXIF で横倒しの写真は正立させてから読むため、width と height が入れ替わることがあります(4000×3000 で送って 3000×4000 が返る)。ピクセル換算も data.image で行ってください。
傾き補正(デスキュー)は行いません — ページを回して座標を作り直すことはなく、返る座標は傾いたまま読んだ入力画像の座標系です。傾いた写真では quad がその傾きに沿います。
このセルの判定です。review の鏡なので、二つが食い違うことはありません — false は review に理由が入っているとき(宣言したルール違反も含め、理由の種類を問いません)、true は何も立たず、かつ照合が実際に走ったとき、null は何も立たなかったが照合できるものが無かったとき(行ユニオンなど幾何のみの項目)です。ゲートに使うのはこの 1 個で足ります。
文字照合そのもの(値がこの座標の OCR 原文と一致したか、2 つの独立エンジンの合意)は evidence.text_match にあります。その不一致には元から専用の理由 text_mismatch があるので、判定から失われるものはありません。
true でも「求めた意味の値である」ことは意味しません。モデルがページ上の別の箇所(近くの補助見出しなど)を拾った場合、座標はその拾った文字に付き、照合も一致し、何も立たなければ true で返ります。座標が答えるのは「この値はどこから来たか」で、「これは正しい項目か」ではありません — そこは label / near / enum の担当です。
null なら通過、値が入っていれば人の確認を推奨。reasons: type_mismatch | out_of_range | pattern_mismatch | near_mismatch | near_ambiguous | near_conflict | nobox | text_mismatch | crop_mismatch | low_ratio | weak_source | low_ocr_confidence | ambiguous_occurrence | overwide_box | missing。reasons は破られたルール全部を ランク順 に並べた配列で、0 番が代表です(長さ 1 でも必ず配列)。前の 6 つは呼び出し側が宣言したルール違反なので、エンジンの推定より上位にランクされます。
near_mismatch と near_ambiguous は別々の質問への答えです: 前者は この値の どの出現も 宣言語のそばに無い(どこにあっても違う値)、後者は そばにある出現は あるが座標が付いたのはそれではない(どの複製を指すか未定 — 値自体は正しいかもしれません)。同じ値が 2 か所に印字される帳票で、正しい値が座標の付き方だけで通ったり落ちたりしていたのを分けたものです(v85)。near_ambiguous が立つとき ambiguous_occurrence は併記しません — 同じ事実を二つの語彙で言うことになるからです。
画面を作る場合は、ここに並んだ 全種(と今後追加され得るコード)に表示を割り当て、1 セルに複数の理由が同時に立つ配列を前提にしてください — 一部だけ対応すると未対応コードで表示が壊れます。未知のコードは汎用の「要確認」に落とすのが安全です。
スカラー型(number / integer / date、あるいは pattern・enum を付けた string)を宣言したフィールドにだけ付きます。data.normalized のその葉が null だった理由がここにあります。method は現在つねに "deterministic"(追加のモデル呼び出しはありません)。
error は読み取れなかった理由を種類で言います: not_numeric / not_an_integer / not_a_date は こちらの誤読の可能性(l510 のような)、no_year は年が印字されていない日付(8/16・9月末日)、conventional_token は書類がそう印字している慣用表記(一式・各・別途・ダッシュだけの欄)、relative_date は他の項目に依存する支払条件(翌月末払い・締日から60日)です。後ろの 2 つは 読み直しても値は出ません — 確認に回すのではなく、そちらの業務ルールで処理する対象です。どれでも reasons は type_mismatch のままなので、件数の集計は変わりません。
宣言がそのまま実行されなかったときだけ付くお知らせです。各項目は path / issue / description を持ち、issue で分岐してください。
issue: "type_coerced" は この API がサポートしない型を宣言した場合(declared_type / applied_type も付きます)。サポートするのは string / number / integer / date / array / object で、スカラー型は黙って処理され、解釈された値は normalized に返ります。
issue: "label_unresolved" は 宣言した label がどこにもアンカーできなかった場合です(印字されていない・2 回以上ある・隣に確信できる値が無い)。値そのものは通常の検索で返るため、これが無いと 宣言が効いていないことに気づけません。
{
"status": "success",
"data": {
"values": {
"store_name": "スーパー ABC",
"date": "2025-04-10",
"invoice_no": "",
"items": [
{ "name": "牛乳", "qty": "1", "price": "¥198" }
],
"total": "¥548"
},
"cells": {
"store_name": { "box": { "xmin": 14, "ymin": 36, "xmax": 210, "ymax": 58 },
"quad": [{"x":14,"y":36},{"x":210,"y":36},{"x":210,"y":58},{"x":14,"y":58}],
"verified": true, "review": null,
"evidence": { "text_match": true, "source": "vision_symbol_match", "match_ratio": 0.98, "ocr_confidence": 0.96 } },
"date": { "box": { "xmin": 14, "ymin": 80, "xmax": 180, "ymax": 102 },
"quad": [{"x":14,"y":80},{"x":180,"y":80},{"x":180,"y":102},{"x":14,"y":102}],
"verified": true, "review": null,
"evidence": { "text_match": true, "source": "token_id", "match_ratio": 1.0, "ocr_confidence": 0.99 } },
"items[0]": { "box": { "xmin": 263, "ymin": 460, "xmax": 738, "ymax": 523 },
"quad": [{"x":263,"y":460},{"x":738,"y":460},{"x":738,"y":523},{"x":263,"y":523}],
"verified": null, "review": null,
"evidence": { "source": "vision_symbol_match", "match_ratio": 1.0 } },
"items[0].name": { "box": { "xmin": 263, "ymin": 460, "xmax": 503, "ymax": 492 },
"quad": [{"x":263,"y":460},{"x":503,"y":460},{"x":503,"y":492},{"x":263,"y":492}],
"verified": true, "review": null,
"evidence": { "text_match": true, "source": "token_id", "match_ratio": 1.0, "ocr_confidence": 0.97 } },
"items[0].qty": { "box": { "xmin": 333, "ymin": 460, "xmax": 338, "ymax": 490 },
"quad": [{"x":333,"y":460},{"x":338,"y":460},{"x":338,"y":490},{"x":333,"y":490}],
"verified": true, "review": null,
"evidence": { "text_match": true, "source": "vision_symbol_match", "match_ratio": 1.0, "ocr_confidence": 0.94 } },
"items[0].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, "ocr_confidence": 0.88 } },
"total": { "box": { "xmin": 380, "ymin": 720, "xmax": 530, "ymax": 742 },
"quad": [{"x":380,"y":720},{"x":530,"y":720},{"x":530,"y":742},{"x":380,"y":742}],
"verified": true, "review": null,
"evidence": { "text_match": true, "source": "vision_symbol_match", "match_ratio": 1.0, "ocr_confidence": 0.98 },
"normalized": { "value": 548, "type": "number", "method": "deterministic" } }
},
"review": {
"unit": "field",
"declared": 7,
"returned": 6,
"boxed": 6,
"verified": 5,
"flagged": [
{ "path": "items[0].price", "reasons": ["text_mismatch"] },
{ "path": "invoice_no", "reasons": ["missing"] }
],
"by_reason": { "text_mismatch": 1, "missing": 1 }
},
// 宣言した型は values を書き換えず、この層に出ます
"normalized": { "total": 548 },
"image": { "width": 1654, "height": 2339 }
}
}Markdown 変換
レイアウトを保ったまま画像を Markdown に変換します。見出し・段落・リスト・表が要素として返り、要素ごとに座標が付きます。
同期呼び出しなので、応答が返るまで接続を保つ必要があります。処理は 最長 180 秒、超えると 504 ocr_engine_timeout(課金されません)。実測では 1 枚あたり数秒〜数十秒に収まりますが、クライアント側のタイムアウトは余裕をもって設定してください。この上限に当たるのは小さな文字が詰まった多ページ書類がほとんどで、原因は画素数ではなく密度です — 縮小すると読めなくなるので、1 ページ 1 画像に分割するか、処理時間に余裕のある非同期 POST /upload をご利用ください。
ボディパラメータ
Base64 文字列または画像 URL。JSON ボディの上限は 28MB で、base64 はファイルの約 1.33 倍になるので、元画像で約 20MB 相当までです。超えると 413(details.limitBytes / receivedBytes)を返します。それより大きい場合は URL 指定か /upload(非同期・ファイルあたり 20MB)をご利用ください。
長辺 4000px を超える画像はサーバー側で自動縮小してから読みます — 座標も縮小後のページ(data.image)基準で返るので、上限に合わせて事前に画質を落として送る必要はありません。
curl -X POST https://api.space-ocr.com/ocr/markdown \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": "https://example.com/report.jpg",
"imageType": "url"
}'レスポンスフィールド
傾いたスキャンに追従する 4 点。box と常に両方付きます。画面に枠を描くならこちらを使ってください。基準は送信したファイルではなく data.image が示す読み取り後のページです — EXIF で横倒しの写真は正立させてから読むため、width と height が入れ替わることがあります(4000×3000 で送って 3000×4000 が返る)。ピクセル換算も data.image で行ってください。
傾き補正(デスキュー)は行いません — ページを回して座標を作り直すことはなく、返る座標は傾いたまま読んだ入力画像の座標系です。傾いた写真では quad がその傾きに沿います。
このセルの判定です。review の鏡なので、二つが食い違うことはありません — false は review に理由が入っているとき(宣言したルール違反も含め、理由の種類を問いません)、true は何も立たず、かつ照合が実際に走ったとき、null は何も立たなかったが照合できるものが無かったとき(表要素そのもの — セル各々は検証されます)です。ゲートに使うのはこの 1 個で足ります。
文字照合そのもの(値がこの座標の OCR 原文と一致したか、2 つの独立エンジンの合意)は evidence.text_match にあります。その不一致には元から専用の理由 text_mismatch があるので、判定から失われるものはありません。
true でも「求めた意味の値である」ことは意味しません。モデルがページ上の別の箇所(近くの補助見出しなど)を拾った場合、座標はその拾った文字に付き、照合も一致し、何も立たなければ true で返ります。座標が答えるのは「この値はどこから来たか」で、「これは正しい項目か」ではありません — そこは label / near / enum の担当です。
null なら通過、値が入っていれば人の確認を推奨。reasons: type_mismatch | out_of_range | pattern_mismatch | near_mismatch | near_ambiguous | near_conflict | nobox | text_mismatch | crop_mismatch | low_ratio | weak_source | low_ocr_confidence | ambiguous_occurrence | overwide_box | missing。reasons は破られたルール全部を ランク順 に並べた配列で、0 番が代表です(長さ 1 でも必ず配列)。前の 6 つは呼び出し側が宣言したルール違反なので、エンジンの推定より上位にランクされます。
near_mismatch と near_ambiguous は別々の質問への答えです: 前者は この値の どの出現も 宣言語のそばに無い(どこにあっても違う値)、後者は そばにある出現は あるが座標が付いたのはそれではない(どの複製を指すか未定 — 値自体は正しいかもしれません)。同じ値が 2 か所に印字される帳票で、正しい値が座標の付き方だけで通ったり落ちたりしていたのを分けたものです(v85)。near_ambiguous が立つとき ambiguous_occurrence は併記しません — 同じ事実を二つの語彙で言うことになるからです。
画面を作る場合は、ここに並んだ 全種(と今後追加され得るコード)に表示を割り当て、1 セルに複数の理由が同時に立つ配列を前提にしてください — 一部だけ対応すると未対応コードで表示が壊れます。未知のコードは汎用の「要確認」に落とすのが安全です。
{
"status": "success",
"data": {
"values": {
"markdown": "# 四半期レポート\n\n売上は前年同期比で増加した。\n\n| 項目 | 金額 |\n| --- | --- |\n| 売上 | 12,000 |",
"elements": [
{ "type": "heading", "level": 1, "text": "四半期レポート" },
{ "type": "paragraph", "text": "売上は前年同期比で増加した。" },
{ "type": "table", "rows": 2, "cols": 2, "cells": [
{ "row": 0, "col": 0, "header": true, "text": "項目" },
{ "row": 0, "col": 1, "header": true, "text": "金額" },
{ "row": 1, "col": 0, "header": false, "text": "売上" },
{ "row": 1, "col": 1, "header": false, "text": "12,000" }
] }
]
},
"cells": {
"elements[0]": { "box": { "xmin": 60, "ymin": 48, "xmax": 520, "ymax": 92 },
"quad": [{"x":60,"y":48},{"x":520,"y":48},{"x":520,"y":92},{"x":60,"y":92}],
"verified": true, "review": null,
"evidence": { "text_match": true, "source": "token_id", "ocr_confidence": 0.98 } },
"elements[1]": { "box": { "xmin": 60, "ymin": 120, "xmax": 900, "ymax": 160 },
"quad": [{"x":60,"y":120},{"x":900,"y":120},{"x":900,"y":160},{"x":60,"y":160}],
"verified": true, "review": null,
"evidence": { "text_match": true, "source": "token_id" } },
"elements[2]": { "box": { "xmin": 60, "ymin": 200, "xmax": 640, "ymax": 320 },
"quad": [{"x":60,"y":200},{"x":640,"y":200},{"x":640,"y":320},{"x":60,"y":320}],
"verified": null, "review": null, "evidence": {} },
"elements[2].cells[0]": { "box": { "xmin": 60, "ymin": 200, "xmax": 350, "ymax": 260 },
"quad": [{"x":60,"y":200},{"x":350,"y":200},{"x":350,"y":260},{"x":60,"y":260}],
"verified": true, "review": null, "evidence": { "text_match": true, "source": "token_id" } },
"elements[2].cells[1]": { "box": { "xmin": 350, "ymin": 200, "xmax": 640, "ymax": 260 },
"quad": [{"x":350,"y":200},{"x":640,"y":200},{"x":640,"y":260},{"x":350,"y":260}],
"verified": false,
"review": { "reasons": ["text_mismatch"] },
"evidence": { "text_match": false, "source": "token_id", "ocr_confidence": 0.71 } },
"elements[2].cells[2]": { "box": { "xmin": 60, "ymin": 260, "xmax": 350, "ymax": 320 },
"quad": [{"x":60,"y":260},{"x":350,"y":260},{"x":350,"y":320},{"x":60,"y":320}],
"verified": true, "review": null, "evidence": { "text_match": true, "source": "token_id" } },
"elements[2].cells[3]": { "box": { "xmin": 350, "ymin": 260, "xmax": 640, "ymax": 320 },
"quad": [{"x":350,"y":260},{"x":640,"y":260},{"x":640,"y":320},{"x":350,"y":320}],
"verified": true, "review": null, "evidence": { "text_match": true, "source": "token_id" } }
},
"review": {
"unit": "element",
"total": 6,
"boxed": 6,
"verified": 5,
"flagged": [{ "path": "elements[2].cells[1]", "reasons": ["text_mismatch"] }],
"by_reason": { "text_mismatch": 1 },
"coverage": { "recovered_blocks": 0, "vision_tokens": 40, "tokens_claimed": 40, "token_coverage": 1.0 }
},
"image": { "width": 1654, "height": 2339 }
}
}プレーンテキスト OCR
スキーマも Markdown 文法もなしに、文書の文字だけを全部返します。モデルが画像を見て本当の読み順にブロックを並べ替えるので、多段組みや傾いたスキャンでも文が入り混じりません。
同期呼び出しなので、応答が返るまで接続を保つ必要があります。処理は 最長 180 秒、超えると 504 ocr_engine_timeout(課金されません)。実測では 1 枚あたり数秒〜数十秒に収まりますが、クライアント側のタイムアウトは余裕をもって設定してください。この上限に当たるのは小さな文字が詰まった多ページ書類がほとんどで、原因は画素数ではなく密度です — 縮小すると読めなくなるので、1 ページ 1 画像に分割するか、処理時間に余裕のある非同期 POST /upload をご利用ください。
ボディパラメータ
Base64 文字列または画像 URL。JSON ボディの上限は 28MB で、base64 はファイルの約 1.33 倍になるので、元画像で約 20MB 相当までです。超えると 413(details.limitBytes / receivedBytes)を返します。それより大きい場合は URL 指定か /upload(非同期・ファイルあたり 20MB)をご利用ください。
長辺 4000px を超える画像はサーバー側で自動縮小してから読みます — 座標も縮小後のページ(data.image)基準で返るので、上限に合わせて事前に画質を落として送る必要はありません。
curl -X POST https://api.space-ocr.com/ocr/text -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" -d '{
"image": "https://example.com/note.jpg",
"imageType": "url",
"includeBlocks": true
}'レスポンスフィールド
傾いたスキャンに追従する 4 点。box と常に両方付きます。画面に枠を描くならこちらを使ってください。基準は送信したファイルではなく data.image が示す読み取り後のページです — EXIF で横倒しの写真は正立させてから読むため、width と height が入れ替わることがあります(4000×3000 で送って 3000×4000 が返る)。ピクセル換算も data.image で行ってください。
傾き補正(デスキュー)は行いません — ページを回して座標を作り直すことはなく、返る座標は傾いたまま読んだ入力画像の座標系です。傾いた写真では quad がその傾きに沿います。
このセルの判定です。review の鏡なので、二つが食い違うことはありません — false は review に理由が入っているとき(宣言したルール違反も含め、理由の種類を問いません)、true は何も立たず、かつ照合が実際に走ったとき、null は何も立たなかったが照合できるものが無かったとき(幾何のみの項目)です。ゲートに使うのはこの 1 個で足ります。
文字照合そのもの(値がこの座標の OCR 原文と一致したか、2 つの独立エンジンの合意)は evidence.text_match にあります。その不一致には元から専用の理由 text_mismatch があるので、判定から失われるものはありません。
true でも「求めた意味の値である」ことは意味しません。モデルがページ上の別の箇所(近くの補助見出しなど)を拾った場合、座標はその拾った文字に付き、照合も一致し、何も立たなければ true で返ります。座標が答えるのは「この値はどこから来たか」で、「これは正しい項目か」ではありません — そこは label / near / enum の担当です。
null なら通過、値が入っていれば人の確認を推奨。reasons: type_mismatch | out_of_range | pattern_mismatch | near_mismatch | near_ambiguous | near_conflict | nobox | text_mismatch | crop_mismatch | low_ratio | weak_source | low_ocr_confidence | ambiguous_occurrence | overwide_box | missing。reasons は破られたルール全部を ランク順 に並べた配列で、0 番が代表です(長さ 1 でも必ず配列)。前の 6 つは呼び出し側が宣言したルール違反なので、エンジンの推定より上位にランクされます。
near_mismatch と near_ambiguous は別々の質問への答えです: 前者は この値の どの出現も 宣言語のそばに無い(どこにあっても違う値)、後者は そばにある出現は あるが座標が付いたのはそれではない(どの複製を指すか未定 — 値自体は正しいかもしれません)。同じ値が 2 か所に印字される帳票で、正しい値が座標の付き方だけで通ったり落ちたりしていたのを分けたものです(v85)。near_ambiguous が立つとき ambiguous_occurrence は併記しません — 同じ事実を二つの語彙で言うことになるからです。
画面を作る場合は、ここに並んだ 全種(と今後追加され得るコード)に表示を割り当て、1 セルに複数の理由が同時に立つ配列を前提にしてください — 一部だけ対応すると未対応コードで表示が壊れます。未知のコードは汎用の「要確認」に落とすのが安全です。
{
"status": "success",
"data": {
"values": {
"text": "株式会社サクラ商事\n請求書\n合計 1,451円",
"blocks": [
{ "text": "株式会社サクラ商事" }
]
},
"cells": {
"blocks[0]": { "box": { "xmin": 60, "ymin": 48, "xmax": 470, "ymax": 92 },
"quad": [{"x":60,"y":48},{"x":470,"y":48},{"x":470,"y":92},{"x":60,"y":92}],
"verified": true, "review": null,
"evidence": { "text_match": true, "source": "token_id", "ocr_confidence": 0.98 } }
},
"review": {
"unit": "block",
"total": 12,
"boxed": 12,
"verified": 11,
"flagged": [{ "path": "blocks[7]", "reasons": ["text_mismatch"] }],
"by_reason": { "text_mismatch": 1 },
"coverage": { "recovered_blocks": 0, "vision_tokens": 96, "tokens_claimed": 96, "token_coverage": 1.0 }
},
"image": { "width": 1654, "height": 2339 },
"source": "llm"
}
}ツリー閲覧
クエリパラメータ
curl https://api.space-ocr.com/space?path=/&depth=1 \
-H "Authorization: Bearer YOUR_API_KEY"レスポンスフィールド
{
"path": "/",
"depth": 1,
"items": [
{ "path": "/invoices", "name": "invoices", "type": "folder", "createdAt": 1716700000000 },
{ "path": "/memo_2024", "name": "メモ", "type": "memo",
"uniqueKey": "...", "createdAt": 1716700000000, "extensions": null }
]
}
// type: folder | sheet | doc | memo | img。folder 以外は uniqueKey / extensions を持ちます。中身を見る
フォルダ/シート/ドキュメント束/メモ/画像、すべての種類の中身を返します。ドキュメント束は pages 配列、シートは rows 配列です。クエリ(where / sort / select / limit / offset / boxes)はシート専用 — 他の種類では無視され、全件がそのまま返ります。
シートの行は アップロード時刻(createdAt)の昇順で返ります。これは POST /edit・POST /remove の row: N と同じ順番なので、レスポンスの N 番目の行がそのまま row: N です。
クエリパラメータ
# 複数 where (AND) + ソート + 投影 + ページネーション
curl "https://api.space-ocr.com/view?path=/invoices/sheet1\
&where=total>=10000\
&where=vendor~ABC\
&sort=-invoice_date\
&select=vendor,total,invoice_date\
&limit=20&offset=0" \
-H "Authorization: Bearer YOUR_API_KEY"レスポンスフィールド
// type=sheet
{
"type": "sheet",
"path": "/invoices/sheet1",
"name": "sheet1",
"columns": [ /* ... */ ],
"total": 128, // シート全体の行数
"matched": 12, // where 通過行数
"offset": 0,
"limit": 20,
"nextOffset": 20, // 次ページ用 / 終端は null
"rows": [
{
"rowKey": "img_abc",
"name": "invoice_2025_04_10.jpg",
"createdAt": 1744243200000, // アップロード時刻 / 既定の行順
"imageUrl": "https://...",
"ocrStatus": "done",
"values": { "vendor": "ABC Corp", "total": "12000", "invoice_date": "2025-04-10" },
"cells": { /* POST /ocr/fields と同じ box / quad / verified / review — boxes=0 で省略 */ },
"review": { "unit": "field" /* ... */ },
"image": { "width": 1654, "height": 2339 }
}
]
}
// type=folder
{
"type": "folder",
"path": "/invoices",
"items": [
{ "path": "/invoices/2024", "name": "2024", "type": "folder" },
{ "path": "/invoices/Kvho45OXMKw…", "name": "sheet1", "type": "sheet", "uniqueKey": "Kvho45OXMKw…" }
]
}
// type=doc — ページ全件がそのまま返ります(where / sort / limit / offset / select / boxes は無視)。
// type=doc (mode=markdown)
{
"type": "doc",
"path": "/reports/quarterly",
"name": "quarterly",
"mode": "markdown",
"total": 2,
"pages": [
{
"pageKey": "img_abc",
"name": "page1.jpg",
"imageUrl": "https://...",
"ocrStatus": "done",
"values": { "markdown": "# ...", "elements": [ /* ... */ ] },
"cells": { /* POST /ocr/markdown と同じ box / quad / verified / review */ },
"review": { "unit": "element" /* ... */ },
"image": { "width": 1654, "height": 2339 }
}
]
}
// type=doc (mode=text)
{
"type": "doc",
"path": "/notes/scan",
"name": "scan",
"mode": "text",
"total": 1,
"pages": [
{
"pageKey": "img_def",
"name": "note.jpg",
"imageUrl": "https://...",
"ocrStatus": "done",
"values": { "text": "...", "blocks": [ /* ... */ ] },
"cells": { /* POST /ocr/text と同じ box / quad / verified / review */ },
"review": { "unit": "block" /* ... */ },
"image": { "width": 1654, "height": 2339 }
}
]
}
// type=memo
{ "type": "memo", "path": "...", "name": "todo", "text": "..." }
// type=img
{ "type": "img", "path": "...", "name": "...", "imageUrl": "...", "ocrStatus": "done" }作成
ボディパラメータ
# sheet
curl -X POST https://api.space-ocr.com/create \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"path": "/invoices",
"type": "sheet",
"name": "sheet1",
"columns": [
{ "id": "amount", "name": "amount", "type": "string", "required": true },
{ "id": "date", "name": "date", "type": "string" }
],
"prompt": "請求書から金額と日付を抽出"
}'レスポンスフィールド
// HTTP 201 Created
// sheet/memo は uniqueKey が path に組み込まれて返却される
{ "path": "/invoices/Kvho45OXMKw…", "type": "sheet", "uniqueKey": "Kvho45OXMKw…" }
// 作成と同時に item.created Webhook が発火します。Idempotency-Key ヘッダーを
// 付けると 24h 以内の再送は同じレスポンスをそのまま返します。
// required: true の列(上の amount)は、以後のアップロードで値が空だと
// その行の review.flagged に reason "missing" として現れます。画像アップロード
フォームフィールド (multipart)
curl -X POST https://api.space-ocr.com/upload \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "path=/invoices/sheet1" \
-F "files=@invoice1.jpg" \
-F "files=@invoice2.jpg"レスポンスフィールド
// async (default)
{
"path": "/invoices/sheet1",
"jobs": [
{ "uniqueKey": "...", "originalName": "invoice1.jpg", "jobId": "job_...", "status": "pending" },
{ "uniqueKey": "...", "originalName": "invoice2.jpg", "jobId": "job_...", "status": "pending" }
]
}
// ドキュメント束にアップロードした場合 — jobs の形は同じで、束の mode で変換方法が決まります
{
"path": "/reports/quarterly",
"jobs": [
{ "uniqueKey": "...", "originalName": "page1.jpg", "jobId": "job_...", "status": "pending" }
]
}
// wait=true — jobs ではなく results が返ります
{
"path": "/invoices/sheet1",
"results": [
{ "uniqueKey": "...", "originalName": "invoice1.jpg", "jobId": "job_...",
"status": "done", "mode": "sheet",
"result": { /* { values, cells, review, image } — GET /jobs と同じ v2 構造 */ } },
{ "uniqueKey": "...", "originalName": "invoice2.jpg", "jobId": "job_...",
"status": "pending" } // 30s 以内に終わらなかった分は /jobs でポーリング
]
}
// 402 — 残高不足
{
"error": { "code": "insufficient_balance", "message": "...", "requestId": "req_..." },
"details": {
"requested": 5,
"processable": 3,
"breakdown": {
"freeRemaining": 0,
"flatfeeRemaining": 3,
"balance": 0,
"perCallCost": 1,
"currency": "scans"
}
}
}シート行/メモを編集
ボディパラメータ
# sheet
curl -X POST https://api.space-ocr.com/edit \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"path":"/invoices/sheet1","row":"img_abc","column":"amount","value":"12000"}'
# memo
curl -X POST https://api.space-ocr.com/edit \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"path":"/todo","text":"新しい本文"}'レスポンスフィールド
{ "ok": true, "patched": { "row": "img_abc", "column": "amount", "value": "12000" } }削除(cascade)
ボディパラメータ
curl -X POST https://api.space-ocr.com/remove \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"path":"/invoices/2024"}'レスポンスフィールド
{ "ok": true }OCR ジョブのポーリング
パスパラメータ
curl https://api.space-ocr.com/jobs/job_xxx \
-H "Authorization: Bearer YOUR_API_KEY"レスポンスフィールド
{
"jobId": "job_xxx",
"status": "done",
"uniqueKey": "img_abc",
"path": "/invoices/sheet1/img_abc",
"sheetRef": "Kvho45OXMKw…",
"docRef": null,
"mode": "sheet",
"result": {
"values": { "amount": "12000", "date": "2025-04-10" },
"cells": { /* POST /ocr/fields と同じ box / quad / verified / review */ },
"review": { "unit": "field" /* ... */ },
"image": { "width": 1654, "height": 2339 }
}
}残高・無料枠
curl https://api.space-ocr.com/amount \
-H "Authorization: Bearer YOUR_API_KEY"レスポンスフィールド
チャージ済み残高。単位は通貨ではなくスキャン数です。
消費は 無料枠 → 定額プラン → 残高 の順で、無料枠が残っている間この値は減りません。残量表示を作る場合は free.remaining と balance の両方を見せてください — balance だけだと、無料枠を使っている間ずっと減らない数字になります。
{
"free": { // 毎月の無料枠
"used": 12,
"limit": 100,
"remaining": 88,
"cycleStart": 1716700000000,
"cycleEnd": 1719378400000
},
"flatfee": { // 定額プラン(未加入なら enabled:false)
"enabled": true,
"used": 340,
"limit": 3000,
"remaining": 2660,
"cycleStart": 1716700000000,
"cycleEnd": 1719378400000,
"nextBillingAt": 1719378400000,
"interval": "monthly",
"renewal": true,
"plan": "pro"
},
"balance": 1240, // チャージ済み残高(スキャン数)
"currency": "scans", // 残高の単位は通貨ではなくスキャン数
"perCallCost": 1 // 1 スキャン = 1 コール
}
// 処理可能枚数 = free.remaining + (flatfee.enabled ? flatfee.remaining : 0) + balance。
// この順で消費されます(無料枠 → 定額 → 残高)。ヘルスチェック
curl https://api.space-ocr.com/healthレスポンスフィールド
この API 自体の変更履歴です。新しい順、エントリごとに version・date・changes[]。
engine.changelog と紛らわしいので一度だけ整理させてください。最上位の changelog は version(v2.x — エンドポイント・パラメータ・レスポンスキーの契約)と対になり、engine.changelog は engine.version(vNN — 読み取りの中身)と対になります。軸が別なので、片方だけが動くことがあります。
{
"status": "ok",
"version": "v2.8",
"time": 1787298502910,
"changelog": [
{ "version": "v2.8", "date": "2026-08-21",
"changes": ["Field extraction responses carry a new evidence key, `printed_text` …", "…"] }
],
"engine": {
"version": "v83",
"build": { "sha": "52b9a92", "deployed_at": "2026-08-21T06:59:16Z" },
"changelog": [
{ "version": "v83", "date": "2026-08-21",
"changes": ["New evidence key `cells[path].evidence.printed_text` …", "…"] }
]
}
}概要
イベント
ペイロード例 — ocr.completed
{
"event": "ocr.completed",
"deliveryId": "dlv_xxx",
"occurredAt": 1716700000000,
"apiVersion": "v2.7",
"data": {
"uid": "...",
"path": "/invoices/sheet1/img_abc",
"parentPath": "/invoices/sheet1",
"uniqueKey": "img_abc",
"sheetRef": "sht_xxx",
"docRef": null,
"mode": "sheet",
"result": {
"values": { "amount": "12000", "date": "2025-04-10" },
"cells": { /* box / quad / verified / review */ },
"review": { "unit": "field" /* ... */ },
"image": { "width": 1654, "height": 2339 }
}
}
}配信ヘッダー
X-Spaceocr-Signature: t=<unix_ms>,v1=<hex>
X-Spaceocr-Timestamp: <unix_ms>
X-Spaceocr-Event: ocr.completed
X-Spaceocr-Delivery: dlv_<id>
Content-Type: application/json署名検証
import crypto from "crypto";
export function verify(secret, headers, rawBody) {
const sig = headers["x-spaceocr-signature"] || "";
const m = sig.match(/^t=(\d+),v1=([a-f0-9]+)$/);
if (!m) return false;
const [, t, v1] = m;
if (Math.abs(Date.now() - Number(t)) > 5 * 60 * 1000) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(v1, "hex"),
);
}再送ポリシー
現在の Webhook 設定
curl https://api.space-ocr.com/webhook \
-H "Authorization: Bearer YOUR_API_KEY"レスポンスフィールド
{
"configured": true,
"url": "https://example.com/hooks/space-ocr",
"active": true,
"secretMasked": "••••a1b2",
"createdAt": 1716700000000,
"updatedAt": 1716700000000
}
// 未設定のとき
{ "configured": false }Webhook 設定の作成・更新
ボディパラメータ
curl -X PUT https://api.space-ocr.com/webhook \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/hooks/space-ocr","active":true}'レスポンスフィールド
{
"configured": true,
"url": "https://example.com/hooks/space-ocr",
"active": true,
"secretMasked": "••••a1b2",
"secret": "kJ8s…", // 新規発行・回転時のみ、この一度だけ平文
"createdAt": 1716700000000,
"updatedAt": 1716700000000
}Webhook 設定の削除
curl -X DELETE https://api.space-ocr.com/webhook \
-H "Authorization: Bearer YOUR_API_KEY"レスポンスフィールド
{ "ok": true }テストイベントを送信
curl -X POST https://api.space-ocr.com/webhook/test \
-H "Authorization: Bearer YOUR_API_KEY"レスポンスフィールド
{ "ok": true, "deliveryId": "dlv_xxx" }最近の配信履歴
クエリパラメータ
curl https://api.space-ocr.com/webhooks/deliveries \
-H "Authorization: Bearer YOUR_API_KEY"レスポンスフィールド
{
"items": [
{
"deliveryId": "dlv_xxx",
"event": "ocr.completed",
"url": "https://example.com/hooks/space-ocr",
"path": "/invoices/sheet1/img_abc",
"uniqueKey": "img_abc",
"status": "success", // pending | success | dead
"attempts": 1, // 試行回数
"lastAttempt": {
"at": 1716700000000,
"attemptIndex": 0,
"responseStatus": 200,
"error": null,
"durationMs": 143,
"responsePreview": "ok"
},
"occurredAt": 1716700000000,
"nextAttemptAt": null,
"completedAt": 1716700000143
}
]
}配信の詳細
パスパラメータ
curl https://api.space-ocr.com/webhooks/deliveries/dlv_xxx \
-H "Authorization: Bearer YOUR_API_KEY"レスポンスフィールド
{
"deliveryId": "dlv_xxx",
"event": "ocr.completed",
"occurredAt": 1716700000000,
"payload": { /* full event body */ },
"attempts": [
{ "at": 1716700000000, "responseStatus": 200, "ok": true }
]
}配信の手動再送
パスパラメータ
curl -X POST https://api.space-ocr.com/webhooks/deliveries/dlv_xxx/redeliver \
-H "Authorization: Bearer YOUR_API_KEY"レスポンスフィールド
{ "ok": true, "deliveryId": "dlv_xxx" }
// 同じ deliveryId を再利用します(新しい ID は発行されません)。配信ログの
// status は pending に戻り、attempts に試行が追記されます。概要
接続
# Claude Code
claude mcp add --transport http space-ocr https://mcp.space-ocr.com/mcp \
--header "Authorization: Bearer YOUR_API_KEY"Cursor / VS Code / Windsurf (mcp.json)
{
"mcpServers": {
"space-ocr": {
"url": "https://mcp.space-ocr.com/mcp",
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
}
}
}