space ocr

API 문서

https://api.space-ocr.com

소개

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 로 올린 문서도 같은 시트에 나타나서, 자동 처리와 눈으로 하는 검수를 한자리에서 다룰 수 있어요.

요청
1
2
3
4
5
6
7
8
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_ 로 시작해요. 혹시 노출되면 바로 폐기해주세요.

요청
1
2
curl https://api.space-ocr.com/amount \
  -H "Authorization: Bearer YOUR_API_KEY"

Base URL

프로덕션 base URL 은 하나예요. 버전 관리는 키랑 이벤트 페이로드의 apiVersion 으로 해요.

1
2
3
4
5
# Production
https://api.space-ocr.com

# OpenAPI spec
https://api.space-ocr.com/openapi.json

Rate 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 는 문의하실 때 단서가 돼요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "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_error

HTTP 상태

200
정상이에요
400
요청 형식이 잘못됐거나 검증에서 걸렸어요. 이미지 URL 을 받아오지 못하거나 base64 가 깨진 경우도 같은 400 이에요 (code: invalid_image, 과금되지 않아요). 다시 보내도 같은 답이라 입력을 고쳐주세요
401
API 키가 없거나 잘못됐어요
402
잔액이 모자라요. /upload 배치는 details.requested / processable / breakdown 까지 같이 알려드려요
403
이 키의 범위 밖 리소스예요 (예: 다른 키가 만든 job)
404
path 가 없거나 api.space-ocr.com 이 아닌 곳으로 호출했어요
413
본문이 2MB 를 넘거나 파일이 20MB 를 넘었어요
429
레이트 제한이에요. Retry-After 헤더에 기다릴 초 수가 적혀 있어요
500
내부 오류예요
502
OCR 엔진 쪽 오류예요 (자동 환불해드려요). message 에 실제 사유가 담겨요. 이미지 입력 자체의 문제는 400 invalid_image 로 갈리니, 502 는 다시 시도해볼 만해요
504
OCR 엔진이 제한 시간 안에 응답하지 않았어요 (자동 환불해드려요). 이미지를 줄여서 다시 시도해주세요
POST/ocr/fieldsBearer₩100

구조화 OCR

이미지에서 이름 붙인 필드를 뽑아내요. fields 로 추출 스키마를 정하거나, autoFields 로 알아서 제안받을 수도 있어요.

바디 파라미터

imagestringrequired
Base64 문자열 또는 이미지 URL. base64 로 JSON 바디가 2MB 를 넘으면 URL 로 넘기거나 /upload 를 써주세요.
imageType"base64" | "url"required
image 의 타입을 직접 알려주세요. 구 이름 image_type 도 하위호환으로 동작해요 (deprecated).
fieldsarray<FieldSpec>optional
추출 스키마 배열이에요. autoFields 를 쓸 거면 생략해도 돼요. 값은 항상 페이지에 인쇄된 그대로 돌아와요 — 그래야 좌표에 붙여서 검증할 수 있거든요.
namestringrequired
응답 JSON 의 키가 돼요.
type"string" | "array" | "object" | "number" | "integer" | "date"optional
기본값은 string 이에요. number / integer / date 를 선언해도 values 는 인쇄된 그대로 돌아와요 — 타입이 모델에게 전달되는 일은 없어요 (타입을 알리면 그 형태의 값을 만들어 버리거든요). 선언한 타입이 만드는 건 normalized 라는 두 번째 층이고, 같은 읽기를 그 타입으로 해석한 값이 values 와 같은 모양으로 놓여요 ("¥13,220" → 13220, "令和8年8月16日" → "2026-08-16", "3袋" → 3). 해석은 결정론적이라 추가 모델 호출이 없어요. 해석하지 못한 값은 normalized 에서 null 이 되고 reason "type_mismatch" 가 붙는데, 대개 그건 오독의 신호예요.
descriptionstringoptional
그 값이 어디 있는지 알려주는 힌트예요 (예: "합계 오른쪽").
childrenarray<FieldSpec>optional
type 이 array / object 일 때의 하위 필드예요. 같은 FieldSpec 이 재귀로 들어가요.
requiredbooleanoptional
true 인 항목이 빈 값으로 오거나 응답에서 아예 빠지면 review.flagged 에 reason "missing" 으로 기록돼요 — 돌아온 적 없는 값은 대조할 상대가 없어서, 문자 대조가 원리적으로 볼 수 없는 유일한 클래스거든요. 모델에게는 전달되지 않아 추출 동작은 그대로예요 (필수라고 알리면 인쇄되지 않은 값을 추론하지 말라는 지시와 부딪혀요). 없는 게 정상인 항목까지 켜면 신호가 묻히니, 늘 인쇄되는 값에만 켜주세요.
labelstring | string[]optional
값 옆에 인쇄된 라벨이에요 (예: "합계"). 같은 값이 페이지에 여러 번 찍혀 있을 때 좌표를 그 라벨 옆 등장에 앵커해줘요. 최상위 string 필드 전용이고 (배열·객체의 자식은 무시), 라벨이 페이지에 정확히 1회 인쇄됐을 때만 작동하며, 못 찾으면 조용히 기존 탐색으로 돌아가요. required 처럼 모델에게는 전달되지 않아요 — 추출 텍스트는 그대로고 좌표 앵커만 바뀌어요. 후보가 여러 개면 배열로 주세요 (예: ["발행일", "발행년월일"]).
patternstring | string[]optional
정규화된 값이 만족해야 하는 정규식이에요. JSON Schema 와 같은 부분 일치라서 값 전체를 보려면 ^…$ 를 붙여주세요. 배열로 주면 "하나라도 맞으면 통과" 예요. string 타입 전용이고요. 대조는 전각을 반각으로 접은 값에 대해 하기 때문에, 페이지가 전각으로 인쇄돼 있어도 평범한 ASCII 패턴이 통해요 (T12… 는 T12… 로 대조). 어기면 reason "pattern_mismatch" 예요. 모델에게는 전달되지 않아요 — 형태를 알려주면 그 형태의 값을 만들어 버리거든요.
min / maxnumberoptional
number / integer 타입 값의 범위예요 (양끝 포함). 정규화된 수치에 대해 판정하고, 벗어나면 reason "out_of_range" 가 붙어요.
enumstring[]optional
string 타입에서 허용할 값의 집합이에요. 정규화된 값과 대조해요. 두 엔진이 같은 오독에 합의해 버리는 클래스 (冊 을 申 으로 읽는 등) 에 듣는 유일한 수단이에요 — 글자끼리 맞춰보는 검증은 양쪽이 같은 실수를 하면 구조적으로 아무 말도 못 하거든요. 어기면 reason "pattern_mismatch" 예요.
review"normal" | "off"optional
"off" 로 하면 그 필드에 대해 엔진이 추정한 검토 사유 (text_mismatch・low_ratio・ambiguous_occurrence 등) 를 내지 않아요. 증거는 전부 남고 판정만 보류해요. 품명이나 비고 같은 자유 기술 열에 걸어두면, 등록번호나 합계에 선 표시가 읽히게 돼요. 선언한 규칙 (required 의 missing, pattern・min/max・enum 위반) 은 못 꺼요 — 직접 쓴 규칙이 직접 쓴 다른 키로 취소되면 위험하니까요.
autoFieldsbooleanoptional
true 면 fields 를 안 줘도 LLM 이 스키마를 알아서 제안해요. 구 이름 auto_fields 도 하위호환으로 동작해요 (deprecated).
promptstringoptional
자유 기술 지시예요 (선택).

