OCR Direct API는 SaaS 환경에서 문서 파일을 업로드하여 문서 분류(Classification) 및 키-값 추출(Key-Value Extraction)을 수행하는 API입니다. 템플릿 ID, 페이지 범위, 멀티 페이지 처리 옵션을 params JSON으로 전달하고, 법인인감 매칭은 reference_seal_image 파일을 함께 업로드해 요청할 수 있습니다.
동기 API 제약 — 본 엔드포인트는 요청 응답 body 에 OCR 결과를 즉시 반환하는 동기 API 입니다. 따라서 결과를 나중에 받아야 하는 예약 실행(schedule) 과, 별도 검수 단계가 없는 본 API 에서 의미가 없는 수동 검수 생략(skip_manual_review) 은 지원하지 않으며, advanced_options로 활성화해 전달하면 400으로 거부됩니다. 법인인감 매칭은 advanced_options.corporate_seal_match가 아니라 reference_seal_image 파일 필드로 요청합니다.
reference_seal_image는 선택 파일 필드입니다. 함께 업로드하면 추출 결과의 각 페이지를 이 참조 인감 이미지와 비교한 결과를 results.seal_match로 반환합니다(미업로드 시 results.seal_match는 null). advanced_options에 schedule·skip_manual_review·corporate_seal_match를 enabled=true로 전달하면 400 Bad Request가 반환됩니다(동기 API 미지원 — 위 제약 참조).
요청 파라미터
파라미터
타입
필수
설명
file
UploadFile
✓
분석할 문서 파일
params
JSON string
-
OCR 처리 옵션. multipart/form-data의 문자열 필드로 JSON 객체를 전달
reference_seal_image
UploadFile
-
법인인감 매칭용 참조 인감 이미지. 전달하면 페이지별 인감 비교 후 results.seal_match 반환. 빈 파일이면 400
미지원 옵션(schedule / skip_manual_review / corporate_seal_match) 활성화나 page_range 제약 위반은 모두 HTTP 400(error_code: BAD_REQUEST)입니다. error_message 에는 사람이 읽을 수 있는 정제된 단문(첫 번째 검증 오류)만 담깁니다(중첩 옵션이면 필드경로: 메시지 형태로 필드 경로가 앞에 붙습니다).
예: schedule.enabled=true (동기 API 미지원)
{"status_code":400,"error_code":"BAD_REQUEST","error_message":"schedule is not supported on the synchronous OCR API; the result is returned inline"}
{"status_code":400,"error_code":"BAD_REQUEST","error_message":"schedule is not supported on the synchronous OCR API; the result is returned inline"}
{"status_code":400,"error_code":"BAD_REQUEST","error_message":"schedule is not supported on the synchronous OCR API; the result is returned inline"}
각 위반의 error_message 는 다음과 같습니다.
케이스
error_message
schedule.enabled=true
schedule is not supported on the synchronous OCR API; the result is returned inline
skip_manual_review.enabled=true
skip_manual_review is not supported on the synchronous OCR API; there is no review step
corporate_seal_match.enabled=true
corporate_seal_match via advanced_options is not supported; upload a reference seal image with the reference_seal_image file field instead
page_range.enabled=true, start_page/end_page 누락
advanced_options.page_range: start_page and end_page are required when page_range is enabled
page_range의 start_page > end_page
advanced_options.page_range: start_page must be <= end_page
corporate_seal_match.enabled=true이면서 reference_file_id까지 누락한 경우엔 중첩 검증이 먼저 걸려 advanced_options.corporate_seal_match: reference_file_id is required when corporate_seal_match is enabled 메시지가 나올 수 있습니다. 어느 쪽이든 400이며, 인감 매칭은 reference_seal_image 파일 필드로 요청해야 합니다.
3.3 크레딧 오류 (400 / 404)
크레딧을 차감하기 전 잔액을 확인하며, 부족 시 400 입니다. 부가 필드가 top-level 로 병기됩니다(detail 중첩 아님). 차감량은 처리 페이지 수 기준 — 페이지당 1크레딧(모드 무관).
OCR Direct Async API는 처리 시간이 긴 문서나 외부 시스템 자동 연동을 위해 비동기로 OCR 작업을 제출하고 결과를 polling 또는 webhook 콜백으로 받는 API입니다. 동기 API(POST /api/v2/ocr/direct)와 같은 처리 파이프라인(문서 분류 + 키-값 추출)을 사용하며, 응답 스키마(results.elements, seal_match 등)는 동기 API와 동일합니다.
언제 동기 / 언제 비동기를 쓰나
동기 (/api/v2/ocr/direct) — 짧은 문서, 디버깅, 단순 스크립트. 응답 body 에서 결과를 즉시 받음
비동기 (/api/v2/ocr/direct/async) — 긴 문서, 묶음 처리, ERP/SI 자동연동(webhook 권장), 방화벽 안쪽 클라이언트(polling 권장)
작업 제출 시엔 크레딧 잔액이 있는지만 확인합니다(잔액이 0이면 400 INSUFFICIENT_CREDIT로 거절). 실제 페이지 수 기준 차감은 처리(워커) 단계에서 이뤄집니다. 처리 실패 시 서버 오류 사유는 즉시 환불, 휴먼 에러(잘못된 파일 등) 사유는 차감 유지됩니다(4.6 비동기 API 정책 참조).
4.2 작업 결과 조회 — GET /api/v2/ocr/direct/jobs/{job_id}
요청 예시
curl-X GET 'https://agent-api.koreadeep.com/api/v2/ocr/direct/jobs/f3b1c8d2-9e4a-4f8b-9c3d-1a2b3c4d5e6f' \
-H'accept: application/json' \
-H'x-api-key: kdl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
curl-X GET 'https://agent-api.koreadeep.com/api/v2/ocr/direct/jobs/f3b1c8d2-9e4a-4f8b-9c3d-1a2b3c4d5e6f' \
-H'accept: application/json' \
-H'x-api-key: kdl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
curl-X GET 'https://agent-api.koreadeep.com/api/v2/ocr/direct/jobs/f3b1c8d2-9e4a-4f8b-9c3d-1a2b3c4d5e6f' \
-H'accept: application/json' \
-H'x-api-key: kdl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
results 와 metadata 객체 스키마는 동기 API 와 완전히 동일합니다(2장 참조).
실패 응답 (200 OK)
작업이 실패해도 HTTP 응답 자체는 200(작업 조회 자체는 성공)입니다. 실패 사유는 본문의 status, error_code, error_message 로 식별합니다.
{"job_id":"f3b1c8d2-9e4a-4f8b-9c3d-1a2b3c4d5e6f","status":"failed","created_at":"2026-06-29T17:13:52Z","started_at":"2026-06-29T17:13:53Z","completed_at":"2026-06-29T17:14:30Z","expires_at":"2026-06-30T17:13:52Z","error_code":"OCR_ENGINE_ERROR","error_message":"OCR API error: upstream timeout"}
{"job_id":"f3b1c8d2-9e4a-4f8b-9c3d-1a2b3c4d5e6f","status":"failed","created_at":"2026-06-29T17:13:52Z","started_at":"2026-06-29T17:13:53Z","completed_at":"2026-06-29T17:14:30Z","expires_at":"2026-06-30T17:13:52Z","error_code":"OCR_ENGINE_ERROR","error_message":"OCR API error: upstream timeout"}
{"job_id":"f3b1c8d2-9e4a-4f8b-9c3d-1a2b3c4d5e6f","status":"failed","created_at":"2026-06-29T17:13:52Z","started_at":"2026-06-29T17:13:53Z","completed_at":"2026-06-29T17:14:30Z","expires_at":"2026-06-30T17:13:52Z","error_code":"OCR_ENGINE_ERROR","error_message":"OCR API error: upstream timeout"}
410 Gone — 결과 보관 만료
expires_at 경과 후(24시간 정책) 호출하면 결과가 정리되고 410 Gone 으로 반환됩니다.
{"status_code":410,"error_code":"JOB_EXPIRED","error_message":"Job result has been purged after 24h TTL"}
{"status_code":410,"error_code":"JOB_EXPIRED","error_message":"Job result has been purged after 24h TTL"}
{"status_code":410,"error_code":"JOB_EXPIRED","error_message":"Job result has been purged after 24h TTL"}
404 Not Found — 본인 잡 아님 / 없는 잡
다른 사용자의 job_id 를 조회하거나 존재하지 않는 job_id 면 404 입니다(정보 누출 방지를 위해 둘 다 같은 응답).
{"status_code":404,"error_code":"NOT_FOUND","error_message":"Job not found"}
{"status_code":404,"error_code":"NOT_FOUND","error_message":"Job not found"}
{"status_code":404,"error_code":"NOT_FOUND","error_message":"Job not found"}
4.3 Polling 가이드라인
권장 간격: 처리 평균 시간이 1분 미만이면 5초, 그 이상이면 10초 간격
지수 백오프 권장 (예: 5s → 10s → 20s → 30s 상한)
클라이언트 측 타임아웃은 최소 5분 이상 권장
1초 미만 폴링은 자제 — 의미 없는 부하
4.4 Webhook 콜백 (선택)
작업 제출 시 webhook_url 을 전달하면, 작업 완료 또는 실패 시점에 서버가 해당 URL로 POST 콜백을 보냅니다.
Webhook payload — 공통 필드
성공/실패 payload 는 동일한 고정 스키마 를 가지며, 성공/실패에 따라 results / metadata / error 세 필드 중 한쪽만 채워지고 나머지는 null 입니다.
필드
타입
설명
event
string
ocr_direct.completed (succeeded) 또는 ocr_direct.failed (failed)
delivered_at
string (ISO 8601 UTC)
webhook 발송 시각. 서명 헤더 t 와는 별개(발송 재시도 시 갱신됨)
job_id
string (UUID)
잡 식별자. GET /jobs/{job_id} 조회 시와 동일
status
string
succeeded 또는 failed
created_at
string | null
잡 제출 시각 (UTC ISO 8601)
completed_at
string | null
잡 종료 시각 (UTC ISO 8601)
results
object | null
성공 시 OCR 결과 (elements/metadata/seal_match), 실패 시 null. GET 응답 results 와 동일 스키마
metadata
object | null
성공 시 처리 metadata (file_info/total_time), 실패 시 null. GET 응답 metadata 와 동일 스키마
{"event":"ocr_direct.failed","delivered_at":"2026-06-29T17:14:31Z","job_id":"f3b1c8d2-9e4a-4f8b-9c3d-1a2b3c4d5e6f","status":"failed","created_at":"2026-06-29T17:13:52Z","completed_at":"2026-06-29T17:14:30Z","results":null,"metadata":null,"error":{"code":"OCR_ENGINE_ERROR","message":"OCR API error: upstream timeout"}}
{"event":"ocr_direct.failed","delivered_at":"2026-06-29T17:14:31Z","job_id":"f3b1c8d2-9e4a-4f8b-9c3d-1a2b3c4d5e6f","status":"failed","created_at":"2026-06-29T17:13:52Z","completed_at":"2026-06-29T17:14:30Z","results":null,"metadata":null,"error":{"code":"OCR_ENGINE_ERROR","message":"OCR API error: upstream timeout"}}
{"event":"ocr_direct.failed","delivered_at":"2026-06-29T17:14:31Z","job_id":"f3b1c8d2-9e4a-4f8b-9c3d-1a2b3c4d5e6f","status":"failed","created_at":"2026-06-29T17:13:52Z","completed_at":"2026-06-29T17:14:30Z","results":null,"metadata":null,"error":{"code":"OCR_ENGINE_ERROR","message":"OCR API error: upstream timeout"}}
이벤트 명
발송 시점
ocr_direct.completed
작업이 succeeded 로 전이
ocr_direct.failed
작업이 failed 로 전이 (서버 오류, 휴먼 에러, 큐 reaper 등)
Webhook 서명 검증
모든 webhook 요청은 X-DeepAgent-Signature 헤더에 HMAC-SHA256 서명을 포함합니다. 서명은 API Key 별로 발급된 webhook secret (whsec_...) 으로 검증합니다. kid 는 서명에 사용된 API Key 의 식별자로, 여러 키를 보유한 환경에서는 kid 로 해당 키의 secret 을 골라 검증합니다.
필드
의미
t
서명 생성 시각 (Unix epoch seconds)
v1
HMAC-SHA256(secret, "<t>.<raw_body>") 16진수
kid
서명에 사용된 API Key 식별자 (정수). 다중 키 환경에서 해당 키의 webhook secret 매칭에 사용
Replay 방지 — timestamp 허용 윈도우 ±5분 (300초).abs(server_now - t) > 300 이면 거부하세요. 시각 동기화를 위해 NTP 사용을 권장합니다.
검증 의사 코드 (Python):
importhmac,hashlib,timedefverify_webhook(secret: str,signature_header: str,raw_body: bytes,max_age_seconds: int = 300) -> bool:
parts = dict(p.split("=",1)forpinsignature_header.split(","))t = int(parts["t"])ifabs(time.time() - t) > max_age_seconds:
returnFalse# replay 방지 (±5분 윈도우)expected = hmac.new(secret.encode(),f"{t}.".encode() + raw_body,hashlib.sha256,).hexdigest()returnhmac.compare_digest(expected,parts["v1"])# 다중 키 보유 시: parts["kid"] 로 검증에 사용할 secret 을 선택해 secret 인자로 전달
importhmac,hashlib,timedefverify_webhook(secret: str,signature_header: str,raw_body: bytes,max_age_seconds: int = 300) -> bool:
parts = dict(p.split("=",1)forpinsignature_header.split(","))t = int(parts["t"])ifabs(time.time() - t) > max_age_seconds:
returnFalse# replay 방지 (±5분 윈도우)expected = hmac.new(secret.encode(),f"{t}.".encode() + raw_body,hashlib.sha256,).hexdigest()returnhmac.compare_digest(expected,parts["v1"])# 다중 키 보유 시: parts["kid"] 로 검증에 사용할 secret 을 선택해 secret 인자로 전달
importhmac,hashlib,timedefverify_webhook(secret: str,signature_header: str,raw_body: bytes,max_age_seconds: int = 300) -> bool:
parts = dict(p.split("=",1)forpinsignature_header.split(","))t = int(parts["t"])ifabs(time.time() - t) > max_age_seconds:
returnFalse# replay 방지 (±5분 윈도우)expected = hmac.new(secret.encode(),f"{t}.".encode() + raw_body,hashlib.sha256,).hexdigest()returnhmac.compare_digest(expected,parts["v1"])# 다중 키 보유 시: parts["kid"] 로 검증에 사용할 secret 을 선택해 secret 인자로 전달
검증 의사 코드 (Node.js):
constcrypto = require("crypto");functionverifyWebhook(secret,signatureHeader,rawBody,maxAgeSeconds = 300){constparts = Object.fromEntries(signatureHeader.split(",").map((p)=>p.split("=")));constt = parseInt(parts.t,10);if(Math.abs(Math.floor(Date.now() / 1000) - t) > maxAgeSeconds){returnfalse;// replay 방지 (±5분 윈도우)}constexpected = crypto
.createHmac("sha256",secret)
.update(`${t}.`)
.update(rawBody)
.digest("hex");returncrypto.timingSafeEqual(Buffer.from(expected,"hex"),Buffer.from(parts.v1,"hex"));}// 다중 키 보유 시: parts.kid 로 검증에 사용할 secret 을 선택해 첫 인자로 전달
constcrypto = require("crypto");functionverifyWebhook(secret,signatureHeader,rawBody,maxAgeSeconds = 300){constparts = Object.fromEntries(signatureHeader.split(",").map((p)=>p.split("=")));constt = parseInt(parts.t,10);if(Math.abs(Math.floor(Date.now() / 1000) - t) > maxAgeSeconds){returnfalse;// replay 방지 (±5분 윈도우)}constexpected = crypto
.createHmac("sha256",secret)
.update(`${t}.`)
.update(rawBody)
.digest("hex");returncrypto.timingSafeEqual(Buffer.from(expected,"hex"),Buffer.from(parts.v1,"hex"));}// 다중 키 보유 시: parts.kid 로 검증에 사용할 secret 을 선택해 첫 인자로 전달
constcrypto = require("crypto");functionverifyWebhook(secret,signatureHeader,rawBody,maxAgeSeconds = 300){constparts = Object.fromEntries(signatureHeader.split(",").map((p)=>p.split("=")));constt = parseInt(parts.t,10);if(Math.abs(Math.floor(Date.now() / 1000) - t) > maxAgeSeconds){returnfalse;// replay 방지 (±5분 윈도우)}constexpected = crypto
.createHmac("sha256",secret)
.update(`${t}.`)
.update(rawBody)
.digest("hex");returncrypto.timingSafeEqual(Buffer.from(expected,"hex"),Buffer.from(parts.v1,"hex"));}// 다중 키 보유 시: parts.kid 로 검증에 사용할 secret 을 선택해 첫 인자로 전달
Webhook secret 발급. API Key 마다 webhook_secret (whsec_...) 이 함께 발급됩니다. raw 값은 발급 응답에 1회만 노출되므로 분실 시 키를 재발급해야 합니다. 응답 스키마/발급 흐름은 API Key 명세를 참고하세요.
재시도 정책
webhook 엔드포인트가 2xx 응답을 반환하지 않으면 다음 일정으로 최대 5회 재시도합니다.
시도
발송 간격
1
초기 발송
2
+ 5초
3
+ 30초
4
+ 5분
5
+ 1시간
6
+ 6시간
5회 모두 실패하면 dead-letter 상태가 되며, 해당 잡은 polling 으로만 결과를 조회할 수 있습니다(결과 보관 24시간 정책 적용).
발송 응답 timeout: 10초
클라이언트 측 응답 코드 2xx 외(3xx 포함)는 모두 재시도 대상
4.5 처리 흐름
클라이언트가 POST /api/v2/ocr/direct/async 로 파일 + params (선택적으로 webhook_url) 제출
서버가 file / params / 동시 요청 한도 검증
크레딧 잔액 확인 — 잔액이 없으면(0) 400 INSUFFICIENT_CREDIT로 요청 거절 (실제 페이지 수 차감은 6단계 워커에서)
잡 row 생성(status=pending), 파일 임시 storage 업로드
즉시 202 Accepted + job_id 반환
백그라운드 워커가 잡을 픽업해 status=running 으로 전이 후 동기 API 와 같은 파이프라인(이미지 변환 → 페이지 수 기준 크레딧 차감 → OCR 엔진 → 인감 매칭) 실행
성공 시 status=succeeded + 결과 저장 + webhook 발송. 실패 시 status=failed + (서버 오류면 크레딧 환불) + webhook 발송
클라이언트는 polling 또는 webhook 으로 결과 수신
created_at + 24h 후 잡 결과 / 임시 파일 정리(status=expired)
4.6 비동기 API 정책
항목
정책
동시 요청 한도
user 당 5건 (동기 워크스페이스 풀과 공유). 초과 시 429 Too Many Requests + Retry-After
요청 처리율 제한 (Rate Limit)
user 당 60 RPM. 초과 시 429
결과 보관 기간
created_at + 24h. 만료 후 polling 410 Gone
멈춘 잡 처리
running 상태로 30분 이상 진행 없으면 자동 failed(error_code: STUCK_JOB_REAPED) + 크레딧 환불 + webhook 발송
환불 정책
서버 오류(OCR 엔진 실패 / 큐 포화 / stuck reaper)는 즉시 환불. 휴먼 에러(파일 미지원·검증 실패 등)는 차감 유지
처리 단위
1 요청 = 1 파일 (sync 와 동일). 멀티 파일은 클라이언트에서 N번 호출
가격
페이지당 1 크레딧 (sync 와 동일)
5. 비동기 API 오류 응답
오류 envelope 는 동기 API 와 동일합니다({status_code, error_code, error_message, +extra}). 비동기 API 에 추가로 등장하는 오류 코드만 정리합니다.
5.1 작업 제출 단계 (POST /async)
케이스
status
error_code
error_message
동시 요청 한도 초과
429
CONCURRENCY_LIMIT_EXCEEDED
Too many concurrent requests. Limit: 5
RPM 한도 초과
429
RATE_LIMIT_EXCEEDED
Rate limit exceeded. Limit: 60 RPM
webhook_url 형식 오류
400
BAD_REQUEST
Invalid webhook_url: must be https://
API Key 에 webhook_secret 미보유 (구 발급 키)
400
WEBHOOK_NOT_ENABLED
This API key does not have a webhook secret. Issue a new API key.
이외 검증 오류(파일 누락·빈 파일·params 오류·미지원 옵션·크레딧 부족 등)는 동기 API 의 3장과 동일합니다.
WEBHOOK_NOT_ENABLED 대응 — per-API-Key webhook secret 도입 이전에 발급된 API Key 는 secret 이 없어 서명이 불가하므로 webhook_url 지정을 거부합니다. POST /api/v2/api-keys 로 신규 발급하면 raw key 와 함께 webhook_secret 이 1회 노출됩니다(발급 명세는 api_key_format.md 참조). 신규 키로 재호출하면 정상 처리됩니다.
5.2 작업 조회 단계 (GET /jobs/{job_id})
케이스
status
error_code
error_message
본인 잡 아님 / 없는 잡
404
NOT_FOUND
Job not found
24h 만료
410
JOB_EXPIRED
Job result has been purged after 24h TTL
잡 본문 응답 안에 담기는 에러 (status=failed)
200
(응답 body 내 error_code)
워커가 기록한 사유
5.3 워커 단계에서 기록되는 주요 error_code (status=failed 일 때 응답 body 내)
error_code
의미
OCR_ENGINE_ERROR
OCR 엔진 호출 실패 (동기 API 의 502 와 같은 사유)
OCR_ENGINE_TIMEOUT
OCR 엔진 큐 포화·일시 오류로 504 매핑
CONVERSION_FAILED
파일 변환 실패 (지원하지 않는 형식·변환 오류)
CONVERSION_TIMEOUT
변환 단계 타임아웃
STUCK_JOB_REAPED
running 30분 이상 진행 없어 reaper 가 강제 fail 처리 (크레딧 환불)
PENDING_DISPATCH_FAILED
pending 24시간 초과 — 워커에 할당되지 못함(dispatch 유실). 차감 전 단계라 환불 없음
INTERNAL_ERROR
위 외 내부 오류
동기 API 가 즉시 5xx 로 반환하던 사유들도 비동기에서는 status=failed 본문 안의 error_code 로 표현됩니다. HTTP 상태는 조회 자체가 성공하면 200 입니다.
6. 변경 이력
일시
버전
변경 내용
2026-06-19
v1.0
OCR Direct SaaS API 동기 명세 초안
2026-06-29
v1.1
OCR Direct 비동기 API(POST /async, GET /jobs/{id}, webhook) 추가. 정책(24h TTL, user 60 RPM, 동시 5건, 환불 정책) 명시
2026-06-30
v1.2
Webhook 서명 강화 — per-API-Key webhook secret (whsec_..., API Key 발급 시 1회 노출), 서명 헤더에 kid 추가 (다중 키 보유 시 검증 대상 secret 선택), timestamp 허용 윈도우 ±5분 명시, Node.js 검증 예제 추가
2026-07-01
v1.3
Webhook payload 스키마 정합화 — 성공/실패 통합 고정 스키마 (필드 표 + 성공/실패 예시), delivered_at / created_at / error: {code, message} 중첩 객체 명시(성공 시 results/metadata 채움 + error: null, 실패 시 results/metadatanull + error 채움). §5.1 async 제출 오류 표에 WEBHOOK_NOT_ENABLED (400) row 추가 (per-key secret 미보유 구 API Key 로 webhook_url 지정 시).
2026-07-02
v1.4
코드 대조 정정 — (1) 동기 API 도 user당 60 RPM rate limit 적용됨을 §3.5 에 명시(동기·비동기 공용, 초과 시 429 RATE_LIMIT_EXCEEDED), (2) 비동기 제출 시 크레딧은 잔액 확인만 하고 실제 페이지 수 차감은 워커 단계에서 이뤄짐을 §4.1·§4.5 에 정정, (3) §5.3 워커 error_code 표에 PENDING_DISPATCH_FAILED(pending 24h dispatch 유실) 추가, (4) Base URL 을 https:// 로 정정.