發票 OCR API/送貨單 OCR 轉 CSV ── 發票資料擷取 API 實作指南
讓開發者擺脫發票、送貨單手動輸入與 Excel 亂碼的指南。把影像 POST 到 /ocr/fields,即可取得結構化的客戶、日期、合計與明細,每個值都附帶原始影像座標(bbox)與 match_ratio。內含 curl、Python 程式碼、CSV 輸出、Webhook 與收費說明。
你是不是還在用手把發票、送貨單一格一格敲進 Excel?日期、客戶、未稅、含稅,還有明細的每一行 ── 一到月底就得對著堆積如山的紙張,把數字一格一格謄寫過去。中途差了一位數,合計兜不攏,又得從頭核對一遍。那段時間,真的很想省下來。
想把掃描好的 PDF 複製出來,卻發現文字根本選不起來。丟去 OCR 跑一遍,明細又全部擠進同一個儲存格,換行和欄位都不見了。CSV 用 Excel 一打開又亂碼,品名完全看不懂。明明只是想匯進會計軟體,卻每次都卡在這道關卡前 ── 這就是天天和文件打交道的人最熟悉的「日常」。
這篇文章,就是要教開發者怎麼把這些作業換成一支 API。把發票、送貨單的影像 POST 到 POST /ocr/fields,就能拿到客戶、日期、合計這些欄位,連明細的每一行都會以帶型別的結構化資料回傳。而且回傳的每一個值,都附帶它是從原始影像哪個位置讀出來的座標(bbox),所以你不必照單全收,可以拿去和原稿逐一比對驗證。文中附上 curl 與 Python 程式碼,從最短路徑一路看到正式上線運維。
先動手玩玩看 ── 免上傳,10 秒體驗
寫程式之前,先看看實際的輸出。下面是一張真實收據的解析結果。把游標移到欄位上,就會highlight出這個值是從影像的哪裡讀出來的,還能看到每個欄位的符合率(match_ratio)。發票、送貨單的行為完全一樣 ── 擷取出的每一個值,都會對應到讀取來源的像素。