응답 필드

status"success"
성공이면 항상 "success" 예요. 오류는 HTTP 4xx/5xx 와 공통 오류 envelope(Errors 섹션)으로 가고, 이 바디로는 안 와요.
data.valuesobject
요청 스키마 그대로의 순수 사용자 데이터예요. 예약 키가 안 섞여서 그대로 DB 에 넣을 수 있어요. 값은 항상 페이지에 인쇄된 그대로 돌아와요.
data.cellsmap<path, Cell>
path 를 키로 쓰는 flat 좌표·검증 맵이에요. 키가 review.flagged[].path 와 같은 문법(items[0].price)이라, flagged 의 path 로 바로 O(1) 조회돼요. items[0] 같은 행 path 는 행 전체 union box 예요.
box{ xmin, ymin, xmax, ymax }
축 평행 사각형이에요. 0~1000 정규화예요 (data.image 로 픽셀 환산).
quad[{ x, y } × 4]
기울어진 스캔을 따라가는 4점이에요. box 와 항상 둘 다 붙어요.
verifiedboolean | null
그 좌표가 가리키는 OCR 원문과 값이 정규화 후 일치했는지 (두 독립 엔진의 합의). false 면 모델이 글자를 바꿨을 수 있어요. null 은 검증 대상 아님 (행 union 같은 기하 전용 항목).
review{ reason, reasons } | null
null 이면 통과, 값이 들어 있으면 사람 확인 권장이에요. reason: type_mismatch | out_of_range | pattern_mismatch | nobox | text_mismatch | crop_mismatch | low_ratio | weak_source | low_ocr_confidence | ambiguous_occurrence | overwide_box | missing. 앞의 셋은 호출하신 쪽이 선언한 규칙을 어긴 거라, 엔진이 추정한 사유보다 위로 랭크돼요. reason 은 언제나 그 1위 하나이고, reasons 는 어긴 규칙 전부의 배열이라 길이가 1이어도 항상 들어 있어요 (reasons[0] 은 항상 reason 과 같아요).
evidenceobject
판정의 원자료예요 — source (좌표 산출 경로: vision_symbol_match / token_id …) / match_ratio (문자 대조 일치율) / ocr_confidence (매칭 글리프에 대한 OCR 자신의 신뢰도 최솟값, 없으면 키 없음) / crop_verified (크롭 재검증 결과, 실행됐을 때만).
normalized{ value, type, method, error? }
스칼라 타입 (number / integer / date, 또는 pattern・enum 을 준 string) 을 선언한 필드에만 붙어요. data.normalized 의 그 리프가 null 이었던 이유가 여기 있어요 — error 는 not_numeric / not_an_integer / not_a_date / no_year 예요. method 는 현재 항상 "deterministic" 이에요 (추가 모델 호출 없음).
data.reviewobject
문서 한 장 분의 검증 요약이에요. 필드별 판정은 cells 쪽이고, 여기는 집계와 검토 목록이에요.
unit"field"
집계 단위예요.
declaredinteger
빈 값·안 돌아온 required 까지 센 전체 슬롯이에요 (고정 분모).
returnedinteger
비어있지 않은 값 수예요.
boxedinteger
좌표가 붙은 셀 수예요.
verifiedinteger
verified: true 인 셀 수예요.
flagged[{ path, reason, reasons }]
검토 목록이에요. 검토 건수는 이 배열의 길이 그 자체(별도 카운터 없음), path 는 cells 키와 같은 문법이에요. 값은 있는데 좌표가 없는 필드(nobox)와 안 돌아온 required(missing)는 셀이 없어서 여기에만 나와요. reason 은 랭킹 1위 하나, reasons 는 어긴 규칙 전부예요.
by_reasonobject
사유별 내역이에요 (예: { "pattern_mismatch": 1, "text_mismatch": 1 }). 한 셀이 선언한 규칙과 엔진의 의심을 동시에 어길 수 있어서, reasons 에 실린 사유를 전부 세요. 그래서 합계는 flagged 건수 이상이에요 (검토 건수 자체는 그대로 flagged.length).
notesarray
스칼라 타입(number / integer / date)을 선언하시면 붙어요 — 값은 인쇄된 그대로 돌려주고 선언한 타입은 normalized 쪽에서 썼다는 고지예요 (path / declared_type / applied_type / description).
data.normalizedobject
스칼라 타입(number / integer / date, 또는 pattern・enum 을 준 string)을 선언한 필드가 있을 때만 붙어요. values 와 완전히 같은 모양의 트리이고 리프만 그 타입으로 해석한 값이에요 (normalized.items[0].qty 가 values.items[0].qty 옆에 놓여요). 선언한 리프만 있는 성긴 트리라, 해석하지 못한 리프는 null 이고 이유는 cells[path].normalized.error 에 있어요. 해석은 결정론적이라 같은 페이지면 매번 같은 값이 나와요. values 쪽은 인쇄된 그대로 변하지 않고, 좌표와 검증이 붙어 있는 건 그쪽이에요.
data.image{ width, height }
원본 이미지의 픽셀 크기예요. 0~1000 정규화 좌표를 픽셀로 되돌릴 때 써요 (pixel_x = box.xmin / 1000 × width).
요청
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
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": "합계" }
    ]
  }'
응답
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
{
  "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 }
  }
}
POST/ocr/markdownBearer₩100

마크다운 변환

레이아웃을 살린 채로 이미지를 마크다운으로 바꿔요. 제목·문단·목록·표가 요소로 나오고, 요소마다 좌표가 붙어요.

바디 파라미터

imagestringrequired
Base64 문자열 또는 이미지 URL. base64 로 JSON 바디가 2MB 를 넘으면 URL 로 넘기거나 /upload 를 써주세요.
imageType"base64" | "url"required
image 의 타입을 직접 알려주세요. 구 이름 image_type 도 하위호환으로 동작해요 (deprecated).
promptstringoptional
레이아웃 해석에 대한 자유 기술 추가 지시예요 (선택).
includeElementsbooleanoptional
기본값 true — values.elements(내용)와 cells(요소별 좌표·검증)를 함께 돌려줘요. false 로 주면 조립된 마크다운 문자열만 나와요.

