space ocr
가이드아티클요금문서

송장에서 데이터를 추출하는 API

송장 데이터 추출을 위한 space-ocr API 개발자 가이드. curl·Python으로 호출하는 POST /ocr/fields, 송장 템플릿, 커스텀 필드, 그리고 검증된 바운딩 박스까지.

송장에서 구조화된 데이터를 뽑아내는 일 — 거래처, 송장 번호, 날짜, 항목 합계, 세금 — 은 문서 자동화에서 가장 흔하면서도 직접 만들기엔 가장 번거로운 작업 중 하나입니다. OCR 텍스트에 정규식을 거는 방식은 거래처가 레이아웃을 바꾸는 순간 무너집니다. 템플릿 매칭 도구는 공급처마다 일일이 박스를 그려달라고 요구하죠. 정작 필요한 건 어떤 레이아웃이든 읽어내고, 깔끔하게 타입이 지정된 필드를 돌려주며, 무엇보다 각 값이 페이지의 어디에서 나왔는지 알려줘서 결과를 신뢰할 수 있게 해주는 송장 데이터 추출 API입니다.

바로 그 마지막 부분이 핵심입니다. total: 2,045만 던져주고 출처는 알려주지 않는 송장 추출 엔드포인트는 매입 처리(AP) 파이프라인에서 오히려 부담입니다. 이 가이드에서는 space-ocr의 POST /ocr/fields 엔드포인트를 차근차근 살펴봅니다. 송장 이미지 하나를 받아 내장된 invoice 템플릿(혹은 직접 정의한 필드 스키마)을 적용하고, 모든 값을 검증된 바운딩 박스와 함께 돌려주는 단일 동기 호출입니다.

코드 한 줄 쓰기 전에 결과부터 확인하세요

아래는 실제로 파싱한 영수증입니다. 아무 필드에나 마우스를 올리면 이미지 위의 해당 박스가 밝아집니다 — 그 박스가 바로 값이 읽힌 위치이고, 각 필드는 자신만의 매치 비율을 함께 가지고 있습니다. 송장도 똑같이 동작합니다. 추출한 모든 필드가 자신이 비롯된 픽셀 위로 정확히 되돌아옵니다.

Invoice with extracted-field bounding boxes
Verified 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로 넘기면(imageTypehttp(s):// 접두사 여부로 자동 감지됩니다) 타입이 지정된 필드를 돌려받습니다.

내장 송장 템플릿으로 POST /ocr/fields 호출
1
2
3
4
5
6
7
8
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"
  }'
Why it matters

카멜케이스가 표준 표기입니다. 파라미터는 imageType, templateId, autoFields 입니다. 구식 스네이크케이스 별칭(image_type, template_id, auto_fields)도 여전히 동작하지만 deprecated 되었습니다 — 새 코드에서는 카멜케이스 이름을 쓰세요.

응답 구조

호출이 성공하면 { status: "success", data: { ... } }를 반환합니다. 추출된 각 값은 자신만의 출처 정보를 가지고 있고, field_bboxes 맵이 필드별 좌표를 제공합니다.

  • bbox0–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(병합된 셀에서 전파된 값).
POST /ocr/fields → 응답 (축약)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "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"
      }
    }
  }
}
✓ Verified

좌표는 모델의 말을 그대로 믿고 정하지 않습니다. 언어 모델은 각 값의 텍스트 — 그리고 어떤 워드 토큰을 사용했는지에 대한 힌트 — 를 반환하지만 박스 자체는 절대 내놓지 않습니다. 그다음 엔진이 그 텍스트를 비전 OCR이 페이지에서 실제로 검출한 심볼들과 문자 단위로 매칭합니다. match_ratio는 그중 얼마나 찾아냈는지를 나타내고, 박스는 그 문자들이 비롯된 실제 픽셀 위에 놓입니다. 모델의 토큰 힌트는 노이즈가 섞일 수 있어서(반복되는 행 사이에서 서로 바뀌기도 합니다), 이를 무조건 신뢰하는 대신 열·행 일관성 검사로 검증합니다. 그래서 값의 좌표는 송장과 다시 대조되며, 얼마나 잘 맞았는지를 말해주는 점수가 함께 붙습니다. 전체 논리는 바운딩 박스가 OCR을 감사 가능하게 만드는 이유를 참고하세요.

템플릿만으로 부족할 때의 커스텀 필드

실제 송장에는 범용 템플릿이 이름 붙이지 못하는 필드가 있습니다 — 발주(PO) 참조 번호, 지급 조건 코드, 프로젝트 태그 같은 것들이죠. 이럴 땐 템플릿 대신(또는 템플릿과 함께) FieldSpec 객체들의 fields 배열을 넘기면 됩니다. 각 FieldSpec은 { name, type, description?, children? } 형태입니다. fieldstemplateId를 둘 다 보내면 fields가 우선합니다.

description은 모델을 유도하는 부분입니다 — 무엇을 어떻게 잡아낼지 평이한 자연어로 적어 주는 지시문이죠. 그리고 children을 곁들인 type: "array"는 반복되는 라인 아이템을 뽑아내는 방법으로, 하나의 자식 스키마로 여러 행을 처리합니다. (이 부분은 송장에서 라인 아이템 추출하기에서 깊게 다룹니다.)