Every value carries a verified on-page location — bbox + 4-point vertices + 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.
「原始影像 → 擷取表單 → 標示對應位置 → 輸出 CSV」的流程
space ocr 的用法,歸結起來就是 4 個步驟。(1) 丟進收據、發票、送貨單的影像 → (2) 在欄位固定的表單裡,以 1 張=1 列的方式擷取出來 → (3) 點一下值,原始影像的對應位置就會亮起,方便和原稿核對 → (4) 直接輸出成 CSV 匯進會計軟體。先從丟一張進去、看欄位被自動填滿開始吧。
驗證與基礎 URL
公開 API 只有 https://api.space-ocr.com 這一個基礎位址 ── 沒有 /v1 這類的路徑版本管理。每一次請求,都用以 spocr_ 開頭的金鑰、透過 HTTP Bearer token 進行驗證。
Authorization: Bearer spocr_xxxxxxxxxxxxxxxx
標頭缺漏或不正確會回傳 401,未註冊的金鑰則回傳 403。所有回應都會帶上 X-Request-Id(格式 req_xxx)標頭,記進 log 裡,日後向客服詢問時會比較安心。如果想自動產生用戶端,GET /openapi.json 提供了 OpenAPI 3.1 的規格。
最短路徑 ── 內建的發票、送貨單範本
最快的方式,是在 templateId 指定內建範本。發票用 templateId: "invoice",送貨單用 templateId: "delivery"。「發票會有哪些欄位」引擎這邊已經知道,所以你不必自己一個一個定義欄位。影像可以用 URL,也可以用純 base64 傳入(imageType 會依有沒有 http(s):// 前綴自動判斷)。
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/docs/delivery-0831.jpg",
"imageType": "url",
"templateId": "delivery"
}'參數的正式名稱是駝峰式命名。 請使用 imageType / templateId / autoFields。舊有的蛇形命名(image_type / template_id / auto_fields)雖然也能運作,但已不建議使用。新的程式碼請優先採用駝峰式命名。另外,發票用 templateId: "invoice",送貨單用 templateId: "delivery"。
回應的格式 ── 每個值都附帶「出處」
成功時會回傳 { status: "success", data: { ... } }。擷取出的每個值都各自帶有出處資訊,各欄位的座標則統整在 field_bboxes map 裡。
bbox──{ xmin, ymin, xmax, ymax }的軸對齊矩形。是在正規化為 0〜1000 的網格上的整數座標(0,0 為左上,1000,1000 為右下)。和影像的像素尺寸無關。換算成像素為pixel_x = bbox_x / 1000 × image_width。vertices── 依左上 → 右上 → 右下 → 左下順序排列的 4 個點{x, y}。是沿著文件傾斜方向的**支援旋轉(oriented)**矩形,所以即使是斜著拍的手機照片也能漂亮地框住。match_ratio── 該值的字元中,實際在頁面上找到的比例(0〜1)。0.85 以上視為確信符合,1.0代表所有字元都在頁面上找到了。bbox_source── 座標導出方式的標籤。vision_symbol_match(字元符合度達 0.85 以上而落地的一般路徑 ── 會伴隨實際的match_ratio)、token_id/token_id_hybrid(用 LLM 回傳的 word token 提示去查 Vision 的詞 token 的路徑)、low_confidence(字元符合度低於 0.85 ── 需確認)、shared_value(從合併儲存格傳播而來)。
{
"status": "success",
"data": {
"total": "2,045",
"field_bboxes": {
"total": {
"bbox": { "xmin": 595, "ymin": 974, "xmax": 781, "ymax": 1000 },
"vertices": [
{ "x": 594, "y": 975 }, { "x": 781, "y": 972 },
{ "x": 781, "y": 998 }, { "x": 595, "y": 1000 }
],
"match_ratio": 0.93,
"bbox_source": "vision_symbol_match"
}
}
}
}座標並不是照單全收 AI 的說法。 語言模型回傳的只有各值的文字,以及它用到的 word token 的提示(wid),並不會回傳座標本身。引擎會先把那段文字,拿去和 Vision OCR 在頁面上實際偵測到的符號逐字比對 ── 所以矩形會落在那些字元真正被找到的像素上,而每個值也都會附上代表「符合了多少」的 match_ratio。當 LLM 回傳 word token 的提示時,引擎也可能用該 token 的座標去覆寫部分欄位,但這個提示有可能夾帶雜訊(在重複的列裡和鄰列搞混,也就是所謂 stochastic 的擺動),所以不會盲信,而是先用欄、列的一致性驗證、修正後才採用。重點不在於「AI 不會出錯」,而在於每一個值都會重新比對回頁面,並留下符合了多少的分數。詳情請參閱用邊界框讓 OCR 可稽核的機制。
當範本不夠用時 ── 自訂欄位
實務上的發票,常有通用範本沒有命名的欄位 ── 訂單號、付款條件代碼、專案標籤等等。這種時候,就用 FieldSpec 的陣列 fields 來取代 templateId(或兩者併用)。每一個 FieldSpec 是 { name, type, description?, children? }。如果同時送了 fields 和 templateId,會以 fields 為優先。
description 就是引導模型的地方 ── 你可以用中文的指示句寫明要抓什麼、怎麼抓。而 type: "array" 搭配 children 的組合,正是抽出重複明細列的方法。只要定義一個子 schema,無論有幾列都會回傳。
import requests, base64, csv
with open("delivery.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": "送貨方(客戶)的公司名稱"},
{"name": "delivery_no", "type": "string",
"description": "送貨單號。保留原文"},
{"name": "delivery_date", "type": "string",
"description": "送貨日期。日本年號/西元維持原文"},
{"name": "total", "type": "string",
"description": "合計金額。保留千分位逗號"},
{"name": "items", "type": "array",
"description": "明細每一列對應一個元素",
"children": [
{"name": "name", "type": "string", "description": "品名"},
{"name": "qty", "type": "number", "description": "數量"},
{"name": "unit_price", "type": "number", "description": "單價"},
]},
],
},
timeout=60,
)
data = resp.json()["data"]
# 把明細寫進 CSV。為了 Excel 與 CJK,用 UTF-8 BOM 輸出
with open("delivery.csv", "w", encoding="utf-8-sig", newline="") as out:
w = csv.writer(out)
w.writerow(["品名", "數量", "單價"])
for row in data.get("items", []):
w.writerow([row["name"], row["qty"], row["unit_price"]])值會原文(verbatim)保留。 合計 7,855 會以字串 "7,855" 回傳,千分位逗號、小數點、全形字元也都原封不動。只有在你用 description 明確要求時才會做正規化。UI 上看到的 ¥ 是裝飾,並不是值的一部分。對付 CSV 亂碼的訣竅是:要用 Excel 開的 CSV,請以 UTF-8 BOM(utf-8-sig)輸出 ── 上面的程式碼就是這麼做的。另外,防止明細「擠成一格」的關鍵就是 type: "array" + children,靠它把 1 筆明細展開成 1 列。
點一下文字,跳到原始位置
累積進表單之後,點一下值,原始影像的對應位置就會亮起。這是批次抽查時最快的方法 ── 不必把整份文件看一遍,視線會直接飛到對應位置。也可以採取只優先確認 match_ratio 偏低的欄位這種運維方式。
非同步大量處理 ── 批次上傳、Job、Webhook
POST /ocr/fields 是同步的,最適合放在 request/response 迴圈裡的單張處理。如果要整批處理一整個發票、送貨單的資料夾,就對表單用 POST /upload(重複 multipart 的 files)丟進去。預設會立刻回傳 job 陣列。
{ "path": "...", "jobs": [ { "uniqueKey": "...", "jobId": "...", "status": "pending" } ] }
取得結果有兩種方式。輪詢 GET /jobs/{jobId},或是註冊 Webhook。Webhook 每個 space 一個 URL,所有事件都會用 X-Spaceocr-Signature 標頭做 HMAC-SHA256 簽章。值得關注的事件有 upload.received、item.created、ocr.completed(擷取結果在 data.result)、ocr.failed。在信任 payload 之前,請務必驗證簽章。
冪等性、請求追蹤、速率限制
為了讓正式環境的 pipeline 能安全地重試,有幾個標頭可以用。
| 標頭 | 作用 |
|---|---|
Idempotency-Key | 在 /upload 與 /create,相同金鑰的重送會重播 24 小時內的快取回應(X-Idempotent-Replay: true)── 不會重複計費,可安心重試。 |
X-Request-Id | 所有回應都會帶(req_xxx)。記進 log 供客服使用。 |
速率限制為每把金鑰 60 次/分、每個 uid 600 次/分(固定 60 秒視窗)。超過時會回傳 429 與 error.code: "rate_limited"。要等待的秒數放在 JSON body 的 details.retryAfterSec ── 不是 Retry-After HTTP 標頭。請依 body 裡的值來做退避(backoff)。
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded",
"requestId": "req_8fa2c1"
},
"details": { "retryAfterSec": 12 }
}從擷取,到可查詢的表單
把發票擷取進表單之後,要回頭讀取並不需要重跑 OCR。GET /view 會對已累積的列執行伺服器端查詢 ── where、sort、select、limit、offset。不重跑 OCR,也不計費。座標預設會一併回傳,只有想讓回應輕一點時才加上 boxes=0。例如用 where=total>=40000 只要高額的發票,用 sort=-invoice_date 依新到舊排序。從這裡再輸出成 CSV(因為是 UTF-8 BOM,用 Excel 與 CJK 也能漂亮地打開),就能拿去匯進會計軟體 ── 詳情請參閱把掃描文件轉成 CSV與把收據轉成 CSV。所有端點的規格則統整在 API 文件裡。
PDF 要先把頁面轉成影像再送。 OCR 引擎直接解析的是點陣影像(JPEG、PNG、GIF、BMP、TIFF、WebP)。如果你直接呼叫 API,請先把 PDF 的每一頁算繪成 PNG 等格式再送(若是拖進 Web 應用程式,頁面影像化會由應用程式自動完成,所以可以直接丟 PDF)。和 freee、マネーフォワード(Money Forward)、弥生(彌生)、kintone 的串接並非官方 API 整合,而是以匯入輸出的 CSV 為前提。此外,是否符合發票(インボイス)制度或電子帳簿保存法等要求,請依各公司的運維與需求自行確認(本服務並不保證滿足法規要求)。
收費
POST /ocr/fields 為一次呼叫 ¥10,POST /upload 為 ¥10 × N 張。失敗不計費 ── OCR 若沒有回傳結果就會退款,502 引擎錯誤或 ocr.failed 事件都會自動退款。唯讀端點(GET /space、/view、/amount、/health)免費。免費額度免信用卡、每月 100 張,Pro 為 $39/月,Business 請洽詢(報價)。
用 API 擷取發票、送貨單的步驟
- 準備 API 金鑰登入後發行以 spocr_ 開頭的 API 金鑰,並在每一次請求加上 Authorization: Bearer spocr_...。基礎 URL 為 https://api.space-ocr.com。
- 準備影像(PDF 要把頁面影像化)把發票、送貨單準備成 JPEG/PNG 等點陣影像。直接呼叫 API 時,PDF 要先把每一頁算繪成 PNG 再送(拖進 Web 應用程式則由應用程式自動影像化)。影像用 URL 或純 base64 傳入,並以 imageType 指定 url / base64。
- 呼叫 POST /ocr/fields發票指定 templateId: "invoice",送貨單指定 templateId: "delivery"。範本不夠的欄位用 fields[](FieldSpec {name,type,description,children})定義,明細用 type:"array" + children 一列一列展開。
- 驗證回應確認回傳的每個值的 bbox、vertices、match_ratio、bbox_source。對 match_ratio 低於 0.85(low_confidence)的欄位,拿去和原稿比對確認。
- 轉成 CSV 匯進會計軟體把擷取結果寫成帶 UTF-8 BOM 的 CSV(明細展開成陣列列),交給 freee、Money Forward(マネーフォワード)、彌生(弥生)等的 CSV 匯入。累積之後可用 GET /view 在不重跑 OCR、不計費的情況下查詢。