응답 필드

status"success"
성공이면 항상 "success" 예요. 오류는 HTTP 4xx/5xx 와 공통 오류 envelope(Errors 섹션)으로 가고, 이 바디로는 안 와요.
data.values.markdownstring
조립된 마크다운 문자열이에요.
data.values.elementsarray<Element>
내용만 담은 요소 배열이에요. 좌표·검증 플래그는 cells 쪽으로 분리돼 있어요. 어떤 요소도 청구하지 않은 OCR 토큰은 paragraph(evidence.source: unclaimed_tokens)로 말미에 회수돼, 흘린 문단이 사라지지 않아요.
type"heading" | "paragraph" | "list_item" | "blockquote" | "code_block" | "thematic_break" | "table"
요소의 종류예요.
textstring
요소의 본문이에요 (table 제외).
levelinteger
heading 의 제목 레벨이에요.
rowsinteger
table 의 행 수예요.
colsinteger
table 의 열 수예요.
cells[{ row, col, header, text }]
table 의 셀 배열이에요.
data.cellsmap<path, Cell>
path 를 키로 쓰는 flat 좌표·검증 맵이에요 — elements[3] 이 요소, elements[2].cells[1] 이 표의 셀. review.flagged[].path 와 같은 문법이라 flagged 에서 바로 찾아가요. includeElements: false 면 안 붙어요.
box{ xmin, ymin, xmax, ymax }
축 평행 사각형이에요. 0~1000 정규화예요 (data.image 로 픽셀 환산).
quad[{ x, y } × 4]
기울어진 스캔을 따라가는 4점이에요. box 와 항상 둘 다 붙어요.
verifiedboolean | null
그 좌표가 가리키는 OCR 원문과 값이 정규화 후 일치했는지 (두 독립 엔진의 합의). false 면 모델이 글자를 바꿨을 수 있어요. null 은 검증 대상 아님 (표 요소 자체 — 셀 각각은 검증돼요).
review{ reason, reasons } | null
null 이면 통과, 값이 들어 있으면 사람 확인 권장이에요. reason: type_mismatch | out_of_range | pattern_mismatch | nobox | text_mismatch | crop_mismatch | low_ratio | weak_source | low_ocr_confidence | ambiguous_occurrence | overwide_box | missing. 앞의 셋은 호출하신 쪽이 선언한 규칙을 어긴 거라, 엔진이 추정한 사유보다 위로 랭크돼요. reason 은 언제나 그 1위 하나이고, reasons 는 어긴 규칙 전부의 배열이라 길이가 1이어도 항상 들어 있어요 (reasons[0] 은 항상 reason 과 같아요).
evidenceobject
판정의 원자료예요 — source (좌표 산출 경로: token_id / char_matcher_fallback / unclaimed_tokens) / match_ratio (문자 대조 일치율) / ocr_confidence (매칭 글리프에 대한 OCR 자신의 신뢰도 최솟값, 없으면 키 없음) / crop_verified (크롭 재검증 결과, 실행됐을 때만).
data.reviewobject
문서 한 장 분의 검증 요약이에요. 요소별 판정은 cells 쪽이고, 여기는 집계와 검토 목록이에요.
unit"element"
집계 단위예요.
totalinteger
셀 단위 총 수예요 (표는 셀 각각을 세요).
boxedinteger
좌표가 붙은 셀 수예요.
verifiedinteger
verified: true 인 셀 수예요.
flagged[{ path, reason, reasons }]
검토 목록이에요. 검토 건수는 이 배열의 길이 그 자체예요 (별도 카운터 없음). path 는 cells 의 키와 같은 문법이라 그대로 조회돼요. reason 은 랭킹 1위 하나, reasons 는 어긴 규칙 전부예요 (길이 1이어도 항상 들어 있어요).
by_reasonobject
사유별 내역이에요 (예: { "pattern_mismatch": 1, "text_mismatch": 1 }). 한 셀이 선언한 규칙과 엔진의 의심을 동시에 어길 수 있어서, reasons 에 실린 사유를 전부 세요. 그래서 합계는 flagged 건수 이상이에요 (검토 건수 자체는 그대로 flagged.length).
coverageobject
recovered_blocks / vision_tokens / tokens_claimed / token_coverage — 페이지를 얼마나 건졌는지예요.
data.image{ width, height }
원본 이미지의 픽셀 크기예요. 0~1000 정규화 좌표를 픽셀로 되돌릴 때 써요 (pixel_x = box.xmin / 1000 × width).
요청
1
2
3
4
5
6
7
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"
  }'
응답
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{
  "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 }
  }
}
POST/ocr/textBearer₩100

원문 텍스트 OCR

스키마도 마크다운 문법도 없이 문서의 글자만 전부 돌려줘요. 모델이 이미지를 보고 진짜 읽기순서로 블록을 정렬하기 때문에, 다단 조판이나 기울어진 스캔에서도 문장이 뒤섞이지 않아요.

바디 파라미터

imagestringrequired
Base64 문자열 또는 이미지 URL. base64 로 JSON 바디가 2MB 를 넘으면 URL 로 넘기거나 /upload 를 써주세요.
imageType"base64" | "url"required
image 의 타입을 직접 알려주세요. 구 이름 image_type 도 하위호환으로 동작해요 (deprecated).
useLlmbooleanoptional
기본값 true — 읽기순서로 정렬하고 줄바꿈으로 끊긴 단어를 다시 붙여요. false 로 주면 Vision 전용 전사예요 (즉시 응답·LLM 비용 0, 대신 raw OCR 순서).
includeBlocksbooleanoptional
true 면 values.blocks(내용)와 cells(블록별 box / quad / verified / review)도 함께 돌려줘요. 기본값은 false 예요.
promptstringoptional
전사 프롬프트를 바꿔 넣을 수 있어요 (선택).

응답 필드

