각 요청에 대해 Auto, TokenLab Verified 또는 Official를 선택할 수 있으며, 가격은 사전에 표시됩니다. 새로운 기능 확인하기

Async AI Task Webhooks: 서명을 검증하고 태스크 읽기

CryptoCrypto
·2026년 9월 28일·약 4분 읽기·업데이트 2026년 9월 28일·26 조회수
#웹훅#비동기 작업#API 연동#보안
Async AI Task Webhooks: 서명을 검증하고 태스크 읽기

웹훅은 작업이 최종 상태에 도달했음을 알리는 서명된 힌트입니다. 이는 레코드 그 자체가 아닙니다. 따라서 규칙은 간단합니다. 원시 바이트(raw bytes)를 검증하고, 이벤트 ID로 중복을 제거하고, 2xx 응답을 빠르게 보낸 다음, GET /v1/tasks/{id}를 호출하여 결과와 청구 상태를 읽으십시오.

워크스페이스 작업 웹훅은 2026년 9월 27일에 출시되었습니다. 웹훅 수명 주기, 테스트 전송, 시크릿 교체 및 전송 기록을 위한 Management API를 사용할 수 있습니다. 대시보드 및 MCP 관리 기능도 제공됩니다.

먼저 한 가지 정정 사항이 있습니다. 이전 비동기 이미지 생성 가이드에서 TokenLab이 작업 콜백을 지원하지 않는다고 언급했으나, 이는 2026년 9월 27일 이전의 사실이며 해당 가이드는 본 가이드와 함께 업데이트되었습니다.

웹훅인가 폴링인가? 둘 다 사용하세요

이 둘은 서로 다른 문제를 해결하며, 어느 하나가 다른 하나를 대체하지 않습니다.

상황 권장 사항
작업이 끝나는 즉시 반응하고 싶을 때 웹훅
권위 있는 결과나 비용이 필요할 때 GET /v1/tasks/{id}
수신기가 잠시 다운되었을 때 저장된 작업 ID를 사용한 폴링
전송이 누락될 경우를 대비한 폴백이 필요할 때 느린 간격의 폴링

웹훅이 상태 쿼리를 없애거나 폴링 제한을 추가하는 것은 아닙니다. 둘 다 유지하십시오. 웹훅을 사용하더라도 저장된 작업 ID를 읽는 느린 조정 루프(reconciliation loop)는 저렴한 보험과 같습니다.

폴링을 사용하는 경우 poll_url을 사용하고, 작업이 보류 중일 때는 대기(back off)하며, 최종 상태에서 중단하십시오. 401, 403, 404가 발생하거나 error.retryable == false일 때 중단하십시오. 503 async_task_owner_unavailable 오류는 백오프를 적용하여 재시도하십시오. 누락되거나 만료된 작업은 404 async_task_not_found를 반환합니다. 폴링 계약에 대한 자세한 내용은 비동기 작업 및 폴링 가이드를 참조하십시오.

세 가지 자격 증명, 세 가지 작업

이들을 혼동하는 것은 수신기를 고장 내는 가장 빠른 방법입니다.

자격 증명 접두사 용도 참고
Management Token mt-… /v1/management/webhooks*에서 웹훅 생성, 나열, 업데이트, 삭제, 테스트 및 교체 Authorization: Bearer mt-…로 전송. 워크스페이스 범위
API key sk-… 모델 요청 제출 및 GET /v1/tasks/{id}를 통한 작업 상태 읽기 Management API에서 거부됨
Signing secret whsec_… 수신기에서 전송 검증 Bearer 토큰이 아님

Management Token에 관한 두 가지 사항입니다. 첫째, 다른 워크스페이스 관리 작업도 승인하므로 웹훅 전용 자격 증명이 아닙니다. 작업을 제출하는 API 키와 동일한 워크스페이스를 선택하십시오. 둘째, 대시보드 → API → Management Tokens에서 생성합니다. 다른 Management API 예시를 참조하십시오.

mt-…와 whsec_…는 백엔드에만 보관하십시오. 브라우저나 모바일 클라이언트에 절대 배포하지 마십시오.

엔드포인트를 생성하고 시크릿을 즉시 저장

