API 가이드
DEEP OCR SaaS API Guide
최종 업데이트
KDL Document AI — 단순 OCR을 넘어서는 문서 이해의 새로운 표준
1. OCR Direct API
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파일 필드로 요청합니다.
- Base URL:
https://agent-api.koreadeep.com - URL:
POST /api/v2/ocr/direct - 설명: 문서 파일을 업로드하여 OCR 처리를 요청하고 결과를 반환
- Content-Type:
multipart/form-data - 인증: API Key 필요 (
x-api-key) - 지원 버전: v2 전용 (
/api/v1/ocr/direct없음)
요청 예시
curl -X POST 'https://agent-api.koreadeep.com/api/v2/ocr/direct' \
-H 'accept: application/json' \
-H 'x-api-key: kdl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
-F 'file=@sample.pdf;type=application/pdf' \
-F 'reference_seal_image=@seal.png;type=image/png' \
-F 'params={
"template_ids": [456],
"advanced_options": {
"multi_pages": {
"enabled": false
},
"page_range": {
"enabled": true,
"start_page": 1,
"end_page": 14
}
}
}'
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 |
HTTP Header
| 헤더명 | 필수 | 설명 | 예시값 |
|---|---|---|---|
| accept | - | 응답 형식 | application/json |
| x-api-key | ✓ | SaaS API Key | kdl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
params 구조 예시
{
"template_ids": [456],
"advanced_options": {
"multi_pages": {
"enabled": false
},
"page_range": {
"enabled": true,
"start_page": 1,
"end_page": 14
}
}
}
params 필드
| 필드명 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| doc_code | string | null | null | 문서 분류 코드/스키마 힌트 |
| template_ids | integer[] | null | null | 사용할 템플릿 ID 목록. 중복 ID는 제거됨 |
| rotate_yn | boolean | false | 문서 회전 보정 여부 |
| review_yn | boolean | true | 검수 단계 사용 여부 |
| advanced_options | object | null | null | 고급 옵션 객체 |
template_ids가 1개이면 해당 템플릿 스키마를 기준으로 추출합니다. 여러 개이면 후보 템플릿 목록을 전달하여 문서 분류 및 추출에 사용합니다.
null이거나 빈 배열이면 템플릿 스키마 없이 OCR 엔진이 자동으로 문서 분류 및 추출을 수행합니다(에러 아님).
advanced_options 필드
| 필드명 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| multi_pages | object | null | null | 여러 페이지를 하나의 문서 단위로 처리할지 여부 |
| page_range | object | null | null | 처리할 페이지 범위 |
미지원 옵션 —
schedule,skip_manual_review,corporate_seal_match는 동기 API 에서 지원하지 않습니다.enabled=true로 전달하면400으로 거부됩니다.
schedule/skip_manual_review: 동기 API 의 구조상 동작할 수 없는 옵션입니다(예약·수동검수 단계 없음).corporate_seal_match: 대신reference_seal_image파일 필드를 사용하세요(아래 법인인감 매칭 참조).
multi_pages
| 필드명 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| enabled | boolean | false | true이면 여러 페이지를 하나의 OCR 처리 단위로 전달. (크레딧 차감은 모드와 무관하게 페이지당 1크레딧) |
page_range
| 필드명 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| enabled | boolean | false | 페이지 범위 적용 여부 |
| start_page | integer | null | null | 시작 페이지 번호. 1부터 시작 |
| end_page | integer | null | null | 종료 페이지 번호. 1부터 시작 |
제약 조건
enabled=true이면start_page,end_page가 모두 필요합니다.start_page와end_page는 1 이상이어야 합니다.start_page는end_page보다 클 수 없습니다.- 변환된 전체 페이지 수를 초과하는 범위를 요청하면
400 Bad Request가 반환됩니다.
schedule · skip_manual_review (미지원)
동기 OCR API 에서는 지원하지 않습니다. enabled=true로 전달하면 400으로 거부됩니다(옵션 검증 오류 참조).
schedule: 결과를 inline 으로 즉시 반환하는 동기 API 라 "나중에 받는" 예약 실행을 표현할 수 없습니다.skip_manual_review: 동기 API 에는 수동 검수 단계 자체가 없어 생략할 대상이 없습니다.
법인인감 매칭 (reference_seal_image)
법인인감 매칭은 advanced_options가 아니라 reference_seal_image 파일 필드로 요청합니다. (advanced_options.corporate_seal_match는 400으로 거부됩니다.)
reference_seal_image에 비교 대상 인감 이미지를 업로드하면, OCR 추출 직후 각 페이지를 참조 인감과 비교합니다.- 비교 결과 요약(문서 전체 기준)을 응답의
results.seal_match로 반환합니다(Results 객체 구조 참조). - 인감 매칭 단계 실패는 OCR 결과 반환을 막지 않습니다(soft-fail). 이 경우
results.seal_match는null입니다.
지원 파일 형식
- 이미지:
.jpg,.jpeg,.png,.webp,.bmp,.gif,.tif,.tiff,.heic,.svg - 문서/PDF:
.pdf,.txt,.csv - 오피스:
.doc,.docx,.xls,.xlsx,.ppt,.pptx - 한글/ODF:
.hwp,.hwpx,.hwtx,.owpml,.odt,.odf
총 25개 확장자를 지원합니다. 위 목록에 없는 형식을 업로드하면 변환 단계에서 실패하여
502가 반환됩니다(별도의 4xx 검증 없음). 자세한 내용은 오류 응답을 참고하세요.
처리 흐름
- 클라이언트에서 API Key와 함께 문서 파일 및
params(선택적으로reference_seal_image) 업로드 - 서버에서 파일 유효성 및
paramsJSON 검증 - 문서를 이미지로 변환하고,
page_range가 있으면 대상 페이지만 선택 - 처리 페이지 수 기준 API 추출 크레딧 차감 — 페이지당 1크레딧. 잔액이 부족하면
400으로 중단됩니다(크레딧 오류 참고) - 이미지와 템플릿/고급 옵션을 OCR 엔진에 전달
- OCR 결과와 위치 정보를 응답 형식에 맞게 변환
reference_seal_image가 있으면 페이지별 인감 비교 후results.seal_match채움(soft-fail)- 클라이언트에 결과 반환
2. 응답 구조
성공 응답 (200 OK)
{
"status": "success",
"results": {
"elements": [
{
"page_number": 1,
"doc_code": "KDL-DOC-01",
"rotated": {
"angle": 5.0
},
"key_value_list": [
{
"key": "registration_no",
"value": ["261-53-00182"],
"type": "string",
"confidence_score": 0.98,
"bbox": {
"x1": 0.1,
"y1": 0.2,
"x2": 0.3,
"y2": 0.4
}
}
]
}
],
"metadata": null,
"seal_match": {
"matched": true,
"score": 0.92,
"grade": "strong_match",
"decision": "match"
}
},
"metadata": {
"file_info": {
"filename": "sample.pdf",
"content_type": "application/pdf",
"size": 3284879
},
"total_time": 12.345
}
}
results.seal_match는reference_seal_image를 업로드했을 때만 채워집니다. 미업로드 시(또는 인감 매칭 soft-fail 시)null입니다.
최상위 응답 필드
| 필드명 | 타입 | 설명 | 예시값 |
|---|---|---|---|
| status | string | 처리 상태. 성공 시 success | "success" |
| results | object | OCR 결과 데이터 | 아래 상세 참조 |
| metadata | object | 파일 정보 및 처리 시간 | 아래 상세 참조 |
metadata 객체 구조
| 필드명 | 타입 | 설명 | 예시값 |
|---|---|---|---|
| file_info | object | 업로드 파일 정보 | 아래 상세 참조 |
| total_time | number | null | 총 처리 시간 (초) | 12.345 |
file_info 객체 구조
| 필드명 | 타입 | 설명 | 예시값 |
|---|---|---|---|
| filename | string | 파일명 | "sample.pdf" |
| content_type | string | MIME 타입 | "application/pdf" |
| size | integer | 원본 파일 크기 (바이트) | 3284879 |
Results 객체 구조
results 객체는 OCR 추론 결과인 elements, 부가 정보 metadata, 그리고 인감 매칭 결과 seal_match를 포함합니다.
results.metadata는 현재 항상null입니다. 향후 확장을 위해 예약된 필드이며, 지금은 값이 채워지는 경로가 없습니다. (최상위metadata(파일 정보·처리 시간)와는 다른 필드입니다.)
seal_match
reference_seal_image를 업로드한 경우에만 채워지는 법인인감 매칭 요약입니다. 미업로드 또는 매칭 soft-fail 시 null입니다.
| 필드명 | 타입 | 설명 | 예시값 |
|---|---|---|---|
| matched | boolean | 한 페이지라도 인감이 매칭되면 true | true |
| score | number | null | 대표 region 유사도 점수 (0~1) | 0.92 |
| grade | string | null | 대표 region 등급 | "strong_match" |
| decision | string | null | 대표 region 판정값 | "match" |
elements
| 필드명 | 타입 | 필수 | 설명 | 예시값 |
|---|---|---|---|---|
| page_number | integer | ✓ | 페이지 번호. page_range 사용 시 원본 문서 기준 페이지 번호 | 1 |
| doc_code | string | null | - | 문서 분류 코드 또는 템플릿 ID. 항상 문자열 또는 null (요청에 doc_code를 주면 그 값, 없으면 분류된 템플릿 ID를 문자열로 반환). 숫자 타입으로 내려가지 않습니다. | "KDL-DOC-01" |
| rotated | object | null | - | 회전 보정 정보 | { "angle": 5.0 } |
| key_value_list | object[] | null | - | 추출된 키-값 목록 | 아래 상세 참조 |
key_value_list
| 필드명 | 타입 | 설명 | 예시값 |
|---|---|---|---|
| key | string | 추출 키. 중첩 객체는 .으로, 반복 배열은 [1], [2]처럼 표현 | "owner.name" |
| value | array | 추출 값 목록. 항상 배열 | ["홍길동"] |
| type | string | null | 값 타입 | "string" |
| confidence_score | number | null | 신뢰도 점수 | 0.98 |
| bbox | object | null | 경계 상자 좌표 | 아래 상세 참조 |
value가 배열인 이유: 하나의 키에 여러 값이 추출될 수 있기 때문입니다(반복 필드·다중 값). 단일 값이어도 1개 요소 배열(["..."])로 반환됩니다.
bbox (경계 상자) 구조
{
"x1": 0.1,
"y1": 0.2,
"x2": 0.3,
"y2": 0.4
}
- 0~1 사이 정규화된 좌표
| 필드명 | 타입 | 설명 |
|---|---|---|
| x1 | number | 좌상단 X 좌표 |
| y1 | number | 좌상단 Y 좌표 |
| x2 | number | 우하단 X 좌표 |
| y2 | number | 우하단 Y 좌표 |
3. 오류 응답
3.1 인증 오류
401 Unauthorized — 유효하지 않거나 폐기된 API Key
{
"status_code": 401,
"error_code": "UNAUTHORIZED",
"error_message": "Invalid or revoked API key"
}
422 Unprocessable Entity — x-api-key 헤더 누락
헤더 자체가 전달되지 않으면 요청 검증 단계에서 반환되며, 무효한 키(401)와 구분됩니다. 요청 검증 실패는 누락 필드명을 기반으로 error_code(<FIELD>_<SUFFIX>)를 만듭니다.
{
"status_code": 422,
"error_code": "X-API-KEY_REQUIRED",
"error_message": "x-api-key is required"
}
3.2 요청·옵션 검증 오류 (400)
직접 발생하는 단문 메시지는 error_message 에 그대로 담기고 error_code 는 BAD_REQUEST 입니다.
params JSON 파싱 실패 / 객체 아님
{ "status_code": 400, "error_code": "BAD_REQUEST", "error_message": "Invalid params JSON: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)" }
{ "status_code": 400, "error_code": "BAD_REQUEST", "error_message": "params must be a JSON object" }
파일 없음 / 빈 파일
{ "status_code": 400, "error_code": "BAD_REQUEST", "error_message": "No file provided" }
{ "status_code": 400, "error_code": "BAD_REQUEST", "error_message": "Empty file provided" }
{ "status_code": 400, "error_code": "BAD_REQUEST", "error_message": "Empty reference_seal_image provided" }
page_range가 변환된 페이지 수 초과
{ "status_code": 400, "error_code": "BAD_REQUEST", "error_message": "page_range exceeds converted page count" }
advanced_options 옵션 제약 위반
미지원 옵션(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"
}
각 위반의 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크레딧(모드 무관).
크레딧 부족:
{
"status_code": 400,
"error_code": "INSUFFICIENT_CREDIT",
"error_message": "Insufficient credit. Available: 3, Requested: 5",
"available_credit": 3,
"requested_credit": 5
}
종량제(metered) 추가 사용 한도 초과:
{
"status_code": 400,
"error_code": "METERED_LIMIT_EXCEEDED",
"error_message": "Metered usage limit exceeded. Available: 0, Requested: 5, Limit: 1000",
"available_credit": 0,
"requested_credit": 5,
"metered_credit_limit": 1000
}
사용자 크레딧 정보 없음 (404):
{ "status_code": 404, "error_code": "NOT_FOUND", "error_message": "Credit not found for user" }
3.4 파일 변환 / 형식 오류
지원하지 않는 형식이거나 변환에 실패하면 502로 반환됩니다(전용 4xx 검증 없음). 변환이 제한 시간을 초과하면 504입니다. 5xx 는 error_code 가 HTTP_EXCEPTION 입니다.
{ "status_code": 502, "error_code": "HTTP_EXCEPTION", "error_message": "Unsupported format: zip" }
{ "status_code": 504, "error_code": "HTTP_EXCEPTION", "error_message": "File conversion timed out" }
그 외 변환 실패도 502 error_message: "<변환 오류 메시지>" 로 반환됩니다.
3.5 OCR 엔진 오류 (5xx)
OCR 엔진 호출 실패 (502):
{ "status_code": 502, "error_code": "HTTP_EXCEPTION", "error_message": "OCR API error: <원인>" }
OCR 엔진이 일시적 오류(큐 포화 429 / 502 / 503 / 504)를 반환하면 504로 매핑되어 반환됩니다(error_message 가변):
{ "status_code": 504, "error_code": "HTTP_EXCEPTION", "error_message": "DeepFlow queue is full (HTTP 429)" }
OCR 결과 없음 (502 또는 500):
{ "status_code": 502, "error_code": "HTTP_EXCEPTION", "error_message": "No results returned from OCR API" }
변환 이미지 없음 / 내부 구성 오류 (500):
{ "status_code": 500, "error_code": "HTTP_EXCEPTION", "error_message": "No converted images returned" }
{ "status_code": 500, "error_code": "HTTP_EXCEPTION", "error_message": "Template service is not configured" }
Rate limit · 타임아웃 참고
- 이 엔드포인트에는 user당 60 RPM (requests / 60초) rate limit 이 적용됩니다(동기·비동기 공용, 4.6 비동기 API 정책 참조). 초과 시
429(error_code: RATE_LIMIT_EXCEEDED) +Retry-After: 60. 그 밖의 호출 제어는 (1) 크레딧 쿼터(3.3,400), (2) OCR 엔진 큐 용량 초과 →504.- OCR 엔진 호출의 클라이언트 측 네트워크 타임아웃(약 300초)은
502(OCR API error: ...)로 표면화됩니다. 업스트림 게이트웨이 타임아웃(예: 60초)은504로 반환됩니다.
오류 발생 시나리오 요약
x-api-key헤더 누락 →422/ 유효하지 않거나 폐기된 키 →401- 파일 미제공·빈 파일, 빈
reference_seal_image,paramsJSON 파싱 실패 →400 - 미지원 옵션 활성화(
schedule/skip_manual_review/corporate_seal_match) 또는page_range제약 위반 →400(정제 단문) page_range가 변환된 페이지 수 초과 →400- 크레딧 부족·종량제 한도 초과 →
400(부가 필드 top-level 병기) / 크레딧 정보 없음 →404 - 지원하지 않는 파일 형식·변환 실패 →
502/ 변환 타임아웃 →504 - OCR 엔진 호출 실패 →
502/ 일시적 오류·큐 포화 →504/ 결과 없음 →500·502 - user당 60 RPM(동기·비동기 공용) 초과 →
429(RATE_LIMIT_EXCEEDED+Retry-After)
4. OCR Direct Async API
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 권장)
- Base URL:
https://agent-api.koreadeep.com - 제출 URL:
POST /api/v2/ocr/direct/async - 조회 URL:
GET /api/v2/ocr/direct/jobs/{job_id} - Content-Type:
multipart/form-data - 인증: API Key 필요 (
x-api-key— 동기 API 와 동일 키)
4.1 작업 제출 — POST /api/v2/ocr/direct/async
요청 예시
curl -X POST 'https://agent-api.koreadeep.com/api/v2/ocr/direct/async' \
-H 'accept: application/json' \
-H 'x-api-key: kdl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
-F 'file=@sample.pdf;type=application/pdf' \
-F 'reference_seal_image=@seal.png;type=image/png' \
-F 'webhook_url=https://your-system.example.com/callbacks/ocr' \
-F 'params={
"template_ids": [456],
"advanced_options": {
"page_range": { "enabled": true, "start_page": 1, "end_page": 14 }
}
}'
요청 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| file | UploadFile | ✓ | 분석할 문서 파일 (1 요청 = 1 파일) |
| params | JSON string | - | OCR 처리 옵션. 동기 API 의 params 와 동일 스키마 |
| reference_seal_image | UploadFile | - | 법인인감 매칭용 참조 인감 이미지. 동기 API 와 동일 동작 |
| webhook_url | string | - | 작업 완료/실패 시 콜백을 받을 URL. 미지정 시 polling 으로만 결과 조회 |
params의schedule/skip_manual_review/corporate_seal_match미지원 정책은 비동기 API에서도 동일하게 적용됩니다(enabled=true→400). 비동기라고 해서 예약(schedule)이 활성화되지는 않습니다.
HTTP Header
| 헤더명 | 필수 | 설명 | 예시값 |
|---|---|---|---|
| accept | - | 응답 형식 | application/json |
| x-api-key | ✓ | SaaS API Key | kdl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
응답 (202 Accepted)
{
"job_id": "f3b1c8d2-9e4a-4f8b-9c3d-1a2b3c4d5e6f",
"status": "pending",
"status_url": "https://agent-api.koreadeep.com/api/v2/ocr/direct/jobs/f3b1c8d2-9e4a-4f8b-9c3d-1a2b3c4d5e6f",
"expires_at": "2026-06-30T17:13:52Z",
"created_at": "2026-06-29T17:13:52Z"
}
| 필드 | 타입 | 설명 |
|---|---|---|
| job_id | string (UUID) | 작업 식별자 |
| status | string | 초기 상태. pending |
| status_url | string | polling 으로 결과를 조회할 URL (4.2 참조) |
| expires_at | string (ISO 8601) | 작업 결과 보관 만료 시각. created_at + 24h |
| created_at | string (ISO 8601) | 작업 제출 시각 (UTC) |
작업 제출 시엔 크레딧 잔액이 있는지만 확인합니다(잔액이 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'
진행 중 응답 (200 OK)
{
"job_id": "f3b1c8d2-9e4a-4f8b-9c3d-1a2b3c4d5e6f",
"status": "running",
"created_at": "2026-06-29T17:13:52Z",
"started_at": "2026-06-29T17:13:53Z",
"expires_at": "2026-06-30T17:13:52Z"
}
| status 값 | 의미 |
|---|---|
pending | 큐 대기 중 (아직 워커에 할당되지 않음) |
running | 워커가 처리 중 |
succeeded | 처리 완료. results / metadata 동봉 |
failed | 처리 실패. error_code / error_message 동봉 |
expired | 결과 보관 만료. 410 Gone 반환 (아래 별도 응답) |
성공 응답 (200 OK)
{
"job_id": "f3b1c8d2-9e4a-4f8b-9c3d-1a2b3c4d5e6f",
"status": "succeeded",
"created_at": "2026-06-29T17:13:52Z",
"started_at": "2026-06-29T17:13:53Z",
"completed_at": "2026-06-29T17:14:35Z",
"expires_at": "2026-06-30T17:13:52Z",
"results": {
"elements": [
{
"page_number": 1,
"doc_code": "KDL-DOC-01",
"rotated": { "angle": 5.0 },
"key_value_list": [
{
"key": "registration_no",
"value": ["261-53-00182"],
"type": "string",
"confidence_score": 0.98,
"bbox": { "x1": 0.1, "y1": 0.2, "x2": 0.3, "y2": 0.4 }
}
]
}
],
"metadata": null,
"seal_match": null
},
"metadata": {
"file_info": {
"filename": "sample.pdf",
"content_type": "application/pdf",
"size": 3284879
},
"total_time": 42.371
}
}
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"
}
410 Gone — 결과 보관 만료
expires_at 경과 후(24시간 정책) 호출하면 결과가 정리되고 410 Gone 으로 반환됩니다.
{
"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"
}
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 와 동일 스키마 |
error | object | null | 실패 시 {code, message} 중첩 객체, 성공 시 null |
성공 payload 예시
{
"event": "ocr_direct.completed",
"delivered_at": "2026-06-29T17:14:36Z",
"job_id": "f3b1c8d2-9e4a-4f8b-9c3d-1a2b3c4d5e6f",
"status": "succeeded",
"created_at": "2026-06-29T17:13:52Z",
"completed_at": "2026-06-29T17:14:35Z",
"results": { "elements": [ ... ], "metadata": null, "seal_match": null },
"metadata": { "file_info": { ... }, "total_time": 42.371 },
"error": null
}
실패 payload 예시
{
"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 을 골라 검증합니다.
X-DeepAgent-Signature: t=1719659675,v1=4f2b8a...hex...,kid=42
| 필드 | 의미 |
|---|---|
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):
import hmac, hashlib, time
def verify_webhook(secret: str, signature_header: str, raw_body: bytes, max_age_seconds: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > max_age_seconds:
return False # replay 방지 (±5분 윈도우)
expected = hmac.new(
secret.encode(),
f"{t}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
# 다중 키 보유 시: parts["kid"] 로 검증에 사용할 secret 을 선택해 secret 인자로 전달
검증 의사 코드 (Node.js):
const crypto = require("crypto");
function verifyWebhook(secret, signatureHeader, rawBody, maxAgeSeconds = 300) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("="))
);
const t = parseInt(parts.t, 10);
if (Math.abs(Math.floor(Date.now() / 1000) - t) > maxAgeSeconds) {
return false; // replay 방지 (±5분 윈도우)
}
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.`)
.update(rawBody)
.digest("hex");
return crypto.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/metadata null + 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:// 로 정정. |
KDL DEEP OCR SaaS API v1.4 — 2026-07-02
문의: cs@koreadeep.com