status"success"
성공이면 항상 "success" 예요. 오류는 HTTP 4xx/5xx 와 공통 오류 envelope(Errors 섹션)으로 가고, 이 바디로는 안 와요.
data.values.textstring
전문이에요. 블록을 읽기순서로 결합한 문자열이에요.
data.values.blocks[{ text }]
내용만 담은 블록 배열이에요 (includeBlocks: true 일 때). 좌표·검증 플래그는 cells 쪽이에요. 어떤 블록도 청구하지 않은 OCR 토큰은 회수 블록(evidence.source: unclaimed_tokens)으로 말미에 붙어, 흘린 문단이 조용히 사라지지 않아요.
data.cellsmap<path, Cell>
path 를 키로 쓰는 flat 좌표·검증 맵이에요 (blocks[7]). review.flagged[].path 와 같은 문법이라 flagged 에서 바로 찾아가요. includeBlocks: true 일 때 붙어요.
box{ xmin, ymin, xmax, ymax }
축 평행 사각형이에요. 0~1000 정규화예요 (data.image 로 픽셀 환산).
quad[{ x, y } × 4]
기울어진 스캔을 따라가는 4점이에요. box 와 항상 둘 다 붙어요.
verifiedboolean | null
그 좌표가 가리키는 OCR 원문과 값이 정규화 후 일치했는지 (두 독립 엔진의 합의). false 면 모델이 글자를 바꿨을 수 있어요. null 은 검증 대상 아님 (기하 전용 항목).
review{ reason, reasons } | null
null 이면 통과, 값이 들어 있으면 사람 확인 권장이에요. reason: type_mismatch | out_of_range | pattern_mismatch | nobox | text_mismatch | crop_mismatch | low_ratio | weak_source | low_ocr_confidence | ambiguous_occurrence | overwide_box | missing. 앞의 셋은 호출하신 쪽이 선언한 규칙을 어긴 거라, 엔진이 추정한 사유보다 위로 랭크돼요. reason 은 언제나 그 1위 하나이고, reasons 는 어긴 규칙 전부의 배열이라 길이가 1이어도 항상 들어 있어요 (reasons[0] 은 항상 reason 과 같아요).
evidenceobject
판정의 원자료예요 — source (좌표 산출 경로: token_id / char_matcher_fallback / unclaimed_tokens / vision_paragraph) / match_ratio (문자 대조 일치율) / ocr_confidence (매칭 글리프에 대한 OCR 자신의 신뢰도 최솟값, 없으면 키 없음) / crop_verified (크롭 재검증 결과, 실행됐을 때만).
data.reviewobject
문서 한 장 분의 검증 요약이에요. Vision 전용 경로(useLlm: false)에서도 항상 실려요.
unit"block"
집계 단위예요.
totalinteger
블록의 총 수예요.
boxedinteger
좌표가 붙은 셀 수예요.
verifiedinteger
verified: true 인 셀 수예요.
flagged[{ path, reason, reasons }]
검토 목록이에요. 검토 건수는 이 배열의 길이 그 자체예요 (별도 카운터 없음). path 는 cells 의 키와 같은 문법이라 그대로 조회돼요. reason 은 랭킹 1위 하나, reasons 는 어긴 규칙 전부예요 (길이 1이어도 항상 들어 있어요).
by_reasonobject
사유별 내역이에요 (예: { "pattern_mismatch": 1, "text_mismatch": 1 }). 한 셀이 선언한 규칙과 엔진의 의심을 동시에 어길 수 있어서, reasons 에 실린 사유를 전부 세요. 그래서 합계는 flagged 건수 이상이에요 (검토 건수 자체는 그대로 flagged.length).
coverageobject
recovered_blocks / vision_tokens / tokens_claimed / token_coverage — 페이지를 얼마나 건졌는지예요 (useLlm: true 인 LLM 경로에서만).
data.image{ width, height }
원본 이미지의 픽셀 크기예요. 0~1000 정규화 좌표를 픽셀로 되돌릴 때 써요 (pixel_x = box.xmin / 1000 × width).
data.source"llm" | "vision"
"llm" 은 읽기순서를 정렬한 경로, "vision" 은 LLM 실패 시 자동 폴백이에요 (사유는 warning 에 실려요).
요청
1
2
3
4
5
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
  }'
응답
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
{
  "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"
  }
}
GET/spaceBearer

트리 조회

MySpace 의 폴더/시트/메모를 한눈에 보여드려요. path 와 depth 로 범위를 좁힐 수 있어요.

쿼리 파라미터

pathstringoptional
"/" 로 구분해요. 폴더는 이름, 시트는 이름 (부모 안에서 유일) 또는 /create 가 알려준 uniqueKey 로 가리킬 수 있어요. 기본값은 "/" 예요.
depthinteger 1..10optional
재귀 깊이예요. 기본은 1.

응답 필드

pathstring
요청에서 지정한 기준 path 예요.
depthinteger
적용된 재귀 깊이예요.
itemsarray<Item>
하위 아이템 목록이에요.
pathstring
아이템의 전체 path 예요. /view・/upload・/remove 에 그대로 넘길 수 있어요.
namestring
표시 이름이에요.
type"folder" | "sheet" | "doc" | "memo" | "img"
아이템의 종류예요. doc 은 문서 묶음(.md / .txt)이에요.
uniqueKeystring
이름과 무관한 안정 키예요 (folder 제외). path 세그먼트로도 쓸 수 있어요.
createdAtinteger (epoch ms)
생성 시각이에요.
extensionsobject | null
부가 메타데이터예요 (없으면 null, folder 제외).
요청
1
2
curl https://api.space-ocr.com/space?path=/&depth=1 \
  -H "Authorization: Bearer YOUR_API_KEY"
