문서웹훅 연동

웹훅 연동

변환 작업이 완료되거나 실패하면 웹훅으로 알림을 받으세요. 요청에 담긴 서명을 검증하는 방법도 안내해요.

변환 작업을 생성할 때 webhook_url을 지정하세요. 작업이 완료되거나 실패하면 해당 주소로 서명된 POST 요청을 보내 드려요.

웹훅은 REST API 전용 기능이에요. MCP 서버에서는 웹훅 파라미터를 지원하지 않아요.

알림을 받을 URL 지정하기

영상 링크를 보내거나 청크 업로드를 예약할 때는 POST /v1/transcriptions의 JSON 본문에 webhook_url을 넣으세요. 파일을 요청 한 번으로 올릴 때는 폼 필드로 보내면 돼요.

{
  "platform": "youtube",
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "webhook_url": "https://example.com/hooks/transcript1"
}

HTTPS URL을 사용하고 URL에 사용자 이름이나 비밀번호를 넣지 마세요. 도메인을 조회했을 때 공개 IP 주소로 연결돼야 해요. 사설, 루프백, 링크 로컬 등 비공개 주소를 가리키면 422 invalid_request를 반환해요. 작업을 생성할 때뿐 아니라 웹훅을 보낼 때마다 주소를 다시 검사해요.

reuse_recent=true를 사용할 때는 webhook_url을 지정할 수 없어요.

알림이 오는 시점

type발생 조건
transcription.completed작업이 completed 상태로 끝났을 때
transcription.failed작업이 failed 상태로 끝났을 때. 원인은 data.error의 code, message에서 확인해요.

작업을 취소한 경우에는 웹훅을 보내지 않아요.

웹훅에 담기는 데이터

웹훅 본문은 다음과 같은 JSON이에요.

{
  "type": "transcription.completed",
  "created_at": "2026-10-13T09:30:04.000Z",
  "data": {
    "id": "tr_8fK2Lm0PqRsTuVwXyZaBcD",
    "object": "transcription",
    "status": "completed",
    "transcript_source": "youtube_captions",
    "billed_minutes": 4,
    "error": null
  }
}

이벤트가 발생한 시각은 created_at에 있어요. data에는 작업 객체가 들어가지만 transcript 본문은 제외해요. 위 예제는 일부 필드만 보여 줘요. 스크립트는 data에 있는 id로 GET /v1/transcriptions/:id를 호출해 가져오세요.

요청 헤더에는 Content-Type: application/json과 User-Agent: transcript1-webhooks/1.0이 들어가요. 서명 검증에 쓰는 헤더도 함께 보내요.

요청 서명 검증하기

웹훅을 보낼 때마다 X-Transcript1-Signature 헤더를 넣어요.

X-Transcript1-Signature: t=<unix seconds>,v1=<hex digest>
  • t: 요청에 서명한 시각을 Unix 시간의 초 단위로 나타내요.
  • v1: "<t>.<raw body>"를 HMAC-SHA256으로 서명한 16진수 값이에요. t 값 뒤에 점을 붙이고, 그 뒤에 받은 요청 본문을 그대로 이어 붙여 계산해요. 키에는 whsec_ 접두어까지 포함한 서명 시크릿 전체를 사용하세요.

서명은 JSON을 파싱하기 전에 받은 본문 바이트 그대로 검증하세요. 파싱 후 다시 JSON으로 만들면 공백이나 키 순서가 달라져 검증에 실패할 수 있어요. 서명값은 상수 시간으로 비교하세요. t가 현재 시각에서 너무 벗어난 요청도 거부해야 해요. 아래 예제에서는 5분 이내만 허용해요.

Node.js

import { Buffer } from 'node:buffer';
import crypto from 'node:crypto';
 
// header: X-Transcript1-Signature, rawBody: the request body exactly as received,
// secret: your full signing secret including the whsec_ prefix.
export const verifySignature = (secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) => {
  const fields = Object.fromEntries(header.split(',').map(part => part.trim().split('=')));
  const t = Number(fields.t);
 
  if (!Number.isInteger(t) || !fields.v1 || Math.abs(nowSeconds - t) > 300) {
    return false;
  }
 
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
 
  return fields.v1.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(fields.v1), Buffer.from(expected));
};

Express에서는 JSON 파서보다 먼저 원문 텍스트를 읽도록 설정하세요.

import process from 'node:process';
import express from 'express';
import { verifySignature } from './verify-signature.js';
 
const app = express();
 
app.post('/hooks/transcript1', express.text({ type: 'application/json' }), (request, response) => {
  const header = request.get('X-Transcript1-Signature') ?? '';
 
  if (!verifySignature(process.env.TRANSCRIPT1_WEBHOOK_SECRET, header, request.body)) {
    response.sendStatus(400);
    return;
  }
 
  const { type, data } = JSON.parse(request.body);
  console.log(type, data.id, data.status);
  response.sendStatus(200);
});

Python

import hashlib
import hmac
import time
 
 
def verify_signature(secret, header, raw_body, now=None):
    # header: the X-Transcript1-Signature header, raw_body: the request body exactly as received,
    # secret: your full signing secret including the whsec_ prefix.
    fields = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part)
    try:
        t = int(fields["t"])
    except (KeyError, ValueError):
        return False
 
    now = int(time.time()) if now is None else now
    if abs(now - t) > 300:
        return False
 
    expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(fields.get("v1", ""), expected)

Flask를 쓴다면 request.get_data(as_text=True)로 읽은 값을 raw_body에 넘기세요.

서명 시크릿 확인과 교체

팀마다 whsec_로 시작하는 서명 시크릿이 있어요. 해당 팀에서 만든 모든 변환 작업의 웹훅에 같은 시크릿을 사용해요. /dashboard/webhooks의 “Webhooks” 페이지에서 확인하거나 교체하세요. 교체는 즉시 적용되며, 이후 전송하는 웹훅부터 새 시크릿으로 서명해요.

전송과 재시도

  • 서버가 2xx로 응답하면 전송 성공으로 처리해요.
  • 다른 상태 코드, 연결 오류, 10초 내 응답 없음은 전송 실패로 처리해요.
  • 실패하면 대기 시간을 늘려 다시 보내요. 첫 전송을 포함해 총 6번까지 시도하며, 각 시도 사이는 2, 4, 8, 16, 32분이에요. 마지막 시도는 첫 전송에서 약 한 시간 뒤예요.
  • 리다이렉트는 따라가지 않아요. 3xx도 전송 실패이므로 최종 도착 URL을 직접 지정하세요.
  • 서버가 제한 시간 이후에 응답하는 경우처럼 같은 알림을 여러 번 받을 수 있어요. data.id와 type을 함께 확인해 중복 처리를 막으세요.

최근 업데이트 2026년 10월 14일