청구서 OCR API·납품서 OCR로 CSV 만들기 ── 청구서 데이터 추출 API 구현 가이드
청구서·납품서를 수기 입력과 Excel 깨짐에서 벗어나게 해 주는 개발자용 가이드. 뽑을 항목을 선언해 POST /ocr/fields 에 이미지를 보내면 거래처·날짜·합계·명세가 구조화 데이터로 돌아오고, 값마다 원본 이미지의 좌표(box·quad)와 확인이 필요한 항목을 모은 review 목록이 함께 붙습니다. curl·Python 코드, CSV 출력, Webhook, 요금까지 한 번에.
청구서나 납품서를 아직도 손으로 Excel에 일일이 입력하고 계신가요. 날짜, 거래처, 공급가액·합계, 그리고 명세 한 줄 한 줄 ── 월말이 되면 산더미처럼 쌓인 종이를 노려보며 숫자를 한 셀씩 옮겨 적습니다. 중간에 한 자리가 어긋나서 합계가 안 맞으면, 또 처음부터 대조해야 하죠. 그 시간을, 없애고 싶습니다.
스캔한 PDF를 복사하려고 하면 문자가 선택되지 않습니다. OCR을 돌리면 명세가 전부 한 셀에 뭉개져 줄바꿈도 열도 사라집니다. CSV를 Excel에서 열면 글자가 깨져서 품명을 읽을 수 없습니다. 그저 회계 소프트웨어에 가져오고 싶을 뿐인데, 그 직전 단계에서 매번 발목을 잡힙니다 ── 서류를 다루는 현장이라면 누구나 겪는 일입니다.
이 글은 그 작업을 API 한 번으로 대체하기 위한 개발자용 가이드입니다. POST /ocr/fields 에 청구서·납품서 이미지를 보내면 거래처·날짜·합계 같은 항목과 명세의 각 행이 타입이 붙은 구조화 데이터로 돌아옵니다. 게다가 돌아오는 모든 값에 원본 이미지의 어디에서 읽어 냈는지를 나타내는 좌표(box·quad)가 붙기 때문에, 추출 결과를 그대로 믿지 않고 원본과 대조해 검증할 수 있습니다. curl과 Python 코드와 함께, 최단 경로부터 실제 운영까지 차근차근 살펴보겠습니다.
일단 직접 만져 보기 ── 업로드 불필요, 10초면 체험
코드를 쓰기 전에, 실제 출력을 먼저 보세요. 아래는 진짜 영수증을 해석한 결과입니다. 항목에 커서를 올리면 그 값이 이미지의 어디에서 읽혔는지 하이라이트됩니다. 청구서·납품서도 동작은 완전히 똑같습니다 ── 추출된 값 하나하나가 읽어 낸 픽셀에 연결되고, 대조가 맞지 않은 항목은 data.review.flagged 에 확인 대상으로 올라옵니다.

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.
'원본 이미지 → 추출 시트 → 해당 위치 강조 → CSV 출력' 흐름
space ocr의 사용법은 결국 4단계입니다. (1) 영수증·청구서·납품서 이미지를 보낸다 → (2) 열이 정해진 시트에 1장=1행으로 추출된다 → (3) 값을 클릭하면 원본 이미지의 해당 위치가 켜져 원본과 바로 대조할 수 있다 → (4) 그대로 CSV로 내보내 회계 소프트웨어에 가져온다. 먼저 1장을 보내 항목이 채워지는 모습부터 보시죠.
인증과 베이스 URL
공개 API의 베이스는 https://api.space-ocr.com 하나뿐입니다 ── /v1 같은 경로 버저닝은 없습니다. 각 요청은 spocr_ 로 시작하는 키를 사용한 HTTP Bearer 토큰으로 인증합니다.
Authorization: Bearer spocr_xxxxxxxxxxxxxxxx헤더가 빠졌거나 키가 무효하면 401(error.code: "invalid_api_key")이 돌아옵니다. 403 은 인증 실패가 아니라, 그 키의 권한 밖에 있는 리소스 ── 다른 키가 만든 잡 등 ── 를 건드렸을 때입니다. 모든 응답에 X-Request-Id(형식 req_xxx) 헤더가 붙으니, 지원 문의용으로 로그에 남겨 두면 나중에 도움이 됩니다. 클라이언트를 자동 생성하고 싶다면 GET /openapi.json 에 OpenAPI 사양이 공개되어 있습니다.
최단 경로 ── 뽑을 항목을 선언한다
fields 에 뽑고 싶은 항목을 FieldSpec 배열로 선언합니다 ── 청구서면 거래처명·청구일·전표번호·합계, 납품서면 납품일·품명·수량·단가. name 이 그대로 응답 JSON의 키가 되므로, 사내 테이블 정의를 그대로 옮겨 적으면 됩니다. 어떤 항목이 실려 있는지 모르는 서식을 시험할 때는 autoFields: true 로 스키마 자체를 제안받을 수도 있습니다. 이미지는 URL로도 순수 base64로도 넘길 수 있고, 둘 중 어느 쪽인지는 imageType 으로 명시합니다.
선언은 추출 자체를 바꾸지 않습니다. type(number / integer / date)·pattern·min / max·required 는 모델에 전달되지 않으므로, values 는 선언하든 안 하든 같은 읽기가 돌아옵니다. 선언이 만드는 것은 두 가지입니다 ── 같은 읽기를 그 타입으로 해석한 data.normalized 층, 그리고 규칙을 어긴 항목에 서는 검토 사유(type_mismatch·out_of_range·pattern_mismatch·missing). 명세처럼 반복되는 행은 행 수를 세지 말고 type: "array" 와 children 으로 한 행의 형태만 선언합니다 ── 몇 행이 돌아올지는 페이지가 정하고, 자식의 좌표는 행 단위로 풀리기 때문에 모든 행에 같은 '수량'·'금액' 열 제목이 반복돼도 서로 헷갈리지 않습니다.
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",
"fields": [
{ "name": "customer", "type": "string",
"description": "거래처명(납품처)",
"near": ["御中", "様"],
"not_near": ["登録番号", "TEL", "〒"] },
{ "name": "delivery_no", "type": "string", "required": true,
"pattern": "^[A-Z]{2}-[0-9]{4,8}$",
"description": "전표 번호" },
{ "name": "delivery_date", "type": "date",
"label": "納品日", "description": "납품일" },
{ "name": "items", "type": "array",
"description": "명세 1행당 1요소",
"children": [
{ "name": "name", "type": "string" },
{ "name": "qty", "type": "integer" },
{ "name": "unit_price", "type": "number" },
{ "name": "amount", "type": "number" }
] },
{ "name": "total", "type": "number", "required": true,
"label": "合計", "description": "합계 금액" }
]
}'바디 파라미터는 카멜케이스가 정식 이름입니다. imageType / autoFields 를 사용하세요. 기존 스네이크케이스(image_type / auto_fields)도 동작하지만 비권장입니다. imageType 은 필수이고 "url" 이나 "base64" 를 반드시 명시해야 합니다 ── 값의 형태를 보고 자동으로 판정하지 않습니다. 한편 fields 안의 속성명(required·label·pattern·near·not_near 등)은 스키마 쪽 이름이므로, API 문서의 FieldSpec 표에 적힌 그대로 써 주세요.
응답의 형태 ── 값마다 '출처'가 붙는다
성공하면 { status: "success", data: { ... } } 가 돌아옵니다. data 는 층으로 갈려 있어 업무 데이터와 검증 정보가 섞이지 않습니다.
data.values── 선언한 스키마 그대로의 순수 업무 데이터. 예약 키가 섞이지 않으므로 그대로 DB에 넣을 수 있습니다.data.cells── path를 키로 쓰는 flat 좌표·검증 맵.total이나items[0].amount같은 path로 조회하면 그 값의box({ xmin, ymin, xmax, ymax }축 정렬 사각형)·quad(서류 기울기를 따라가는 4점)·verified(판정)·review(검토 사유)·evidence(대조 증적)가 들어 있습니다. 좌표는 0~1000으로 정규화된 정수이고, 환산 기준은 보낸 파일이 아니라data.image입니다 ──pixel_x = box.xmin / 1000 × data.image.width.data.review── 문서 한 장 분의 집계와, 확인이 필요한 항목의 작업 목록flagged.{ path, reasons }배열이고path는cells의 키와 같은 문법이라 바로 조회됩니다. 건수는flagged.length입니다.data.normalized── 스칼라 타입이나pattern/enum을 선언했을 때만 붙는 층.values와 같은 모양의 트리이고 리프만 그 타입으로 해석된 값입니다.data.image── 실제로 읽어 낸 페이지의width/height(px). EXIF 방향을 반영한 뒤의 값이므로, 좌표를 이미지 위에 겹칠 때는 이쪽을 기준으로 삼습니다.
evidence 에 실리는 text_match(문자 대조가 통과했는지)와 match_ratio(그 값의 문자 중 페이지에서 찾아낸 비율)는 판정을 뒷받침하는 증거입니다. 직접 임계값을 정해 전 항목을 훑는 것보다, review.flagged 를 그대로 작업 목록으로 받는 편이 확실합니다.
{
"status": "success",
"data": {
"values": {
"customer": "株式会社サンプル商事",
"delivery_no": "DN-100482",
"delivery_date": "令和8年8月31日",
"items": [
{ "name": "A4 복사용지", "qty": "5", "unit_price": "480", "amount": "2,400" }
],
"total": "2,400"
},
"cells": {
"customer": { "box": { "xmin": 62, "ymin": 118, "xmax": 384, "ymax": 152 },
"quad": [{"x":62,"y":118},{"x":384,"y":118},{"x":384,"y":152},{"x":62,"y":152}],
"verified": false,
"review": { "reasons": ["near_conflict"] },
"evidence": { "text_match": true, "source": "vision_symbol_match",
"match_ratio": 1.0,
"not_near": { "matched": "登録番号", "distance": 0.4 } } },
"delivery_date": { "box": { "xmin": 612, "ymin": 96, "xmax": 812, "ymax": 124 },
"quad": [{"x":612,"y":96},{"x":812,"y":96},{"x":812,"y":124},{"x":612,"y":124}],
"verified": true, "review": null,
"evidence": { "text_match": true, "source": "vision_symbol_match", "match_ratio": 1.0 },
"normalized": { "value": "2026-08-31", "type": "date", "method": "deterministic" } },
"items[0].qty": { "box": { "xmin": 512, "ymin": 470, "xmax": 536, "ymax": 496 },
"quad": [{"x":512,"y":470},{"x":536,"y":470},{"x":536,"y":496},{"x":512,"y":496}],
"verified": true, "review": null,
"evidence": { "text_match": true, "source": "token_id", "match_ratio": 1.0 },
"normalized": { "value": 5, "type": "integer", "method": "deterministic" } },
"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 },
"normalized": { "value": 2400, "type": "number", "method": "deterministic" } }
},
"review": {
"unit": "field",
"declared": 8,
"returned": 8,
"boxed": 8,
"verified": 7,
"flagged": [
{ "path": "customer", "reasons": ["near_conflict"] }
],
"by_reason": { "near_conflict": 1 }
},
"normalized": {
"delivery_date": "2026-08-31",
"items": [ { "qty": 5, "unit_price": 480, "amount": 2400 } ],
"total": 2400
},
"image": { "width": 1654, "height": 2339 }
}
}좌표는 AI의 말을 그대로 믿지 않습니다. 언어 모델이 돌려주는 것은 각 값의 텍스트뿐이고, 좌표 자체는 돌려주지 않습니다. 엔진은 그 텍스트를 OCR이 페이지 위에서 실제로 검출한 심볼과 한 글자씩 대조합니다 ── 그래서 사각형은 그 문자가 정말로 발견된 픽셀에 안착합니다. 대조가 통과했는지는 evidence.text_match 에, 얼마나 일치했는지는 evidence.match_ratio 에 남습니다. 셀의 verified 는 그 위의 판정이고 review 의 거울입니다 ── 검토 사유가 하나라도 서면 false, 아무것도 안 서고 대조가 돌았으면 true, 행 유니언처럼 대조할 상대가 없으면 null. 그래서 verified: false 와 text_match: true 는 모순이 아니라 "글자는 맞는데 선언한 규칙이 걸렸다"는 정상 조합입니다. 다만 두 엔진이 같은 오독에 합의해 버리면 그 값은 통과할 수 있습니다 ── 출처 검증과 업무 쪽 규칙(required·pattern·enum·near)은 서로를 보완하는 두 층이고, 프로덕션에서는 둘 다 돌립니다. 자세한 내용은 바운딩 박스로 OCR을 감사 가능하게 만드는 구조를 참고하세요.
수신처와 발행처가, 같은 페이지에 나란히 있다
일본 청구서·납품서에서 가장 까다로운 오류는 글자를 잘못 읽은 것이 아닙니다. 글자는 완벽하게 읽었는데, 가져온 자리가 틀린 오류입니다. 종이 한 장에 회사명이 둘 ── 수신처와 발행처 ── 인쇄돼 있고, 반대쪽을 집어도 문자 대조는 일치하므로 verified: true 로 통과합니다. 거래처 마스터를 enum 에 넘겨도 둘 다 등록된 정당한 상호라면 갈라내지 못합니다.
이 층을 맡는 것이 near 와 not_near 입니다. 수신처 회사명에는 값 옆에 인쇄돼 있어야 할 어휘로 near: ["御中", "様"] 를, 옆에 있으면 안 되는 어휘로 not_near: ["登録番号", "TEL", "〒"] 를 선언합니다. 값의 어느 출현도 near 어휘의 이웃에 없으면 near_mismatch, 이웃에 있는 출현은 있는데 좌표가 붙은 것이 다른 사본이면 near_ambiguous, 발행처 블록의 어휘 옆에 앉아 있으면 near_conflict 가 섭니다. 판정의 내역은 cells[path].evidence.near / evidence.not_near 에 그대로 실립니다.
near 는 선언한 어휘가 페이지 어디에도 인쇄돼 있지 않으면 판정을 보류하고, 그 사실을 review.notes 에 issue: "near_unresolved" 로 알립니다 ── '御中' 를 찍지 않는 사무용 폼을 벌하지 않기 위해서입니다. 당사자가 뒤바뀌는 것은 오히려 그런 서식이라, 거기에 닿는 것은 not_near 쪽입니다. 어휘가 인쇄물의 어디에 붙는 것을 인정할지는 match 로 지정하며, 기본값 boundary 외에 suffix(御中·様)·prefix(〒·TEL)·standalone·anywhere 를 고를 수 있습니다 ── 공사명 '中野様邸増築工事' 안의 '様' 가 수신처 판정의 증인이 되지 않도록 하는 지정입니다. 두 선언 모두 모델에는 전달되지 않으므로 추출되는 값은 달라지지 않습니다. 옳은 쪽을 고르게 만드는 장치가 아니라, 틀린 자리에서 읽은 값을 보이게 만드는 장치로 이해하시면 됩니다.
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": "customer", "type": "string",
"description": "거래처명(납품처)",
"near": ["御中", "様"],
"not_near": ["登録番号", "TEL", "〒"]},
{"name": "delivery_no", "type": "string", "required": True,
"pattern": "^[A-Z]{2}-[0-9]{4,8}$",
"description": "전표 번호"},
{"name": "delivery_date", "type": "date",
"label": "納品日", "description": "납품일"},
{"name": "items", "type": "array",
"description": "명세 1행당 1요소",
"children": [
{"name": "name", "type": "string", "description": "품명"},
{"name": "qty", "type": "integer", "description": "수량"},
{"name": "unit_price", "type": "number", "description": "단가"},
{"name": "amount", "type": "number", "description": "금액"},
]},
{"name": "total", "type": "number", "required": True,
"label": "合計", "description": "합계 금액"},
],
},
timeout=200, # 동기 처리 상한은 180초
)
data = resp.json()["data"]
values = data["values"]
norm_items = data.get("normalized", {}).get("items", [])
# 확인이 필요한 항목부터 받는다. 건수는 flagged 의 길이
for flag in data["review"]["flagged"]:
cell = data["cells"].get(flag["path"])
print(flag["path"], flag["reasons"], cell["box"] if cell else None)
# 명세를 CSV 로. 표시하는 열은 values, 계산하는 열은 normalized
with open("delivery.csv", "w", encoding="utf-8-sig", newline="") as out:
w = csv.writer(out)
w.writerow(["품명", "수량", "단가", "금액", "금액(수치)"])
for i, row in enumerate(values.get("items", [])):
n = norm_items[i] if i < len(norm_items) else {}
w.writerow([row["name"], row["qty"], row["unit_price"],
row["amount"], n.get("amount")])values 는 모델이 페이지에서 읽은 문자열이고, 바이트 단위 복사본이 아닙니다. 문자 대조는 전각·괄호·공백을 접고 비교하므로 (税抜) 가 (税抜) 로 바뀌는 정도의 표기 차이는 통과합니다 ── 정확 일치로 대조하실 거면 그 좌표에서 OCR이 읽은 글자인 cells[path].evidence.printed_text 를 쓰세요. 숫자나 날짜로 다루고 싶을 때는 values 를 고쳐 쓰는 대신 type 을 선언해 data.normalized 를 받습니다("令和8年8月31日" → "2026-08-31", "2,400" → 2400). 해석은 결정론적이라 추가 모델 호출이 없습니다. 해석하지 못한 리프는 normalized 에서 null 이 되고, 이유는 cells[path].normalized.error 에 들어갑니다. 그래서 CSV 에서는 표시하는 열은 values, 계산하는 열은 normalized 로 나누는 편이 안전합니다.
반대로, 수량 칸의 '一式' 이나 지불기한의 '翌月末払い' 처럼 값이 아닌 표기가 정식으로 인쇄되는 항목에 타입을 선언하면, 서류로서는 맞는데도 매번 type_mismatch 로 확인 목록에 올라옵니다 ── 늘 숫자·날짜가 들어오는 항목에만 타입을 선언하세요. CSV 글자 깨짐 대책으로, Excel에서 여는 CSV는 UTF-8 BOM(utf-8-sig)으로 내보냅니다. 값에는 반각 ¥(U+00A5) 같은 문자가 그대로 실리므로 cp932 / Shift_JIS 로 변환하지 말고 UTF-8 그대로 다뤄 주세요. 명세가 '한 셀에 뭉개지는' 것을 막는 열쇠는 type: "array" + children 이며, 이로써 명세 1건=1행으로 전개됩니다.
문자를 클릭해, 원본 위치로 점프
시트에 쌓은 뒤에는 값을 클릭하면 원본 이미지의 해당 위치가 환하게 켜집니다. 이 방식이 배치 처리의 스폿 체크에서 가장 빠릅니다 ── 서류 전체를 훑어보는 대신, 시선이 그대로 해당 위치로 날아갑니다. 전 항목을 비교할 필요는 없습니다. data.review.flagged 에 오른 항목 ── 글자가 맞지 않았거나, 선언한 규칙을 어겼거나, 필수인데 돌아오지 않은 값 ── 만 열면 확인할 자리는 cells[path] 의 좌표가 그대로 가리켜 줍니다.
비동기로 대량 처리 ── 배치 업로드·잡·Webhook
POST /ocr/fields 는 동기 방식이라, 요청/응답 루프에 두는 1장 처리에 가장 적합합니다. 청구서·납품서 폴더를 한꺼번에 처리하려면, 시트에 대해 POST /upload(multipart의 files 를 반복)로 보냅니다. 기본적으로 즉시 잡 배열이 돌아옵니다.
{ "path": "...", "jobs": [ { "uniqueKey": "...", "jobId": "...", "status": "pending" } ] }결과를 받는 방법은 두 가지입니다. GET /jobs/{jobId} 를 폴링하거나, Webhook을 등록합니다. Webhook은 스페이스마다 1개 URL이고, 모든 이벤트가 X-Spaceocr-Signature 헤더로 HMAC-SHA256 서명됩니다. 주목할 이벤트는 upload.received·item.created·ocr.completed(data.result 에 추출 결과)·ocr.failed. 페이로드를 신뢰하기 전에 반드시 서명을 검증하세요.
멱등성·요청 추적·레이트 리밋
프로덕션 파이프라인을 안전하게 재시도 가능하게 만들기 위한 헤더가 몇 가지 있습니다.
| 헤더 | 역할 |
|---|---|
Idempotency-Key | /ocr/fields·/create·/upload 에서 같은 키의 재전송은 24시간 캐시 응답을 재생(X-Idempotent-Replay: true) ── 이중 과금 없이 안전하게 재시도. |
X-Request-Id | 모든 응답에 붙음(req_xxx). 지원 문의용으로 로그에. |
X-RateLimit-Remaining | 이번 분에 남은 호출 수. |
레이트 리밋은 키당 60 요청/분, uid당 600 요청/분입니다. 초과하면 429 와 error.code: "rate_limited" 가 돌아오고, 기다려야 할 초 수는 Retry-After 헤더(초)에 들어갑니다.
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded",
"requestId": "req_8fa2c1"
}
}추출에서, 쿼리할 수 있는 시트로
청구서를 시트에 추출했다면, 다시 읽어 오기 위해 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 등으로 렌더링한 뒤 보내세요(웹 앱에 드롭하는 경우에는 페이지 이미지화를 앱이 자동으로 처리하므로 PDF를 그대로 던질 수 있습니다). freee·마네 포워드(マネーフォワード)·야요이(弥生)·kintone 연계는 공식 API 연동이 아니라, 내보낸 CSV 가져오기로 한다는 전제입니다. 또한 인보이스 제도(적격청구서)나 전자장부보존법 대응 가능 여부는 각 사의 운영·요건에 맞춰 확인하세요(본 서비스가 법적 요건 충족을 보장하지는 않습니다).
요금
POST /ocr/fields 는 1콜 ₩100(부가세 포함), POST /upload 는 ₩100 × N장입니다. 실패는 과금하지 않음 ── 이미지를 읽지 못한 400(invalid_image)이나 동기 처리 상한을 넘은 504 는 애초에 과금되지 않고, 502 엔진 에러와 ocr.failed 이벤트는 자동으로 환불됩니다. 읽기 전용 엔드포인트(GET /space·/view·/amount·/health)는 무료입니다. 무료 플랜은 신용카드 없이 월 100크레딧, Pro는 ₩69,800/월(부가세 포함) 입니다. 플랜 목록은 요금 페이지에 있습니다.
청구서·납품서를 API로 추출하는 절차
- API 키 준비하기로그인해서 spocr_ 로 시작하는 API 키를 발급하고, 각 요청에 Authorization: Bearer spocr_... 를 붙입니다. 베이스 URL은 https://api.space-ocr.com 입니다.
- 이미지 준비하기(PDF는 페이지를 이미지화)청구서·납품서를 JPEG/PNG 등의 래스터 이미지로 준비합니다. API를 직접 호출하는 경우, PDF는 각 페이지를 PNG로 렌더링한 뒤 보냅니다(웹 앱에 드롭하는 경우에는 앱이 자동으로 이미지화합니다). 이미지는 URL 또는 순수 base64로 넘기고, imageType을 url / base64 로 지정합니다.
- POST /ocr/fields 호출하기뽑고 싶은 항목을 fields[] 에 FieldSpec({name, type, description, required, label, pattern, near, not_near, children})으로 선언합니다. 명세는 행 수를 세지 말고 type:"array" + children 으로 한 행의 형태만 선언하고, 몇 행이 돌아올지는 페이지에 맡깁니다. 어떤 항목이 실릴지 모르는 서식은 autoFields: true 로 스키마를 제안받을 수도 있습니다.
- 응답 검증하기data.review.flagged 에 놓인 {path, reasons} 를 작업 목록으로 열고, 그 path 로 data.cells[path] 를 조회해 box·quad 좌표와 evidence(text_match·match_ratio)를 확인합니다. 타입을 선언한 항목은 data.normalized 에 해석된 값이 놓이고, 해석하지 못한 이유는 cells[path].normalized.error 에 들어갑니다.
- CSV로 만들어 회계 소프트웨어로추출 결과를 UTF-8 BOM이 붙은 CSV로 내보내고(명세는 배열 행으로 전개), freee·마네 포워드(マネーフォワード)·야요이(弥生) 등의 CSV 가져오기에 넘깁니다. 쌓인 뒤에는 GET /view 로 재 OCR·무과금 상태로 쿼리할 수 있습니다.