응답
1
2
3
4
5
6
7
8
9
10
11
{
  "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 도 같이 와요.
GET/viewBearer

내용 조회

폴더/시트/문서 묶음/메모/이미지 **모든 종류**의 내용을 돌려드려요. 문서 묶음은 pages 배열, 시트는 rows 배열로 나와요. **쿼리(where / sort / select / limit / offset / boxes)는 시트에서만 동작해요** — 다른 종류에 붙이면 무시되고 전체가 그대로 나와요. 시트 행은 **업로드 시각(createdAt) 오름차순**으로 나와요. POST /edit・POST /remove 의 `row: N` 과 같은 순서라서, 응답의 N 번째 행이 곧 `row: N` 이에요.

쿼리 파라미터

pathstringrequired
대상 path 예요.
wherestring | string[]optional
【시트 전용】 행 필터예요. 예: total>=40000 / vendor~ABC. 반복해서 넣으면 AND 로 묶여요. 연산자는 = != > >= < <= ~ (~ 는 부분일치) 가 있어요. 시트 컬럼이랑 name / ocrStatus / createdAt 까지 대상이에요. 값은 페이지에 인쇄된 그대로의 문자열로 저장되므로 비교 전에 숫자로 바꿔 봐요: 전각을 반각으로 정규화하고 통화기호(¥ ¥ $ ₩ € £ 円 元 원 등)·자릿수 구분자·회계식 음수 표기((1,200) / △1,200 / 끝 하이픈)를 떼어 낸 뒤 순수한 십진수면 숫자로 비교하고, 아니면 문자열로 비교해요 (양쪽 다 숫자일 때만 숫자 비교). 날짜·전화번호·12% 같은 건 숫자로 안 읽혀요.
sortstring | string[]optional
【시트 전용】 정렬 기준이에요. 예: total:desc / -invoice_date. 여러 번 넣으면 tie-break 으로 쓰여요. where 와 같은 규칙으로 숫자화를 시도해서, 둘 다 숫자면 숫자 순, 아니면 문자열 순으로 정렬해요.
selectstringoptional
【시트 전용】 콤마로 구분한 반환 컬럼명이에요. 예: vendor,total.
limitinteger 1..500optional
【시트 전용】 한 번에 받을 행 수 상한이에요. 문서 묶음의 pages 에는 안 걸려요.
offsetintegeroptional
【시트 전용】 페이징용 건너뛰기 수예요. 응답의 nextOffset 이랑 같이 써요.
boxes"0" | "1" | "true" | "false"optional
【시트 전용】 0 / false 로 주면 행의 cells(좌표·검증 맵)를 뺀 가벼운 응답으로 와요. values / review / image 는 남아요. select= 는 values 키와, 첫 세그먼트가 일치하는 cells 경로에 적용돼요.

응답 필드

type"folder" | "sheet" | "doc" | "memo" | "img"
대상의 종류예요. 아래 필드들은 type 에 따라 붙는 게 달라요.
pathstring
대상 path 예요.
namestring
표시 이름이에요 (folder 제외).
columnsarray<ColumnSpec>
【시트】 이 시트의 컬럼 스키마예요 (POST /create 와 같은 모양).
totalinteger
【시트】 시트 전체 행 수 /【doc】 페이지 수예요.
matchedinteger
【시트】 where 를 통과한 행 수예요.
offset / limit / nextOffsetinteger | null
【시트】 페이징 상태예요. nextOffset 을 다음 offset 으로 넘겨요 (끝나면 null).
rowsarray<Row>
【시트】 행 배열이에요. createdAt 오름차순 — POST /edit・POST /remove 의 row: N 과 같은 순서예요.
rowKeystring
행의 안정 키예요. POST /edit 의 row 로 그대로 넘길 수 있어요.
namestring
원본 파일명이에요.
createdAtinteger (epoch ms)
업로드 시각 — 기본 행 순서의 근거예요.
imageUrlstring | null
원본 이미지 URL 이에요.
ocrStatus"pending" | "done" | "failed"
OCR 상태예요.
valuesobject | null
추출된 값이에요 (컬럼명이 키). POST /ocr/fields 의 data.values 와 같은 순수 사용자 데이터예요. OCR 전이면 null.
cellsmap<path, Cell>
POST /ocr/fields 와 같은 좌표·검증 맵이에요. boxes=0 이면 생략돼요.
reviewobject
POST /ocr/fields 와 같은 검증 요약이에요 (unit: "field").
image{ width, height }
원본 이미지의 픽셀 크기예요.
mode"markdown" | "text"
【doc】 묶음의 변환 모드예요.
pagesarray<Page>
【doc】 페이지 배열이에요 (업로드순). 각 페이지는 values(mode 에 따라 { markdown, elements } 또는 { text, blocks }) + cells + review + image — POST /ocr/markdown・/ocr/text 와 같은 v2 구조예요. OCR 전 페이지는 values: null 만 있어요.
pageKeystring
페이지의 안정 키예요.
namestring
원본 파일명이에요.
imageUrlstring | null
원본 이미지 URL 이에요.
ocrStatus"pending" | "done" | "failed"
OCR 상태예요.
valuesobject | null
mode=markdown 이면 { markdown, elements }, mode=text 면 { text, blocks } 예요.
cellsmap<path, Cell>
POST /ocr/markdown・/ocr/text 와 같은 좌표·검증 맵이에요 (elements[3] / blocks[7] …).
reviewobject
같은 엔드포인트와 같은 검증 요약이에요 (unit: "element" / "block").
image{ width, height }
원본 이미지의 픽셀 크기예요.
itemsarray
【folder】 자식 아이템 목록이에요 ({ path, name, type, uniqueKey? }).
textstring
【memo】 메모 본문이에요.
imageUrl / ocrStatusstring | null
【img】 원본 이미지 URL 과 OCR 상태예요.
요청
1
2
3
4
5
6
7
8
# 다중 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"
응답
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// 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" }
POST/createBearer

생성

부모 폴더 아래에 folder / sheet / doc / memo 를 만들어요. 시트는 OCR 스키마(columns) 랑 prompt 를, doc(문서 묶음)은 mode 를 가져요.

바디 파라미터

pathstringrequired
부모 폴더의 path 예요.
type"folder" | "sheet" | "doc" | "memo"required
어떤 아이템을 만들지 정해요. doc 은 문서 묶음(.md / .txt) 이에요.
namestringrequired
표시할 이름이에요.
textstringoptional
메모 본문이에요 (type=memo 일 때만 써요).
columnsarray<ColumnSpec>optional
시트의 OCR 스키마예요 (type=sheet 용). 이후 이 시트에 올리는 사진은 매번 이 스키마대로 추출돼요.
idstringoptional
컬럼의 안정 ID 예요. 생략하면 자동으로 만들어 드려요.
namestringrequired
컬럼 이름이에요. 뽑아낸 값이 이 이름으로 행에 들어가요.
typestringrequired
값의 형태예요. 단일 값은 string, 명세행처럼 반복되는 줄은 array 로 두고 children 으로 줄 안의 항목을 선언해요.
descriptionstringoptional
그 값이 어디 있는지 알려주는 힌트예요 (예: "합계 오른쪽").
childrenarray<ColumnSpec>optional
type 이 array 인 컬럼의 하위 필드예요. 명세행의 각 셀이 돼요.
requiredbooleanoptional
true 인 컬럼은 그 값이 비어서 오거나 읽히지 않았을 때 그 행의 review.flagged 에 reason "missing" 으로 나타나요 — 업로드할 때마다 적용되고 추출 동작은 그대로예요. 늘 인쇄되는 값에만 켜주세요.
labelstring | string[]optional
값 옆에 인쇄된 라벨이에요 (예: "합계"). 같은 값이 페이지에 여러 번 찍혀 있을 때 좌표를 그 라벨 옆 등장에 앵커해줘요 (v64). string 컬럼 전용이고 라벨이 페이지에 정확히 1회일 때만 작동하며, 모델에게는 전달되지 않아요 — 추출 텍스트는 그대로고 좌표만 바뀌어요.
promptstringoptional
시트의 추출 지시 프롬프트예요 (선택).
mode"markdown" | "text"optional
문서 묶음의 변환 모드예요 (type=doc 용, 기본값 markdown). markdown 은 레이아웃 보존, text 는 원문 그대로예요.

응답 필드

pathstring
만들어진 아이템의 path 예요. sheet / doc / memo 는 uniqueKey 가 path 에 박혀서 돌아와요.
type"folder" | "sheet" | "doc" | "memo"
만들어진 종류예요.
uniqueKeystring
이름과 무관한 안정 키예요 (folder 제외). 이후 /view・/upload 의 path 세그먼트로 그대로 쓸 수 있어요.
요청
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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": "청구서에서 금액과 날짜 추출"
  }'