중첩된 라인 아이템을 가진 커스텀 FieldSpec
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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"])
Why it matters

값은 입력 그대로 보존됩니다. 합계 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 헤더는 없으므로 본문 값을 보고 백오프하세요.

429 응답 본문
1
2
3
4
5
6
7
8
{
  "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로 변환하기를 참고하세요.

송장을 끌어다 놓으면 타입이 지정된 필드가 채워집니다 — API가 반환하는 것과 똑같은 데이터를 UI에서 보여줍니다.

요금

POST /ocr/fields호출당 ₩100, POST /upload는 이미지 N장 기준 ₩100 × N 입니다. 실패 시에는 과금이 없습니다 — OCR이 결과를 내지 못하면 환불되고, 502 엔진 오류나 ocr.failed 이벤트도 자동으로 환불됩니다. 읽기 전용 엔드포인트(GET /space, /view, /amount, /health)는 무료입니다. 무료 등급은 신용카드 없이 월 100스캔, Pro는 월 ₩49,800, Business는 영업팀 문의입니다.

추출된 송장 전체를 검색하고 일치하는 셀로 — 그리고 그 출처 박스로 — 곧바로 이동하세요.

API로 송장에서 데이터를 추출하는 방법

  1. 이미지를 준비하고 인증하기
    송장을 래스터 이미지(JPEG, PNG, GIF, BMP, TIFF, WebP)로 준비하고, URL이나 순수 base64로 인코딩하세요. spocr_ 접두사가 붙은 키로 Authorization: Bearer 헤더를 설정합니다.
  2. POST /ocr/fields 호출하기
    https://api.space-ocr.com/ocr/fields 로 이미지를 보냅니다. imageType('url' 또는 'base64')과 함께, 빠르게 시작하려면 templateId 'invoice'를, 직접 스키마를 정의하려면 fields[] 배열을 넘기세요.
  3. 타입이 지정된 필드와 출처 받기
    응답은 { status: "success", data: { ... } } 형태로 옵니다. field_bboxes 맵에서 필드별 bbox, vertices, match_ratio, bbox_source를 확인하세요.
  4. 각 값을 검증하기
    match_ratio가 0.85 이상이면 확실히 매칭된 것입니다. 낮은 값이나 low_confidence는 검토 대상으로 표시하고, 바운딩 박스로 원본 픽셀과 대조해 값을 확인하세요.
송장에서 데이터를 추출하는 가장 좋은 API는 무엇인가요?
좋은 송장 추출 API는 (미리 박스를 그려둔 템플릿만이 아니라) 어떤 레이아웃이든 읽어내고, 깔끔하게 타입이 지정된 필드를 반환하며, 각 값의 출처를 알려줍니다. space-ocr의 POST /ocr/fields는 이를 단 한 번의 동기 호출로 처리합니다. templateId 'invoice' 또는 직접 정의한 fields[] 스키마와 함께 송장 이미지를 넘기면, 모든 값이 바운딩 박스, 방향이 적용된 꼭짓점, 매치 비율과 함께 돌아와 원본과 대조해 검증할 수 있습니다.
헤더 필드뿐 아니라 송장 라인 아이템도 추출할 수 있나요?
네. type이 'array'인 FieldSpec과, 한 행을 설명하는 children 스키마(예: description, qty, unit_price)를 사용하세요. API는 송장의 라인 아이템 한 줄당 하나의 행을 반환하며, 각 행은 자신만의 바운딩 박스를 가집니다. vendor, 송장 번호, 날짜, total 같은 헤더 필드는 같은 호출에서 함께 추출됩니다.
송장 추출 API가 PDF를 받나요?
엔진은 래스터 이미지만 받습니다 — JPEG, PNG, GIF, BMP, TIFF, WebP — 그리고 자동으로 RGB로 변환합니다. 이미지는 'image' 필드에 URL이나 순수 base64로 넘기고, imageType을 'url' 또는 'base64'로 설정하세요.
송장 추출 API의 오류와 레이트 리밋은 어떻게 처리되나요?
레이트 리밋은 고정 60초 윈도우 기준으로 키당 60 requests/min, uid당 600/min 입니다. 이를 초과하면 error.code 'rate_limited'와 함께 HTTP 429를 반환하며, 대기 시간은 JSON 본문의 details.retryAfterSec에 담깁니다 — Retry-After 헤더는 없습니다. /upload와 /create에서는 Idempotency-Key를 사용해, 재시도 시 다시 과금하는 대신 24시간 동안 캐시된 응답을 재생하도록 하세요.
송장에서 데이터를 추출하는 데 비용이 얼마나 드나요?
POST /ocr/fields는 호출당 ₩100, /upload는 이미지당 ₩100입니다. 실패 시에는 과금이 없어, OCR이 결과를 내지 못하면 자동으로 환불됩니다. 무료 등급은 신용카드 없이 월 100스캔을 포함하고, Pro는 월 ₩49,800, Business는 영업팀 문의입니다.

단 한 번의 호출로 첫 송장을 추출하세요

무료 등급 — 신용카드 없이 월 100스캔. 모든 필드가 페이지 위 위치 정보와 함께 돌아옵니다.

관련