소개
space ocr API 는 문서 사진을 이름 붙인 필드·마크다운·원문 텍스트 중 원하는 형태로 읽고, 값마다 읽어 낸 좌표와 검증 플래그를 함께 돌려줘요. 그 결과를 보관·조회하는 게 MySpace 고요. spocr_* 로 시작하는 API 키 하나면 REST 엔드포인트랑 이벤트 webhook 까지 한 번에 써요.
REST 기반, JSON, CORS 다 돼요. 가변 길이 배치나 비동기 처리는 Jobs / Webhooks 섹션을 보세요.
5분 퀵스타트
키 발급 → curl 복붙 → JSON. 첫 호출까지 5분이면 충분해요. 무료 할당은 매월 100건이에요.
① Developer → API Keys 에서 키를 발급하세요 (카드 불필요).
② 오른쪽 curl 을 그대로 실행하세요 — 샘플 이미지가 실제로 호스팅돼 있어서 키만 바꾸면 바로 돌아가요.
③ 응답의 data.values 값과 data.cells 의 box / quad / verified, 그리고 data.review.flagged(검토 목록)를 확인하세요.
④ 코드를 쓰기 전에 먼저 보고 싶다면 — 마이스페이스 콘솔이 그대로 플레이그라운드예요. 시트에 파일을 올리면 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" }]
}'인증
모든 요청에 Authorization 헤더로 Bearer <API키> 를 실어서 보내요. 키 발급·폐기는 Developer → API Keys 에서 할 수 있어요.
키 형식은 spocr_ 로 시작해요. 혹시 노출되면 바로 폐기해주세요.
curl https://api.space-ocr.com/amount \
-H "Authorization: Bearer YOUR_API_KEY"Base URL
프로덕션 base URL 은 하나예요. 버전 관리는 키랑 이벤트 페이로드의 apiVersion 으로 해요.
# Production
https://api.space-ocr.com
# OpenAPI spec
https://api.space-ocr.com/openapi.jsonRate limits
60 req/min/key, 600 req/min/uid 까지 받아요. 초과하면 HTTP 429 와 Retry-After 헤더로 몇 초 기다리면 되는지 알려드려요.
응답엔 항상 X-Request-Id (req_xxx) 와 X-RateLimit-Remaining (이번 분에 남은 호출 수) 가 붙어요. 문의하실 때 X-Request-Id 를 같이 주시면 좋아요.
/ocr/fields・/create・/upload 에서는 Idempotency-Key 헤더를 쓸 수 있어요. 같은 키로 다시 보내면 24h 동안 캐시된 응답이 그대로 와요. 이땐 X-Idempotent-Replay: true 헤더가 붙어요.
이미지 크기와 응답 시간
응답 시간은 요청 이미지 크기에 크게 좌우돼요. 관측 분포는 p50 7.2초 / p90 10.5초예요 (SLA 는 아니에요).
JSON 바디는 2MB 까지라 base64(파일의 약 1.33배)로는 실질 ~1.5MB 가 상한이에요. 넘는다면 imageType: "url" 로 URL 을 넘기거나, /upload(비동기, 파일당 20MB) + /jobs 폴링 또는 webhook 을 써주세요. 처리가 110초를 넘으면 ocr_engine_timeout 이 나요 — 그땐 줄여 보내거나 비동기 경로로.
오류
4xx / 5xx 오류는 다 같은 envelope 으로 돌려드려요. requestId 는 문의하실 때 단서가 돼요.
{
"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 로 알아서 제안받을 수도 있어요.
바디 파라미터
응답 필드
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": "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": "합계" }
]
}'{
"status": "success",
"data": {
"values": {
"store_name": "슈퍼마켓 ABC",
"date": "2025-04-10",
"invoice_no": "",
"items": [
{ "name": "우유", "qty": "1", "price": "₩1,980" }
],
"total": "₩4,780"
},
"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": { "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": { "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": { "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": { "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": { "reason": "text_mismatch", "reasons": ["text_mismatch"] },
"evidence": { "source": "vision_symbol_match", "match_ratio": 1.0, "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": { "source": "vision_symbol_match", "match_ratio": 1.0, "ocr_confidence": 0.98 },
"normalized": { "value": 4780, "type": "number", "method": "deterministic" } }
},
"review": {
"unit": "field",
"declared": 7,
"returned": 6,
"boxed": 6,
"verified": 5,
"flagged": [
{ "path": "items[0].price", "reason": "text_mismatch", "reasons": ["text_mismatch"] },
{ "path": "invoice_no", "reason": "missing", "reasons": ["missing"] }
],
"by_reason": { "text_mismatch": 1, "missing": 1 },
"notes": [
{ "path": "total", "declared_type": "number", "applied_type": "string",
"description": "\"total\" was declared as number and read as string. Values are returned exactly as printed on the page so the text can be matched to coordinates and verified; the declared type was kept as an extraction hint, not applied as formatting." }
]
},
// 선언한 타입은 values 를 건드리지 않고 이 층으로 나와요
"normalized": { "total": 4780 },
"image": { "width": 1654, "height": 2339 }
}
}마크다운 변환
레이아웃을 살린 채로 이미지를 마크다운으로 바꿔요. 제목·문단·목록·표가 요소로 나오고, 요소마다 좌표가 붙어요.
바디 파라미터
응답 필드
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"
}'{
"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": { "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": { "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": { "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": { "reason": "text_mismatch" },
"evidence": { "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": { "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": { "source": "token_id" } }
},
"review": {
"unit": "element",
"total": 6,
"boxed": 6,
"verified": 5,
"flagged": [{ "path": "elements[2].cells[1]", "reason": "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
스키마도 마크다운 문법도 없이 문서의 글자만 전부 돌려줘요. 모델이 이미지를 보고 진짜 읽기순서로 블록을 정렬하기 때문에, 다단 조판이나 기울어진 스캔에서도 문장이 뒤섞이지 않아요.
바디 파라미터
응답 필드
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
}'{
"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": { "source": "token_id", "ocr_confidence": 0.98 } }
},
"review": {
"unit": "block",
"total": 12,
"boxed": 12,
"verified": 11,
"flagged": [{ "path": "blocks[7]", "reason": "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"
}
}트리 조회
MySpace 의 폴더/시트/메모를 한눈에 보여드려요. path 와 depth 로 범위를 좁힐 수 있어요.
쿼리 파라미터
응답 필드
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. 폴더가 아닌 항목은 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" }생성
부모 폴더 아래에 folder / sheet / doc / memo 를 만들어요. 시트는 OCR 스키마(columns) 랑 prompt 를, doc(문서 묶음)은 mode 를 가져요.
바디 파라미터
응답 필드
# 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 웹훅이 발사돼요. Idempotency-Key 헤더를 붙이면
// 24h 안의 재요청은 같은 응답이 그대로 와요.
// required: true 인 컬럼(위의 amount)은 이후 업로드에서 값이 비면
// 그 행의 review.flagged 에 reason "missing" 으로 나타나요.이미지 업로드
시트 또는 문서 묶음에 이미지를 한 장 이상 올려요. multipart/form-data 예요. 기본은 비동기로, jobs 가 먼저 돌아오고 완료는 webhook 으로 알려드려요.
폼 필드 (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"
}
}
}시트 행/메모 편집
시트 셀 값이나 메모 본문을 덮어써요. anyOf 라서 (path, row, column, value) 둘 중 하나, 아니면 (path, text) 로 보내요. **편집되는 건 시트랑 메모뿐이에요** — 문서 묶음(.md / .txt)은 판독 결과 자체라 400 으로 거절돼요.
바디 파라미터
응답 필드
# 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)
폴더/시트/메모/이미지를 지워요. 폴더를 지우면 하위 메타데이터・flat 항목・Storage 까지 줄줄이 같이 삭제돼요.
바디 파라미터
응답 필드
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 잡 폴링
POST /upload (비동기) 가 알려준 jobId 의 상태를 확인해요. webhook 을 안 쓸 때 유용해요.
패스 파라미터
응답 필드
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": { // 매월 무료 할당
"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{ "status": "ok", "version": "v1", "time": 1716700000000 }개요
스페이스 전체에 Webhook URL 하나만 등록해두면, 모든 이벤트가 HMAC 서명이랑 같이 그쪽으로 가요. 설정은 Developer → Webhooks 에서 하거나, 아래의 Webhook 관리 엔드포인트로 해도 돼요.
이벤트
모든 이벤트는 동일한 envelope (event / deliveryId / occurredAt / apiVersion / data) 으로 와요.
페이로드 예시 — ocr.completed
{
"event": "ocr.completed",
"deliveryId": "dlv_xxx",
"occurredAt": 1716700000000,
"apiVersion": "v1",
"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 }
}
}
}mode 는 업로드 대상이 정합니다 — 시트면 "sheet", 문서 묶음이면 "markdown" / "text" 예요. result 는 GET /jobs 와 같은 { values, cells, review, image }(v2) 이고, values 안쪽만 mode 를 따릅니다 (sheet: 필드 값 / markdown: { markdown, elements } / text: { text, blocks }). 문서 묶음이면 sheetRef 가 null 이고 docRef 에 묶음의 uniqueKey 가 들어옵니다.
전달 헤더
받는 쪽 엔드포인트에 아래 헤더가 같이 붙어요. 서명 검증에 필요한 건 Signature / Timestamp 두 개예요.
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서명 검증
X-Spaceocr-Signature 헤더는 t=<unix_ms>,v1=<hex> 형식이에요. canonical 문자열은 `${t}.${rawBody}`, 알고리즘은 HMAC-SHA256 이에요. Replay 공격 막으려면 timestamp 가 5분 넘게 차이나는 건 거절해주세요.
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"),
);
}재시도 정책
2xx 가 아니면 exponential backoff (1m → 5m → 30m → 2h) 로 최대 5 번까지 시도해요. 5xx / 408 / 429 / timeout 만 재시도 대상이고, 다른 4xx 는 바로 dead 처리해요. 배달 로그는 30일 동안 보관해드려요.
현재 webhook 설정
지금 스페이스에 등록돼 있는 webhook URL 과 상태를 알려드려요.
응답 필드
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 설정 생성·갱신
스페이스 전체 webhook URL 을 등록하거나 바꿔요. rotateSecret 으로 서명 키도 새로 발급받을 수 있어요.
바디 파라미터
응답 필드
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 설정 삭제
등록한 webhook 을 지워요. 그 뒤로는 이벤트가 안 가요.
응답 필드
curl -X DELETE https://api.space-ocr.com/webhook \
-H "Authorization: Bearer YOUR_API_KEY"{ "ok": true }테스트 이벤트 발사
설정해둔 URL 로 webhook.test 이벤트를 바로 쏴드려요. 받는 쪽 구현이 잘 됐는지 확인할 때 좋아요.
응답 필드
curl -X POST https://api.space-ocr.com/webhook/test \
-H "Authorization: Bearer YOUR_API_KEY"{ "ok": true, "deliveryId": "dlv_xxx" }최근 전달 이력
최근 webhook 배달 로그를 보여드려요. 디버깅할 때 써요.
쿼리 파라미터
응답 필드
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 에 시도가 덧붙습니다.개요
MCP 서버는 이 API 를 AI 에이전트의 툴로 그대로 열어 줍니다. 읽기 3종에 더해 폴더와 시트를 만들고, 사진을 올리고, 쌓인 행을 조건 걸어 꺼내는 것까지 에이전트가 합니다. 안에서 부르는 건 같은 REST 라우트라 과금도 같습니다.
연결
설치할 건 없습니다. 헤더를 설정할 수 있는 클라이언트는 API 키를 베어러 토큰으로 그대로 보내면 됩니다. 헤더를 못 넣는 Claude 데스크톱/모바일/claude.ai 에서는 이 URL 을 커스텀 커넥터로 추가하면 OAuth 동의 화면이 열려서, 어떤 API 키로 동작할지 고르면 됩니다. 어느 쪽이든 키는 그 요청에만 쓰이고 서버에 저장되지 않습니다.
# 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" }
}
}
}툴 목록
읽기 3개와 작업공간 8개예요. 과금은 대응하는 REST 라우트와 같고, 읽기와 이미지 업로드만 크레딧을 씁니다.