웹훅
GET /batches/{batch_name}을 완료될 때까지 폴링하는 대신, 콜백 URL을 한 번 등록하면 배치가 완료되는 즉시 HTTP POST를 받을 수 있습니다. 세 가지 서명 헤더와 id.timestamp.body 서명 콘텐츠 구성은 Standard Webhooks 공개 사양을 따릅니다 — OpenAI의 웹훅에서도 사용하는 방식입니다. 사양의 자체 규칙과 한 가지 차이점: 여기의 webhook_secret은 whsec_ 접두사가 붙은 base64 값이 아닌 일반 16진수 문자열입니다. 일반 Standard Webhooks/Svix 검증 라이브러리는 이를 base64로 디코딩하려다 실패합니다. 아래 검증 코드를 사용하여 반환된 시크릿을 원시 HMAC 키 바이트로 그대로 사용하세요. 접두사를 인식하는 라이브러리는 사용하지 마세요.
웹훅 등록
PUT /api/v1/batches/{batch_name}/webhook로 배치의 콜백을 등록합니다:
curl -X PUT https://imagetotable.ai/api/v1/batches/260716-4K9P/webhook \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"callback_url": "https://example.com/hooks/imagetotable"}'응답에는 웹훅별 webhook_secret이 포함되며, 자동으로 생성됩니다 — 계정 전체 공유 키가 아닌 등록별 고유 시크릿이므로, 시크릿이 유출되어도 해당 콜백 하나에만 영향을 미칩니다:
{
"batch_name": "260716-4K9P",
"callback_url": "https://example.com/hooks/imagetotable",
"webhook_secret": "9f2a3b7c1d8e4f5061728394a5b6c7d81a2b3c4d5e6f7089",
"created_at": "2026-07-16T09:12:03Z",
"fired_at": null
}webhook_secret을 저장하세요 — 수신된 전송을 검증할 때 필요합니다. 별도의 "시크릿 공개" 엔드포인트는 없지만, 동일한 callback_url로 다시 PUT하면 분실 시 안전하게 다시 가져올 수 있습니다.
이벤트 봉투
모든 전달은 원시 배치 데이터가 아닌 봉투에 담긴 JSON 객체입니다. 따라서 기존 통합을 깨뜨리지 않으면서 향후 이벤트 유형을 포괄하도록 형태를 확장할 수 있습니다. type을 확인하여 이벤트를 구분하세요. 배치에 등록된 콜백 URL은 아래 두 종류를 혼합하여 수신할 수 있습니다.
{
"type": "batch.completed",
"created_at": "2026-07-16T09:14:31Z",
"data": {
"batch_name": "260716-4K9P",
"status": "succeeded",
"document_count": 3
}
}data.status는 배치의 최종 결과를 반영합니다(succeeded 또는 failed — 전체 상태 용어는 비동기 작업 모델 참조). 페이로드는 완료 알림이지 결과 자체가 아닙니다. 수신 후 GET /batches/{batch_name}/results를 호출하여 실제 추출된 데이터를 가져오세요.
문서에서 bbox 주석 작업을 트리거하면 해당 문서의 배치에 이미 등록된 웹훅을 재사용하며, 또 다른 batch.completed 대신 별개의 이벤트를 전달합니다:
{
"type": "document.bbox_completed",
"created_at": "2026-07-16T09:20:07Z",
"data": {
"document_id": "3f9c9e2a-1b7d-4e2b-9a3e-2f5b6c7d8e9f",
"group_batch_id": "bx_8a3c2e1f",
"status": "succeeded"
}
}batch.completed와 마찬가지로 이 이벤트의 전달은 원자적으로 처리되므로, 여러 행 그룹이 서로 다른 스레드에서 거의 동시에 종료 상태에 도달하더라도 하나의 bbox 작업에 대해 두 개의 별도 document.bbox_completed 전달을 받지 않습니다. 실패한 전달이 재시도되는 일반적인 경우에는 webhook-id 기반 중복 제거가 그 위에 추가로 적용됩니다.
폴링 트리거용 이벤트 피드
배치 콜백 웹훅은 하나의 알려진 batch_name으로 범위가 제한됩니다. Zapier의 "New Completed Batch" 또는 "Failed Extraction"과 같은 폴링 트리거는 계정 범위의 피드가 필요합니다: GET /api/v1/events. 이 피드는 지속된 이벤트를 최신순으로 반환하며, Zapier가 중복 제거에 사용할 수 있는 안정적인 id 필드를 포함합니다.
curl "https://imagetotable.ai/api/v1/events?type=batch.completed&limit=20" \
-H "Authorization: Bearer $API_KEY"{
"events": [
{
"id": "evt_3f9c9e2a1b7d4e2b9a3e2f5b6c7d8e9f",
"type": "batch.completed",
"created_at": "2026-08-04T08:31:22+00:00",
"batch_name": "260804-4K9P",
"document_id": null,
"status": "succeeded",
"document_count": 3,
"filename": null,
"error": null,
"batch_url": "https://imagetotable.ai/api/v1/batches/260804-4K9P",
"batch_results_url": "https://imagetotable.ai/api/v1/batches/260804-4K9P/results"
}
],
"has_more": false,
"next_page_token": null
}지원되는 이벤트 유형은 batch.completed, document.completed, document.failed입니다. 이벤트는 작업자가 문서의 최종 상태를 커밋한 후에만 기록되므로, 소비자는 document_url 또는 batch_results_url을 즉시 안전하게 따라갈 수 있습니다. 실패한 문서를 재처리하거나 동일한 배치에서 이후 웨이브를 처리하면 새 이벤트 id가 생성됩니다. batch_name 또는 document_id만으로 중복 제거하지 마세요.
이미 배치를 알고 있고 대기 중인 비동기 작업을 재개하려면 콜백을 사용하세요. 트리거가 전체 계정에서 새 작업을 발견해야 한다면 /events를 사용하세요. 사용자에게 batch_name을 수동으로 입력하도록 요청하는 트리거는 일반적으로 잘못된 모델입니다.
서명 확인
Standard Webhooks 사양에 따라 모든 전송에는 세 개의 헤더가 포함됩니다:
| 헤더 | 목적 |
|---|---|
webhook-id | 이 전송 시도에 대한 고유 ID입니다. 중복 제거에 사용하세요. 재시도된 전송은 동일한 ID를 재사용합니다. |
webhook-timestamp | 전송이 이루어진 Unix 타임스탬프로, 재전송 공격을 방어합니다. |
webhook-signature | 서명 자체로, v1,<base64 서명> 형식입니다. |
서명은 webhook-id, webhook-timestamp, 원시 요청 본문을 순서대로 .로 연결한 문자열에 대해 웹훅의 webhook_secret을 키로 사용한 HMAC-SHA256입니다:
signed_content = "{webhook_id}.{webhook_timestamp}.{raw_request_body}"
signature = base64(hmac_sha256(webhook_secret, signed_content))이를 서버에서 다시 계산하여 webhook-signature 헤더의 v1, 뒤에 있는 값과 비교하세요. 항상 원시 요청 본문 바이트를 기준으로 검증해야 하며, 파싱된 JSON을 다시 직렬화한 버전이 아닙니다. 다시 직렬화하면 공백/키 순서가 변경되어 실제 전송임에도 서명 불일치가 발생할 수 있습니다.
# 서명 검증은 HTTP 호출이 아니므로, 이미 디스크에 저장된 전달 데이터(headers.txt / body.json)를
# openssl로 재현합니다. 실제 웹훅 핸들러에 검증 코드를 작성하기 전에 수동으로 스키마를 이해하는 데 유용합니다.
WEBHOOK_ID=$(grep -i '^webhook-id:' headers.txt | cut -d' ' -f2 | tr -d '\r')
WEBHOOK_TS=$(grep -i '^webhook-timestamp:' headers.txt | cut -d' ' -f2 | tr -d '\r')
SIGNED_CONTENT="${WEBHOOK_ID}.${WEBHOOK_TS}.$(cat body.json)"
echo -n "$SIGNED_CONTENT" \
| openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -binary \
| base64import base64
import hashlib
import hmac
import os
headers = dict(line.split(": ", 1) for line in open("headers.txt") if ": " in line)
webhook_id = headers["webhook-id"].strip()
webhook_timestamp = headers["webhook-timestamp"].strip()
body = open("body.json", "rb").read()
signed_content = f"{webhook_id}.{webhook_timestamp}.".encode() + body
signature = base64.b64encode(
hmac.new(os.environ["WEBHOOK_SECRET"].encode(), signed_content, hashlib.sha256).digest()
)
print(signature.decode())import { createHmac } from "node:crypto";
import { readFileSync } from "node:fs";
const headers = Object.fromEntries(
readFileSync("headers.txt", "utf8")
.split("\n")
.filter((line) => line.includes(": "))
.map((line) => line.split(": "))
);
const webhookId = headers["webhook-id"].trim();
const webhookTimestamp = headers["webhook-timestamp"].trim();
const body = readFileSync("body.json", "utf8");
const signedContent = `${webhookId}.${webhookTimestamp}.${body}`;
const signature = createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(signedContent)
.digest("base64");
console.log(signature);수동으로 테스트할 때 — 위 스니펫 중 하나를 실행하기 전에 전달된 본문을 직접 body.json에 저장하는 경우 — 편집기나 캡처 방식이 추가하는 후행 개행 문자를 주의하세요. 위의 curl/openssl 버전은 우연히 이 문제에 영향을 받지 않지만(bash의 $(cat ...)는 후행 개행을 제거함), Python과 JavaScript 버전은 파일의 정확한 바이트를 읽으므로 추가 \n이 들어가면 일치하지 않는 서명을 조용히 생성합니다. 전형적인 증상은 "curl에서는 유효하다고 나오는데 Python 검증기에서는 유효하지 않다고 나온다"는 것이며, 코드 자체에는 버그가 없기 때문에 더 혼란스럽습니다. 실제 웹훅 핸들러에서는 복사/붙여넣기나 수동 저장 대신 웹 프레임워크의 원시 요청 본문 접근자(Flask의 request.get_data(), Express의 raw-body 미들웨어)를 통해 본문을 캡처하세요. 그러면 프로덕션에서는 이 문제가 발생하지 않으며, 수동 테스트 시에만 주의할 함정입니다.
재시도
배치가 완료되면 즉시 한 번 전송이 시도됩니다. 해당 시도가 실패하면 지수 백오프 방식으로 재시도됩니다: 1분, 5분, 30분, 2시간, 6시간, 24시간 — 총 7회 시도까지 이루어집니다. 첫 번째 시도 후 24시간 이내에 성공하지 못하면 전송은 실패로 표시되고 더 이상 재시도되지 않습니다. 이벤트를 안전하게 기록하는 즉시 엔드포인트가 2xx 상태로 응답하도록 하세요. 느린 처리는 응답 후에 수행하고, 응답 전에 수행하지 마세요. 그래야 느린 다운스트림 단계가 자체적으로 재시도를 유발하지 않습니다.
배치 재처리: 웨이브당 하나의 알림
특정 batch.completed 알림은 배치 내 모든 문서가 최종 상태에 도달하는 순간 한 번만 전송됩니다. 이후 동일한 batch_name에 문서를 더 추가하고 process를 다시 호출하면 — "새로운 웨이브" — 해당 웨이브가 완료될 때도 자동으로 다시 알림을 받게 됩니다. process_batch는 이전 웨이브에서 이미 알림을 보낸 배치에 새로운 적격 문서가 있는 순간 웹훅을 자동으로 재설정합니다. 웨이브 사이에 웹훅을 다시 PUT하거나 다른 작업을 수행할 필요가 없습니다.
두 웨이브가 겹치는 경우 — 이전 웨이브가 완료된 후가 아니라 실행 중인 동안 문서를 추가하고 처리하면 — 두 웨이브의 모든 문서를 포함하는 하나의 알림을 받게 되며, 두 웨이브 중 마지막 문서가 완료되면 전송됩니다. 두 개의 개별 알림을 받는 경우는 배치가 실제로 유휴 상태가 되었을 때 두 번의 process 호출 사이에만 발생합니다.
이 문서의 이전 버전에서는 한 번만 전송되는 것을 영구적인 제한 사항으로 설명하며, 각 웨이브 전에 웹훅을 다시 PUT하여 해결해야 한다고 안내했습니다. 그 조언은 실제로 효과가 없었습니다. 등록 엔드포인트는 fired_at에 대해 의도적으로 관여하지 않았고 여전히 그렇습니다(웹훅 참조 참조). 이제는 process가 재설정을 자동으로 처리합니다. 이전 조언에 따라 통합을 작성한 경우 각 웨이브 전에 재등록을 안전하게 중단할 수 있습니다. 이전에는 아무 효과가 없었습니다.