송장에서 데이터를 추출하는 API
송장 데이터 추출을 위한 space-ocr API 개발자 가이드. curl·Python으로 호출하는 POST /ocr/fields, 송장 템플릿, 커스텀 필드, 그리고 검증된 바운딩 박스까지.
송장에서 구조화된 데이터를 뽑아내는 일 — 거래처, 송장 번호, 날짜, 항목 합계, 세금 — 은 문서 자동화에서 가장 흔하면서도 직접 만들기엔 가장 번거로운 작업 중 하나입니다. OCR 텍스트에 정규식을 거는 방식은 거래처가 레이아웃을 바꾸는 순간 무너집니다. 템플릿 매칭 도구는 공급처마다 일일이 박스를 그려달라고 요구하죠. 정작 필요한 건 어떤 레이아웃이든 읽어내고, 깔끔하게 타입이 지정된 필드를 돌려주며, 무엇보다 각 값이 페이지의 어디에서 나왔는지 알려줘서 결과를 신뢰할 수 있게 해주는 송장 데이터 추출 API입니다.
바로 그 마지막 부분이 핵심입니다. total: 2,045만 던져주고 출처는 알려주지 않는 송장 추출 엔드포인트는 매입 처리(AP) 파이프라인에서 오히려 부담입니다. 이 가이드에서는 space-ocr의 POST /ocr/fields 엔드포인트를 차근차근 살펴봅니다. 송장 이미지 하나를 받아 내장된 invoice 템플릿(혹은 직접 정의한 필드 스키마)을 적용하고, 모든 값을 검증된 바운딩 박스와 함께 돌려주는 단일 동기 호출입니다.
코드 한 줄 쓰기 전에 결과부터 확인하세요
아래는 실제로 파싱한 영수증입니다. 아무 필드에나 마우스를 올리면 이미지 위의 해당 박스가 밝아집니다 — 그 박스가 바로 값이 읽힌 위치이고, 각 필드는 자신만의 매치 비율을 함께 가지고 있습니다. 송장도 똑같이 동작합니다. 추출한 모든 필드가 자신이 비롯된 픽셀 위로 정확히 되돌아옵니다.