응답
1
2
3
4
5
6
7
8
// 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" 으로 나타나요.
POST/uploadBearer₩100 × N

이미지 업로드

시트 또는 문서 묶음에 이미지를 한 장 이상 올려요. multipart/form-data 예요. 기본은 비동기로, jobs 가 먼저 돌아오고 완료는 webhook 으로 알려드려요.

폼 필드 (multipart)

pathstringrequired
업로드할 시트 또는 문서 묶음의 path 예요. 묶음의 mode(markdown / text) 에 따라 변환 방식이 정해져요.
filesfile (repeatable)required
이미지 파일이에요. 여러 장 보낼 땐 files 를 반복해서 넣어주세요. 한 요청에 최대 20개, 파일당 최대 20MB 예요.
waitbooleanoptional
true 로 주면 동기로 실행해요 (장당 최대 30s 까지 기다리고, 넘으면 status:"pending" 으로 돌려드려요). 한 장 업로드할 때 쓰기 좋아요.

응답 필드

pathstring
업로드한 대상의 path 예요.
jobsarray<Job>
비동기(기본)일 때 와요. 파일 하나가 잡 하나고, 완료는 ocr.completed 웹훅이나 GET /jobs/{jobId} 폴링으로 받아요.
uniqueKeystring
만들어진 행/페이지의 안정 키예요.
originalNamestring
원본 파일명이에요.
jobIdstring
GET /jobs/{jobId} 에 넘길 잡 ID 예요.
status"pending"
접수 시점엔 항상 pending 이에요.
resultsarray<Result>
wait=true 면 jobs 대신 와요. Job 의 속성에 더해, 끝난 건 mode 와 result 를 가져요 — result 는 GET /jobs 와 같은 v2 구조({ values, cells, review, image })예요. 30s 안에 못 끝난 건 status: "pending" 으로 남으니 /jobs 로 폴링해주세요.
요청
1
2
3
4
5
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"
응답
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
34
35
36
37
38
39
40
41
42
43
44
// 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"
    }
  }
}
POST/editBearer

시트 행/메모 편집

시트 셀 값이나 메모 본문을 덮어써요. anyOf 라서 (path, row, column, value) 둘 중 하나, 아니면 (path, text) 로 보내요. **편집되는 건 시트랑 메모뿐이에요** — 문서 묶음(.md / .txt)은 판독 결과 자체라 400 으로 거절돼요.

바디 파라미터

pathstringrequired
대상 시트/메모의 path 예요.
rowinteger | stringoptional
정수 인덱스 (음수는 끝에서부터) 나 rowKey 문자열로 줘요. 시트 편집할 땐 꼭 필요해요.
columninteger | stringoptional
정수 컬럼 인덱스나 컬럼명/id 로 줘요. 시트 편집할 땐 꼭 필요해요.
valueanyoptional
새로 덮어쓸 값이에요. 시트 편집할 땐 꼭 필요해요.
textstringoptional
새 메모 본문이에요. 메모 편집할 땐 꼭 필요해요.

응답 필드

okboolean
true 면 반영이 끝난 거예요.
patchedobject
실제로 적용된 변경 내용이에요. 시트 편집이면 { row, column, value }.
요청
1
2
3
4
5
6
7
8
9
10
11
# 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":"새 본문"}'
응답
1
{ "ok": true, "patched": { "row": "img_abc", "column": "amount", "value": "12000" } }
POST/removeBearer

삭제 (cascade)

폴더/시트/메모/이미지를 지워요. 폴더를 지우면 하위 메타데이터・flat 항목・Storage 까지 줄줄이 같이 삭제돼요.

바디 파라미터

pathstringrequired
지울 대상의 path 예요.

응답 필드

okboolean
true 면 삭제가 끝난 거예요 (폴더는 하위까지 cascade 완료).
요청
1
2
3
4
curl -X POST https://api.space-ocr.com/remove \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"path":"/invoices/2024"}'
응답
1
{ "ok": true }
GET/jobs/{jobId}Bearer

OCR 잡 폴링

POST /upload (비동기) 가 알려준 jobId 의 상태를 확인해요. webhook 을 안 쓸 때 유용해요.

패스 파라미터

jobIdstringrequired
/upload 응답의 jobs[].jobId 예요.

응답 필드

jobIdstring
잡 ID 예요.
status"pending" | "done" | "failed"
처리 상태예요. failed 는 자동 환불돼 있어요.
uniqueKeystring
만들어진 행/페이지의 안정 키예요.
pathstring
아이템의 path 예요.
sheetRef / docRefstring | null
업로드 대상이 시트면 sheetRef, 문서 묶음이면 docRef 에 uniqueKey 가 들어와요 (다른 쪽은 null).
mode"sheet" | "markdown" | "text"
업로드 대상이 정하는 출력 형식이에요.
resultobject
status 가 done 일 때만 와요. { values, cells, review, image } — mode 와 무관하게 OCR 엔드포인트(/ocr/fields・/ocr/markdown・/ocr/text)와 같은 v2 구조고, values 안쪽만 mode 를 따라요. ocr.completed 웹훅의 data.result 도 같은 모양이에요.
요청
1
2
curl https://api.space-ocr.com/jobs/job_xxx \
  -H "Authorization: Bearer YOUR_API_KEY"
응답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "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 }
  }
}
GET/amountBearer

잔액 및 무료 할당

지금 잔액과 남은 무료 할당을 알려드려요.

응답 필드

freeobject
매월 무료 할당이에요.
used / limit / remaininginteger
이번 사이클의 사용량·상한·남은 수예요.
cycleStartinteger (epoch ms)
사이클 시작 시각이에요.
cycleEndinteger (epoch ms)
사이클 종료 시각이에요 (여기서 리셋).
flatfeeobject
정액 플랜이에요. 미가입이면 enabled: false 고 다른 속성은 안 붙어요.
enabledboolean
가입 중인지예요.
used / limit / remaininginteger
이번 사이클의 사용량·상한·남은 수예요.
cycleStart / cycleEndinteger (epoch ms)
사이클 기간이에요.
nextBillingAtinteger (epoch ms)
다음 결제 시각이에요.
interval"monthly"
결제 주기예요.
renewalboolean
자동 갱신 여부예요.
planstring
플랜 이름이에요 (예: "pro").
balanceinteger
충전 잔액이에요. 단위는 통화가 아니라 스캔 수예요.
currency"scans"
잔액의 단위예요.
perCallCostinteger
한 번 호출에 차감되는 스캔 수예요 (1).
요청
1
2
curl https://api.space-ocr.com/amount \
  -H "Authorization: Bearer YOUR_API_KEY"