생성 호출은 웹훅 id와 whsec_…로 시작하는 일회성 secret을 포함하여 201을 반환합니다. 나열, 가져오기, 업데이트 시에는 해당 시크릿이 다시 표시되지 않습니다. 확인하는 즉시 저장하십시오.

export TOKENLAB_MANAGEMENT_TOKEN="mt-your-management-token"
curl https://api.tokenlab.sh/v1/management/webhooks \
  -H "Authorization: Bearer $TOKENLAB_MANAGEMENT_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://your-app.example/webhooks/tokenlab","events":["task.completed","task.failed","task.timeout"],"description":"Production task results"}'

동일한 엔드포인트를 세 가지 방식으로 관리할 수 있으며, 모두 동일한 객체를 편집합니다:

URL 규칙은 엄격합니다. 엔드포인트는 공개 HTTPS여야 합니다. URL에 자격 증명, 쿼리 문자열 또는 프래그먼트를 포함할 수 없습니다. 리다이렉트는 따르지 않으므로 301은 실패한 전송으로 간주됩니다.

워크스페이스당 최대 10개의 엔드포인트를 가질 수 있습니다. 11번째 생성 시 409 webhook_limit_reached가 반환됩니다.

메서드 경로 목적
GET /v1/management/webhooks 엔드포인트 나열
POST /v1/management/webhooks 엔드포인트 생성
GET /v1/management/webhooks/{webhookId} 엔드포인트 하나 읽기
PATCH /v1/management/webhooks/{webhookId} 업데이트, 일시 중지 또는 재개
DELETE /v1/management/webhooks/{webhookId} 삭제
POST /v1/management/webhooks/{webhookId}/rotate-secret 서명 시크릿 교체
POST /v1/management/webhooks/{webhookId}/test webhook.test 전송
GET /v1/management/webhooks/{webhookId}/deliveries?page=1&limit=50 전송 기록, 최대 100개 제한

PATCH {"is_active": false}로 일시 중지하고, PATCH {"is_active": true}로 재개합니다. 재개하면 연속 실패 횟수가 초기화되는데, 이는 장애 발생 후 중요합니다.

실제로 도착하는 데이터

모든 전송은 JSON 봉투(envelope)가 포함된 POST 요청입니다. Management API 필드는 snake_case이지만 콜백 필드는 camelCase입니다. 한쪽의 표기법이 다른 쪽에도 적용된다고 가정하지 마십시오.

필드 의미
id 이벤트 ID. 중복 제거에 사용
type 이벤트 유형
created Unix 초
data 이벤트 페이로드, 형태는 이벤트에 따라 다름
이벤트 발생 시점
task.completed 작업 성공 완료
task.failed 작업 실패로 종료
task.timeout 작업 시간 제한 도달
webhook.test 테스트 작업에 의해서만 전송

task.completed는 taskType(예: video 또는 image), taskId, 선택적 model, durationMs, resultUrls 및 settledCost를 포함합니다.

task.failed는 taskType, taskId, error, errorCode, retryable 및 refundOutcome을 포함합니다.

task.timeout은 taskType, taskId, refundOutcome 및 대기 시간 필드를 포함합니다. 해당 값들은 작업 레코드를 읽으십시오. 필드 세트는 작업에 따라 다릅니다.

구독은 워크스페이스 내 비동기 작업의 향후 최종 이벤트를 다룹니다. 동기식 결과 및 과거 작업은 다시 재생되지 않습니다. 선택한 이벤트 유형에 대해 모든 워크스페이스 작업을 수신하므로, data.taskId를 작업을 생성할 때 저장한 ID와 대조하십시오.

작업에 따라 필드가 없을 수 있습니다. 이것이 바로 원래 워크스페이스의 sk-… 키를 사용하여 GET /v1/tasks/{id}를 호출하는 것이 결과와 청구 상태의 진실 공급원(source of truth)으로 남는 이유입니다. 이벤트는 무언가가 완료되었음을 알려주고, 작업 레코드는 무엇을 생성했고 비용이 얼마인지 알려줍니다.

실패 이벤트의 retryable에 대해 한 가지 더 말씀드리면, 이는 생성 실패를 설명하는 것이지 자동으로 재제출하라는 지시가 아닙니다. 새로운 제출은 새로운 청구 대상 작업입니다.

원시 바이트를 검증하고 한 번만 처리