Each value with a box 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.
인증과 베이스 URL
공개 API는 단일 베이스 — https://api.space-ocr.com — 에 있으며 /v1 같은 경로 버전 표기는 없습니다. 모든 요청은 spocr_ 접두사가 붙은 키로 HTTP Bearer 토큰 인증을 합니다.
Authorization: Bearer spocr_xxxxxxxxxxxxxxxx
헤더가 없거나 형식이 잘못되면 401, 인식되지 않는 키이면 403을 반환합니다. 모든 응답에는 지원 문의 추적을 위해 로그로 남겨둬야 할 X-Request-Id 헤더(형식 req_xxx)가 실려 옵니다. 직접 클라이언트를 생성하고 싶다면 전체 명세가 GET /openapi.json에서 OpenAPI 3.1로 공개되어 있습니다.
가장 간단한 호출: 내장 송장 템플릿
가장 빠른 길은 templateId: "invoice" 입니다 — 송장이 어떻게 생겼는지 이미 아는 사전 정의 스키마라서, 필드를 직접 설명할 필요가 없습니다. 이미지를 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/invoices/inv-4471.jpg",
"imageType": "url",
"templateId": "invoice"
}'카멜케이스가 표준 표기입니다. 파라미터는 imageType, templateId, autoFields 입니다. 구식 스네이크케이스 별칭(image_type, template_id, auto_fields)도 여전히 동작하지만 deprecated 되었습니다 — 새 코드에서는 카멜케이스 이름을 쓰세요.
응답 구조
호출이 성공하면 { status: "success", data: { ... } }를 반환합니다. 추출된 각 값은 자신만의 출처 정보를 가지고 있고, field_bboxes 맵이 필드별 좌표를 제공합니다.
bbox— 0–1000 normalized 그리드(0,0 = 왼쪽 위, 1000,1000 = 오른쪽 아래) 위의 축 정렬 사각형{ xmin, ymin, xmax, ymax }로, 이미지의 픽셀 크기와 무관합니다. 픽셀로 변환하려면pixel_x = bbox_x / 1000 × image_width를 쓰면 됩니다.vertices— 순서가 정해진 네 점{x, y}(왼쪽 위 → 오른쪽 위 → 오른쪽 아래 → 왼쪽 아래)로, 문서의 기울기를 따라가는 방향이 적용된 박스를 이룹니다. 그래서 송장을 비뚤게 찍은 휴대폰 사진이라도 박스가 깔끔하게 잡힙니다.match_ratio— 값의 문자 중 실제로 페이지에서 찾아낸 비율(0–1)입니다. 필드는 0.85 이상이면 확실히 매칭된 것으로 간주하며,1.0은 모든 문자를 찾았다는 뜻입니다.bbox_source— 좌표가 어떻게 도출됐는지를 나타냅니다:vision_symbol_match(실제match_ratio를 동반하는 일반적인 문자 매칭 경로),token_id/token_id_hybrid(워드 토큰 힌트를 사용한 경우),low_confidence(약한 매칭 — 검토할 가치가 있음), 또는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"
}
}
}
}좌표는 모델의 말을 그대로 믿고 정하지 않습니다. 언어 모델은 각 값의 텍스트 — 그리고 어떤 워드 토큰을 사용했는지에 대한 힌트 — 를 반환하지만 박스 자체는 절대 내놓지 않습니다. 그다음 엔진이 그 텍스트를 비전 OCR이 페이지에서 실제로 검출한 심볼들과 문자 단위로 매칭합니다. match_ratio는 그중 얼마나 찾아냈는지를 나타내고, 박스는 그 문자들이 비롯된 실제 픽셀 위에 놓입니다. 모델의 토큰 힌트는 노이즈가 섞일 수 있어서(반복되는 행 사이에서 서로 바뀌기도 합니다), 이를 무조건 신뢰하는 대신 열·행 일관성 검사로 검증합니다. 그래서 값의 좌표는 송장과 다시 대조되며, 얼마나 잘 맞았는지를 말해주는 점수가 함께 붙습니다. 전체 논리는 바운딩 박스가 OCR을 감사 가능하게 만드는 이유를 참고하세요.
템플릿만으로 부족할 때의 커스텀 필드
실제 송장에는 범용 템플릿이 이름 붙이지 못하는 필드가 있습니다 — 발주(PO) 참조 번호, 지급 조건 코드, 프로젝트 태그 같은 것들이죠. 이럴 땐 템플릿 대신(또는 템플릿과 함께) FieldSpec 객체들의 fields 배열을 넘기면 됩니다. 각 FieldSpec은 { name, type, description?, children? } 형태입니다. fields와 templateId를 둘 다 보내면 fields가 우선합니다.
description은 모델을 유도하는 부분입니다 — 무엇을 어떻게 잡아낼지 평이한 자연어로 적어 주는 지시문이죠. 그리고 children을 곁들인 type: "array"는 반복되는 라인 아이템을 뽑아내는 방법으로, 하나의 자식 스키마로 여러 행을 처리합니다. (이 부분은 송장에서 라인 아이템 추출하기에서 깊게 다룹니다.)
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",
"description": "Invoice number, verbatim"},
{"name": "invoice_date", "type": "string"},
{"name": "total", "type": "string",
"description": "Grand total, keep comma separators"},
{"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=60,
)
data = resp.json()["data"]
print(data["total"], data["field_bboxes"]["total"]["match_ratio"])값은 입력 그대로 보존됩니다. 합계 7,855는 문자열 "7,855"로 돌아옵니다 — 쉼표 구분자, 소수점, 전각 문자가 그대로 유지됩니다. 엔진은 필드의 description이 명시적으로 요청할 때만 정규화합니다. 웹 UI에서 보이는 ¥는 장식일 뿐 값의 일부가 아닙니다. 엔진은 래스터 이미지만 받습니다 — JPEG, PNG, GIF, BMP, TIFF, WebP — 그리고 자동으로 RGB로 변환합니다.
비동기로 가기: 일괄 업로드, 잡, 웹훅
POST /ocr/fields는 동기 호출이라 요청/응답 루프에서 송장 하나를 처리하기에 안성맞춤입니다. 송장이 한 폴더 가득이라면 POST /upload(멀티파트 files를 반복)로 시트에 올리세요. 기본적으로는 즉시 jobs 배열과 함께 반환됩니다.
{ "path": "...", "jobs": [ { "uniqueKey": "...", "jobId": "...", "status": "pending" } ] }
그다음 결과는 두 가지 방법으로 알 수 있습니다. GET /jobs/{jobId}를 폴링하거나, 웹훅을 등록하면 됩니다. 웹훅은 스페이스당 URL 하나이며, X-Spaceocr-Signature 헤더로 HMAC-SHA256 서명됩니다. 신경 쓸 이벤트는 upload.received, item.created, ocr.completed(추출 결과를 담은 data.result 동반), 그리고 ocr.failed 입니다. 페이로드를 신뢰하기 전에 항상 서명을 검증하세요.
멱등성, 요청 추적, 그리고 레이트 리밋
몇 가지 헤더만 있으면 프로덕션 파이프라인을 안전하게 재시도할 수 있습니다.
| 헤더 | 용도 |
|---|---|
Idempotency-Key | /upload와 /create에서, 같은 키로 재요청하면 24시간 동안 캐시된 응답을 재생합니다(X-Idempotent-Replay: true) — 중복 과금 없이 안전하게 재시도. |
X-Request-Id | 모든 응답에 반환됩니다(req_xxx). 지원 문의를 위해 로그로 남기세요. |
레이트 리밋은 고정 60초 윈도우 기준으로 키당 60 requests/min, uid당 600 requests/min 입니다. 이를 초과하면 error.code: "rate_limited"와 함께 429를 받습니다. 대기 시간은 JSON 본문의 details.retryAfterSec에 담겨 있습니다 — Retry-After HTTP 헤더는 없으므로 본문 값을 보고 백오프하세요.
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded",
"requestId": "req_8fa2c1"
},
"details": { "retryAfterSec": 12 }
}추출에서 조회 가능한 시트로
송장이 시트에 추출되고 나면, 그걸 다시 읽기 위해 OCR을 재실행하지 않습니다. GET /view는 저장된 행에 대해 서버 측 쿼리 — where, sort, select, limit, offset — 를 실행하며, 과금도 재추출도 없습니다. 바운딩 박스는 기본으로 함께 실려 오고, 더 가벼운 페이로드를 원하면 boxes=0을 붙이면 됩니다. 거기서 CSV로 내보낼 수 있습니다(UTF-8 BOM이라 Excel과 CJK 텍스트가 깨지지 않고 열립니다) — 스캔 문서를 CSV로 변환하기를 참고하세요.
요금
POST /ocr/fields는 호출당 ₩100, POST /upload는 이미지 N장 기준 ₩100 × N 입니다. 실패 시에는 과금이 없습니다 — OCR이 결과를 내지 못하면 환불되고, 502 엔진 오류나 ocr.failed 이벤트도 자동으로 환불됩니다. 읽기 전용 엔드포인트(GET /space, /view, /amount, /health)는 무료입니다. 무료 등급은 신용카드 없이 월 100스캔, Pro는 월 ₩49,800, Business는 영업팀 문의입니다.
API로 송장에서 데이터를 추출하는 방법
- 이미지를 준비하고 인증하기송장을 래스터 이미지(JPEG, PNG, GIF, BMP, TIFF, WebP)로 준비하고, URL이나 순수 base64로 인코딩하세요. spocr_ 접두사가 붙은 키로 Authorization: Bearer 헤더를 설정합니다.
- POST /ocr/fields 호출하기https://api.space-ocr.com/ocr/fields 로 이미지를 보냅니다. imageType('url' 또는 'base64')과 함께, 빠르게 시작하려면 templateId 'invoice'를, 직접 스키마를 정의하려면 fields[] 배열을 넘기세요.
- 타입이 지정된 필드와 출처 받기응답은 { status: "success", data: { ... } } 형태로 옵니다. field_bboxes 맵에서 필드별 bbox, vertices, match_ratio, bbox_source를 확인하세요.
- 각 값을 검증하기match_ratio가 0.85 이상이면 확실히 매칭된 것입니다. 낮은 값이나 low_confidence는 검토 대상으로 표시하고, 바운딩 박스로 원본 픽셀과 대조해 값을 확인하세요.