응답
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
{
  "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.
// 소진 순서도 이 순서예요 (무료 → 정액 → 잔액).
GET/health

헬스 체크

인증 없이 부르는 헬스 체크예요.

응답 필드

status"ok"
서비스가 응답할 수 있으면 ok 예요.
versionstring
API 버전이에요.
timeinteger (epoch ms)
서버 시각이에요.
요청
1
curl https://api.space-ocr.com/health
응답
1
{ "status": "ok", "version": "v1", "time": 1716700000000 }

개요

스페이스 전체에 Webhook URL 하나만 등록해두면, 모든 이벤트가 HMAC 서명이랑 같이 그쪽으로 가요. 설정은 Developer → Webhooks 에서 하거나, 아래의 Webhook 관리 엔드포인트로 해도 돼요.

이벤트

모든 이벤트는 동일한 envelope (event / deliveryId / occurredAt / apiVersion / data) 으로 와요.

item.createdeventoptional
/create 로 폴더/시트/메모가 만들어졌어요
upload.receivedeventoptional
/upload 로 이미지가 들어왔어요
ocr.completedeventoptional
OCR 끝났어요. data.mode 가 출력 형식(sheet / markdown / text), data.result 에 결과가 있어요
ocr.failedeventoptional
OCR 실패 (자동 환불해드렸어요)
webhook.testeventoptional
/webhook/test 로 직접 쏘는 테스트예요

페이로드 예시 — ocr.completed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "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 두 개예요.

1
2
3
4
5
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분 넘게 차이나는 건 거절해주세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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일 동안 보관해드려요.

수동 재발송은 POST /webhooks/deliveries/{deliveryId}/redeliver 로 해요. 자세한 건 배달 이력 섹션을 봐주세요.
GET/webhookBearer

현재 webhook 설정

지금 스페이스에 등록돼 있는 webhook URL 과 상태를 알려드려요.

응답 필드

configuredboolean
등록돼 있는지예요. false 면 다른 필드는 안 붙어요.
urlstring
이벤트를 받는 URL 이에요.
activeboolean
전송이 켜져 있는지예요.
secretMaskedstring
서명 키의 끝 4자만 남긴 마스킹이에요. 평문은 등록·회전 그 한 번만 와요.
createdAt / updatedAtinteger (epoch ms)
등록·갱신 시각이에요.
요청
1
2
curl https://api.space-ocr.com/webhook \
  -H "Authorization: Bearer YOUR_API_KEY"
응답
1
2
3
4
5
6
7
8
9
10
11
{
  "configured": true,
  "url": "https://example.com/hooks/space-ocr",
  "active": true,
  "secretMasked": "••••a1b2",
  "createdAt": 1716700000000,
  "updatedAt": 1716700000000
}

// 설정 안 돼 있을 때
{ "configured": false }
PUT/webhookBearer

webhook 설정 생성·갱신

스페이스 전체 webhook URL 을 등록하거나 바꿔요. rotateSecret 으로 서명 키도 새로 발급받을 수 있어요.

바디 파라미터

urlstring (uri)required
이벤트를 받을 URL 이에요.
activebooleanoptional
전송을 켤지 정해요 (기본은 true).
rotateSecretbooleanoptional
true 면 서명 키를 새로 발급하고, 새 secret 을 한 번만 돌려드려요. 처음 등록할 땐 이 값을 안 줘도 secret 이 발급되고, 그때도 평문으로 한 번 나와요.

응답 필드

configured / url / active / secretMasked / createdAt / updatedAt
GET /webhook 과 같은 필드예요.
secretstring
새로 발급되거나 rotateSecret 일 때만, 이 한 번만 평문으로 와요. 나중에 다시 꺼낼 수 없으니 바로 보관해주세요.
요청
1
2
3
4
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}'
응답
1
2
3
4
5
6
7
8
9
{
  "configured": true,
  "url": "https://example.com/hooks/space-ocr",
  "active": true,
  "secretMasked": "••••a1b2",
  "secret": "kJ8s…",   // 새로 발급/회전할 때만, 이 한 번만 평문
  "createdAt": 1716700000000,
  "updatedAt": 1716700000000
}
DELETE/webhookBearer

webhook 설정 삭제

등록한 webhook 을 지워요. 그 뒤로는 이벤트가 안 가요.

응답 필드

okboolean
true 면 삭제된 거예요. 그 뒤로는 이벤트가 안 가요.
요청
1
2
curl -X DELETE https://api.space-ocr.com/webhook \
  -H "Authorization: Bearer YOUR_API_KEY"
응답
1
{ "ok": true }
POST/webhook/testBearer

테스트 이벤트 발사

설정해둔 URL 로 webhook.test 이벤트를 바로 쏴드려요. 받는 쪽 구현이 잘 됐는지 확인할 때 좋아요.

응답 필드

okboolean
true 면 전송 큐에 실렸어요.
deliveryIdstring
발행된 배달 ID 예요. /webhooks/deliveries/{deliveryId} 로 결과를 추적할 수 있어요.
요청
1
2
curl -X POST https://api.space-ocr.com/webhook/test \
  -H "Authorization: Bearer YOUR_API_KEY"
응답
1
{ "ok": true, "deliveryId": "dlv_xxx" }
GET/webhooks/deliveriesBearer

최근 전달 이력

최근 webhook 배달 로그를 보여드려요. 디버깅할 때 써요.

쿼리 파라미터

status"pending" | "success" | "dead"optional
배달 상태로 필터해요.
limitinteger 1..200optional
반환 건수 상한이에요. 기본 50.

응답 필드

itemsarray<Delivery>
최신순 배달 로그예요. 로그는 30일 보관돼요.
deliveryIdstring
배달 ID 예요.
eventstring
이벤트 이름이에요 (ocr.completed 등).
urlstring
보낸 URL 이에요.
path / uniqueKeystring
대상 아이템의 path 와 키예요 (해당 이벤트만).
status"pending" | "success" | "dead"
pending 은 재시도 대기, dead 는 모든 시도가 끝난 상태예요.
attemptsinteger
시도 횟수예요.
lastAttemptobject
마지막 시도의 상세예요 — { at, attemptIndex, responseStatus, error, durationMs, responsePreview }.
occurredAtinteger (epoch ms)
이벤트 발생 시각이에요.
nextAttemptAtinteger | null
다음 재시도 예정이에요. 없으면 null.
completedAtinteger | null
성공 시각이에요.
요청
1
2
curl https://api.space-ocr.com/webhooks/deliveries \
  -H "Authorization: Bearer YOUR_API_KEY"
