웹훅
GET /batches/{batch_name}을 완료될 때까지 폴링하는 대신, 콜백 URL을 한 번 등록하면 배치가 완료되는 순간 HTTP POST를 받을 수 있습니다. 세 가지 서명 헤더와 id.timestamp.body 서명 콘텐츠 구성은 Standard Webhooks 공개 사양을 따릅니다 — OpenAI의 웹훅에서도 사용하는 방식입니다. 사양의 일반적인 규칙과 한 가지 차이점: 여기서 webhook_secret은 일반 16진수 문자열이며, whsec_ 접두사가 붙은 base64 값이 아닙니다 — 일반적인 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 미들웨어)를 통해 본문을 캡처하면 프로덕션에서는 이런 문제가 발생하지 않습니다 — 손으로 테스트할 때만 발생하는 함정입니다.
재시도
배치가 완료되면 즉시 한 번 전달이 시도됩니다. 해당 시도가 실패하면(시간 초과, 연결 오류 또는 2xx가 아닌 응답) 지수 백오프로 재시도됩니다: 1분, 5분, 30분, 2시간, 6시간, 24시간 — 총 최대 7회 시도(초기 시도 + 6회 재시도). 첫 시도 후 24시간 내에 성공하지 못하면 전달은 실패로 표시되고 더 이상 재시도하지 않습니다. 이벤트를 안정적으로 기록하는 즉시 엔드포인트가 2xx 상태로 응답하도록 하세요 — 느린 후속 단계(예: 배치 결과를 실제로 가져와 파싱하는 작업)는 응답 후에 수행하고, 응답 전에 하지 마세요. 느린 다운스트림 단계가 재시도를 유발하지 않도록 하기 위함입니다.
배치 재처리: 웨이브당 알림 1회
주어진 batch.completed 알림은 배치의 모든 문서가 최종 상태에 도달하는 순간 한 번만 발생합니다. 나중에 동일한 batch_name에 문서를 더 추가하고 process를 다시 호출하면 — "새 웨이브" — 그 웨이브가 끝나면 자동으로 다시 알림을 받게 됩니다. process_batch는 이전 웨이브가 이미 알림을 보낸 배치에서 새 적격 문서를 찾는 즉시 웹훅을 자체적으로 다시 준비합니다. 웨이브 사이에 웹훅을 다시 PUT하거나 다른 작업을 할 필요가 없습니다.
두 웨이브가 겹치는 경우 — 이전 웨이브가 완료된 후가 아니라 아직 실행 중일 때 문서를 추가하고 처리하는 경우 — 하나의 알림만 받게 되며, 두 웨이브의 모든 문서를 포함하고 마지막 문서(어느 웨이브든)가 완료되면 전달됩니다. 두 개의 별도 알림을 받는 경우는 두 process 호출 사이에 배치가 실제로 유휴 상태(모든 문서가 최소 한 번 최종 상태에 도달)였을 때뿐입니다.
이 문서의 이전 버전에서는 한 번만 발생하는 발화를 영구적인 제한으로 설명하고 각 웨이브 전에 웹훅을 다시 PUT하여 해결해야 한다고 했습니다. 그 조언은 실제로 효과가 없었습니다 — 등록 엔드포인트는 의도적으로 fired_at에 대해 관여하지 않으며(참조: 웹훅 참조) — 이제 재준비는 process가 자동으로 처리합니다. 만약 당신이re on an integration written against the old advice, you can safely stop re-registering before each wave; it was never doing anything.