모든 POST 요청은 세 가지 헤더를 포함합니다:

  • X-Webhook-ID
  • X-Webhook-Timestamp, Unix 초
  • X-Webhook-Signature, sha256= 형식

서명은 정확한 타임스탬프 문자열, 마침표(.), 그리고 원시 요청 본문 바이트에 대해 전체 whsec_… 시크릿을 키로 사용하여 HMAC-SHA256을 수행한 것입니다. 순서와 본문 내용 모두 중요합니다.

서명 검증을 망치는 가장 흔한 두 가지 실수는 다음과 같습니다:

  1. 파싱된 JSON을 검증하는 것. 본문을 파싱하고 다시 직렬화하면 바이트가 변경되어 HMAC이 일치하지 않게 됩니다. 원시 본문을 읽으십시오. 검증이 통과될 때까지 바이트 상태로 유지하십시오.
  2. 교체 중에 하나의 시크릿으로만 검증하는 것. 교체 후에도 전송 중인 데이터는 이전 서명을 가지고 있을 수 있습니다. 짧은 기간 동안은 여러 시크릿 목록을 허용하십시오.

아래의 Node 수신기는 의존성이 없으며 node:http를 사용합니다. 원시 본문을 읽고, 시크릿 목록에 대해 검증하며, 300초 창을 확인하고, 본문의 id와 X-Webhook-ID를 비교하고, 이벤트 ID로 중복을 제거한 뒤 큐에 넣고 204를 반환합니다. 샘플의 중복 제거는 메모리 내 세트를 사용하지만, 프로덕션에서는 고유 데이터베이스 제약 조건을 사용하십시오.

import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';

// 교체 중에는 새 시크릿과 이전 whsec_ 시크릿을 모두 나열하십시오.
const SECRETS = (process.env.TOKENLAB_WEBHOOK_SECRETS ?? '').split(',').filter(Boolean);
const TOLERANCE_SECONDS = 300;
const seen = new Set(); // 프로덕션에서는 메모리가 아닌 고유 DB 제약 조건을 사용하십시오.