응답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "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
    }
  ]
}
GET/webhooks/deliveries/{deliveryId}Bearer

배달 상세

고른 배달의 전체 페이로드랑 시도 이력을 다 보여드려요.

패스 파라미터

deliveryIdstringrequired
/webhooks/deliveries 의 deliveryId 예요.

응답 필드

deliveryIdstring
배달 ID 예요.
eventstring
이벤트 이름이에요.
occurredAtinteger (epoch ms)
이벤트 발생 시각이에요.
payloadobject
받는 쪽에 보낸 이벤트 본문 전체예요 (envelope 의 data 까지).
attempts[{ at, responseStatus, ok }]
시도 이력이에요.
요청
1
2
curl https://api.space-ocr.com/webhooks/deliveries/dlv_xxx \
  -H "Authorization: Bearer YOUR_API_KEY"
응답
1
2
3
4
5
6
7
8
9
{
  "deliveryId": "dlv_xxx",
  "event": "ocr.completed",
  "occurredAt": 1716700000000,
  "payload": { /* full event body */ },
  "attempts": [
    { "at": 1716700000000, "responseStatus": 200, "ok": true }
  ]
}
POST/webhooks/deliveries/{deliveryId}/redeliverBearer

수동 재발송

실패한 배달을 수동으로 다시 쏴드려요.

패스 파라미터

deliveryIdstringrequired
다시 보낼 deliveryId 예요.

응답 필드

okboolean
true 면 재발송 큐에 실렸어요.
deliveryIdstring
같은 ID 를 그대로 다시 써요 (새 ID 를 만들지 않아요). 배달 로그의 status 가 pending 으로 돌아가고 attempts 에 시도가 덧붙어요.
요청
1
2
curl -X POST https://api.space-ocr.com/webhooks/deliveries/dlv_xxx/redeliver \
  -H "Authorization: Bearer YOUR_API_KEY"
응답
1
2
3
4
{ "ok": true, "deliveryId": "dlv_xxx" }

// 같은 deliveryId 를 그대로 다시 씁니다 (새 ID 를 만들지 않아요). 배달 로그의
// status 가 pending 으로 돌아가고 attempts 에 시도가 덧붙습니다.

개요

MCP 서버는 이 API 를 AI 에이전트의 툴로 그대로 열어 줍니다. 읽기 3종에 더해 폴더와 시트를 만들고, 사진을 올리고, 쌓인 행을 조건 걸어 꺼내는 것까지 에이전트가 합니다. 안에서 부르는 건 같은 REST 라우트라 과금도 같습니다.

연결

설치할 건 없습니다. 헤더를 설정할 수 있는 클라이언트는 API 키를 베어러 토큰으로 그대로 보내면 됩니다. 헤더를 못 넣는 Claude 데스크톱/모바일/claude.ai 에서는 이 URL 을 커스텀 커넥터로 추가하면 OAuth 동의 화면이 열려서, 어떤 API 키로 동작할지 고르면 됩니다. 어느 쪽이든 키는 그 요청에만 쓰이고 서버에 저장되지 않습니다.

1
2
3
# 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)

1
2
3
4
5
6
7
8
{
  "mcpServers": {
    "space-ocr": {
      "url": "https://mcp.space-ocr.com/mcp",
      "headers": { "Authorization": "Bearer YOUR_API_KEY" }
    }
  }
}
그 밖의 클라이언트에서도 표준 Streamable HTTP 엔드포인트라 같은 URL 과 헤더로 붙습니다. 이미지는 서버에서 받을 수 있는 https:// URL 이나 base64 / data URI 로 보내주세요.

툴 목록

읽기 3개와 작업공간 8개예요. 과금은 대응하는 REST 라우트와 같고, 읽기와 이미지 업로드만 크레딧을 씁니다.

ocr_extract사진 한 장에서 이름 붙인 필드를 뽑아요. fields 스키마를 직접 정하거나 autoFields 로 제안받아요.1 크레딧
ocr_markdown레이아웃을 살린 마크다운이에요. 요소마다 좌표가 붙어요.1 크레딧
ocr_text읽기 순서를 되살린 원문 텍스트예요. 블록마다 좌표가 붙어요.1 크레딧
space_list트리를 훑어봐요 (GET /space).무료
space_view아이템 내용을 읽어요. 시트는 where / sort / select / limit 으로 조회해요 (GET /view). 좌표는 boxes: true 일 때만 옵니다.무료
space_create폴더 / 시트 / 문서 묶음 / 메모를 만들어요 (POST /create).무료
space_upload시트나 묶음에 이미지를 최대 20장 올려요 (POST /upload).장당 1 크레딧
space_job비동기 업로드 잡 상태를 확인해요 (GET /jobs).무료
space_edit시트 셀 값이나 메모 본문을 고쳐요 (POST /edit).무료
space_balance남은 무료 사용량과 플랜 한도, 잔액을 봐요 (GET /amount).무료
space_delete아이템을 삭제해요 (POST /remove, 폴더는 안의 것까지). 2단계로 동작해요: confirm 없이 부르면 아무것도 안 지우고 사라질 항목 수와 서명된 토큰만 돌려줘요. 사용자에게 보여 주고 동의를 받은 뒤 그 토큰을 붙여 다시 부르면 삭제돼요. 루트는 거부해요.무료

삭제는 2단계

space_delete 를 `confirm` 없이 부르면 아무것도 지우지 않고 «사라질 것» 만 돌아옵니다: 대상, 그 아래의 폴더/시트/문서 묶음/메모/이미지 개수, 샘플, 그리고 `confirm` 토큰이에요. 이 토큰은 호출한 API 키와 대상 경로에 묶인 서명이라 모델이 지어낼 수 없고, 그래서 사용자에게 보여 주는 단계를 건너뛸 수 없습니다. 동의를 받으면 그 토큰을 붙여 다시 부르면 돼요. 토큰은 10~20분 동안 유효하고, 루트 경로는 언제나 거부됩니다.
삭제는 되돌릴 수 없습니다. 폴더를 지우면 안에 있던 이미지까지 함께 사라집니다.
툴 말고 MCP 리소스와 프롬프트도 함께 제공합니다. 리소스(space-ocr://guide/schemas · /verification · /queries · /workflows)에는 스키마 짜는 법, 검증 플래그 읽는 법, 필터 쓰는 법, 일괄 처리 순서가 담겨 있고 필요할 때만 읽힙니다. 프롬프트(file_documents · review_flagged · ask_documents)는 정형 작업 절차예요. 둘 다 MCP 선택 기능이라 클라이언트에 따라 지원 여부가 갈립니다.