function verify(rawBody, headers) {
  const timestamp = headers['x-webhook-timestamp'];
  const signature = headers['x-webhook-signature'];
  if (typeof timestamp !== 'string' || !/^\d+$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;
  if (typeof signature !== 'string' || !/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
  const received = Buffer.from(signature.slice(7), 'hex');
  return SECRETS.some((secret) => {
    const expected = createHmac('sha256', secret).update(timestamp + '.').update(rawBody).digest();
    return timingSafeEqual(expected, received);
  });
}

const server = createServer((req, res) => {
  if (req.method !== 'POST' || req.url !== '/webhooks/tokenlab') {
    res.writeHead(404).end();
    return;
  }
  const chunks = [];
  req.on('data', (chunk) => chunks.push(chunk));
  req.on('end', () => {
    const rawBody = Buffer.concat(chunks); // JSON.parse 전, 정확한 바이트를 검증
    if (!verify(rawBody, req.headers)) {
      res.writeHead(401).end();
      return;
    }
    const event = JSON.parse(rawBody.toString('utf8'));
    if (event.id !== req.headers['x-webhook-id']) {
      res.writeHead(400).end();
      return;
    }
    if (!seen.has(event.id)) {
      seen.add(event.id);
      enqueue(event); // 핸드오프; 요청 외부에서 느린 작업 수행
    }
    res.writeHead(204).end();
  });
});

function enqueue(event) {
  console.log('queued', event.type, event.data?.taskId);
}

server.listen(Number(process.env.PORT ?? 3000));

이 수신기는 2026년 9월 28일에 프로덕션 발신자와 동일하게 서명된 요청(유효한 전송, 중복 전송, 교체 중 이전 시크릿, 잘못된 시크릿, 오래된 타임스탬프, 헤더와 본문 ID 불일치, 변조된 본문, 재직렬화된 JSON)에 대해 로컬에서 테스트되었습니다. 8가지 경우 모두 통과했습니다. 중복 항목은 한 번만 큐에 추가되었습니다.

Python 측은 단일 검증 함수입니다. hmac.compare_digest로 서명을 비교하며 Flask의 request.get_data() 또는 FastAPI의 await request.body()로부터 원시 본문 바이트를 기대합니다.

import hashlib
import hmac
import re
import time

TOLERANCE_SECONDS = 300
SIGNATURE_RE = re.compile(r"^sha256=[a-f0-9]{64}$")


def verify_webhook(raw_body: bytes, headers, secrets: list[str]) -> bool:
    """하나 이상의 whsec_ 시크릿에 대해 TokenLab 웹훅을 확인합니다.

    raw_body는 JSON 파싱 전 읽은 정확한 요청 바이트여야 합니다.
    (Flask: request.get_data(), FastAPI/Starlette: await request.body())
    """
    timestamp = headers.get("x-webhook-timestamp", "")
    signature = headers.get("x-webhook-signature", "")
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
        return False
    if not SIGNATURE_RE.match(signature):
        return False
    received = signature.removeprefix("sha256=")
    for secret in secrets:
        expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
        if hmac.compare_digest(expected, received):
            return True
    return False

2026년 9월 28일에 테스트 완료: 유효, 이전 시크릿, 잘못된 시크릿, 오래된 타임스탬프, 변조된 본문, 기본 json.dumps 공백으로 재직렬화된 본문 등 6가지 경우 모두 통과했습니다.

서명 외에도 모든 요청에 대해 다음 세 가지를 수행하십시오:

  • 현재 시간으로부터 300초 이상 차이 나는 타임스탬프는 거부하십시오. 이는 5분이며, 재생 공격이 얼마나 오래될 수 있는지 제한합니다.
  • 본문 id가 X-Webhook-ID와 같은지 확인하십시오.
  • 이벤트 ID를 작업 항목과 함께 원자적 쓰기(atomic write)로 저장하고 고유 제약 조건을 적용하십시오. 그런 다음 2xx를 빠르게 반환하고 자체 큐에서 무거운 작업을 수행하십시오.

전송은 반복될 수 있으며 순서는 보장되지 않습니다. 타임스탬프 창은 재생 공격의 유효 기간을 제한합니다. 이벤트 ID 중복 제거는 이중 처리를 방지합니다.

재시도, 자동 일시 중지 및 복구 런북

각 전송 주기는 최대 3번의 시도를 수행합니다.

시도 대기 시간 시도 타임아웃
1 없음 10 s
2 1 s 10 s
3 4 s 10 s

출처: TokenLab 웹훅 가이드, 2026년 9월 28일 관찰됨.

모든 시도는 새로운 타임스탬프와 서명을 가집니다. 즉, 서명 검증은 캐시된 값이 아닌 동일한 요청의 타임스탬프를 사용해야 합니다.

재시도 가능한 응답: 네트워크 오류, 429, 5xx. 주기 내에서 재시도되지 않는 경우: 기타 4xx, 리다이렉트, 잘못된 네트워크 대상. 일시적인 장애는 동일한 전송 ID를 가진 동일한 이벤트의 나중 재시도를 트리거할 수 있으며, 이것이 중복 제거가 선택 사항이 아닌 또 다른 이유입니다.

10회 연속 실패 주기는 엔드포인트를 자동으로 일시 중지합니다.

수신기가 다운되었을 때 다음 순서대로 작업하십시오:

  1. 수신기를 수정하십시오. 원시 바이트를 읽고 2xx를 빠르게 반환하는지 확인하십시오.
  2. PATCH {"is_active": true}로 엔드포인트를 재개하십시오. 이는 실패 횟수를 초기화합니다.
  3. POST …/test로 테스트를 전송하십시오. 테스트 API의 200은 시도가 기록되었음을 의미할 뿐입니다. 전송 기록을 확인하고 outcome == "delivered"인지 확인하십시오.
  4. 공백을 조정하십시오. 엔드포인트가 일시 중지된 동안 저장한 작업 ID를 가져와 각각 GET /v1/tasks/{id}를 호출하십시오.
  5. 그제야 웹훅 스트림을 다시 신뢰하십시오.

전송 기록은 outcome, http_status, attempts 및 delivered_at을 제공합니다. 메타데이터만 저장하며 페이로드는 저장하지 않습니다. 이전 이벤트는 수동으로 재생할 수 없으므로 4단계는 선택 사항이 아닙니다. 저장된 작업 ID가 복구 경로입니다.

이벤트 누락 없이 시크릿 교체

교체는 되돌릴 수 없으므로 시작하기 전에 창을 계획하십시오.

  1. POST /v1/management/webhooks/{webhookId}/rotate-secret을 호출하십시오. 응답은 새 시크릿을 한 번 반환합니다.
  2. 수신기의 검증 목록에 새 시크릿을 추가하십시오. 이전 시크릿도 목록에 유지하십시오.
  3. 아무것도 삭제하기 전에 수신기 변경 사항을 배포하십시오. 목록은 두 시크릿을 동시에 보유해야 합니다.
  4. 테스트를 보내고 기록에서 outcome == "delivered"인지 확인하십시오.
  5. 짧은 기간 후, 이전 시크릿을 제거하고 재배포하십시오.

전송 중인 데이터는 이전 서명을 가지고 있을 수 있습니다. 한 단계에서 시크릿을 교체하면 해당 이벤트가 삭제됩니다. 하나의 시크릿만 보유한 검증기는 교체 직전에 서명된 전송을 거부할 수 있습니다.

MCP에서 웹훅 관리

에이전트에서 TokenLab을 구동하는 경우, MCP 서버는 동일한 수명 주기를 노출합니다. full 프로필과 함께 @tokenlabai/mcp-server를 사용하십시오. 도구는 list_webhooks, create_webhook, get_webhook, update_webhook, delete_webhook, rotate_webhook_secret, test_webhook 및 list_webhook_deliveries입니다.

서버는 TOKENLAB_MANAGEMENT_TOKEN에서 Management Token을 읽습니다. 2026년 9월 28일에 관찰된 최신 게시 패키지는 0.6.24입니다. MCP는 대시보드에서 보는 것과 동일한 엔드포인트를 편집하므로 조정할 별도의 상태가 없습니다.

FAQ

이미지 작업도 웹훅을 보내나요?

네. 이미지 작업을 포함하여 워크스페이스의 모든 비동기 작업은 해당 이벤트 유형을 구독하는 엔드포인트로 최종 이벤트를 보냅니다. 페이로드의 taskType 필드는 작업 유형(예: video 또는 image)을 알려줍니다. 동기식 결과는 포함되지 않습니다.

엔드포인트가 다운되면 어떻게 되나요?

각 주기는 최대 3번 재시도합니다. 10회 연속 실패 주기는 엔드포인트를 자동으로 일시 중지합니다. 일시적인 전송 실패는 나중에 동일한 전송 ID로 재시도될 수 있습니다. 엔드포인트가 일시 중지되면 일시 중지 기간 동안의 이벤트는 나중에 전송되지 않으며 수동으로 재생할 수 없습니다. 수신기를 수정하고, 엔드포인트를 재개하고, 테스트를 보내고, 저장된 작업 ID로 GET /v1/tasks/{id}를 호출하여 공백 기간 동안 생성된 작업을 조정하십시오.

이전 이벤트를 재생할 수 있나요?

아니요. 전송 기록은 페이로드가 아닌 메타데이터만 보유하며 수동 재생 기능은 없습니다. 타임스탬프 창은 300초보다 오래된 모든 것을 거부합니다. 작업 API를 통한 조정이 지원되는 따라잡기 방식입니다.

retryable: true인 task.failed를 자동으로 재제출해도 안전한가요?

아니요. retryable은 생성 실패를 설명합니다. 재제출하라는 지시가 아닙니다. 새로운 제출은 새로운 청구 대상 작업이므로, 비용을 고려하여 직접 재시도 여부를 결정하십시오.

Seedance 호환성 API도 이 웹훅을 사용하나요?

아니요. 요청별 callback_url은 자체 페이로드를 가진 별도의 계약입니다. 워크스페이스 이벤트나 이 HMAC 헤더를 사용하지 않으므로, 하나의 검증기를 둘 다에 지정하지 마십시오.

웹훅 가이드의 전체 계약으로 시작한 다음, API 키를 생성하고 작업을 제출하는 워크스페이스에서 첫 번째 엔드포인트를 켜십시오.

출처

최근 출시된 모델

이 가이드의 모델로 바로 구축하기

가격을 비교하고 라우트를 테스트한 뒤, 조사 내용을 실제 API 호출로 이어가세요.