본문으로 건너뛰기

구현 가이드

요청이 사라진 뒤에 작업을 끝내세요.

미디어 작업은 그것을 만든 HTTP 호출보다, 그것을 띄운 페이지보다, 종종 그때 돌던 배포보다도 오래 살아남습니다. 이 글은 그걸 견디게 만드는 방법입니다.

해결 과제

연결은 트랜잭션이 아닙니다

제출부터 파일을 손에 쥘 때까지 작업을 잃을 곳이 넷 있습니다. 타임아웃 뒤 생각 없이 다시 보내는 생성, 검증할 수 없는 콜백, 두들기거나 포기하는 폴링 루프, 그리고 어떻게 처리할지 고민하는 사이에 만료된 결과 URL입니다.

01

키 없는 재전송은 두 번째 청구서입니다

Idempotency-Key가 있으면 24시간 안의 같은 모델·입력·콜백 재전송은 자금을 두 번 묶지 않고 원래 작업을 돌려줍니다. 범위는 API 키 하나이며, 형제 키가 같은 값을 재생하면 409만 받고 작업 ID는 받지 못합니다.

02

검증하지 않은 콜백은 열린 엔드포인트입니다

전달에는 X-Webhook-Timestamp와, taskId.timestamp.sha256(raw_body)로 계산한 Base64 HMAC-SHA256이 X-Webhook-Signature에 실립니다. 원시 바이트를 대상으로 상수 시간 비교하고, 오래된 타임스탬프는 작업이나 과금 상태를 건드리기 전에 거부하세요.

03

상한 없는 폴링은 예약된 레이트 리밋입니다

recordInfo는 보조 수단이지 전송 계층이 아닙니다. 2초쯤에서 시작해 매번 1.5배로 늘리고 15초에서 멈추며, 네트워크가 아니라 여러분이 정한 기한에서 끝내세요.

구현 방식

작업마다 지속 레코드 하나를, 응답보다 먼저 쓰세요

멱등 키, taskId, 자체 상관 ID를 호출자에게 응답하는 것과 같은 트랜잭션에서 저장하세요. 이후의 콜백, 폴링, 재시도, 문의는 모두 그 한 행으로 귀결되고, 중복 전달은 두 번째 주문이 아니라 한 번의 갱신이 됩니다.

먼저 검증하고 그다음에 읽으세요. 서명은 원시 바이트를 덮으므로 비교가 통과한 뒤에 파싱합니다.

Verify the callback before you trust it

import { createHash, createHmac, timingSafeEqual } from "node:crypto";

// rawBody is the exact bytes that arrived. Parse only to read the task ID:
// the digest is always computed over those raw bytes, never over a re-encode.
export function verifyDelivery(rawBody, headers, webhookKey) {
  const timestamp = headers["x-webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const taskId = JSON.parse(rawBody).data.taskId;          // payload version 2
  const digest = createHash("sha256").update(rawBody).digest("hex");
  const expected = createHmac("sha256", webhookKey)
    .update(`${taskId}.${timestamp}.${digest}`)
    .digest("base64");

  const sent = Buffer.from(headers["x-webhook-signature"] ?? "", "utf8");
  const mine = Buffer.from(expected, "utf8");
  return sent.length === mine.length && timingSafeEqual(sent, mine);
}

Delivery headers

X-Webhook-Timestamp: 1788652800
X-Webhook-Signature: <base64>
X-Webhook-Payload-Version: 2

{
  "code": 200,
  "msg": "success",
  "request_id": "whd_01k3m8x9q2z4v7n5p6r8s0t1w3",
  "data": { "taskId": "job_01k3…", "state": "succeeded", "settled": true, … }
}

계약

먼저 못 박아 둘 항목

비동기 미디어 API: 작업, 웹훅, 재시도 — 먼저 못 박아 둘 항목
필드 또는 헤더값 또는 상한
Idempotency-Key24 h · scoped to one API key
X-Webhook-Signaturebase64(HMAC-SHA256)
signed stringtaskId.timestamp.sha256(raw_body)
X-Webhook-Payload-Version2 for new integrations

좌우로 밀어 모든 항목을 확인하세요.

연동

첫 호출에서 운영까지

  1. 01멱등 키를 만들어 저장하고 그것과 함께 createTask를 보냅니다. callBackUrl은 80 또는 443 포트의 HTTPS 엔드포인트로 둡니다.
  2. 02호출자에게 응답하기 전에 data.taskId를 데이터베이스에 씁니다. 202는 그것이 처음 나타나는 유일한 자리입니다.
  3. 03콜백을 받아 원시 본문으로 서명을 검증하고 request_id로 중복을 제거한 뒤 레코드를 갱신합니다.
  4. 04보조로 지수 백오프의 recordInfo를 쓰고, 서명된 URL이 만료되기 전에 output.assets를 자체 스토리지로 복사합니다.

적합성

이 경로가 맞는 경우

이럴 때 고르세요

  • 미디어를 만드는 모든 것. 이미지·영상·오디오 엔드포인트는 구조상 비동기입니다.
  • 무엇을 보냈고 얼마가 들 예정이었고 어떻게 끝났는지에 대한 지속적인 기록이 필요한 경우.

이럴 때는 다른 길로

  • 텍스트 모델을 호출하는 경우. 그쪽은 요청 안에서 스트리밍하고 실측 사용량으로 정산합니다.
  • 브라우저가 작업을 소유하게 하려는 경우. 키는 서버의 것이고 작업 레코드도 마찬가지입니다.

근거

직접 확인할 수 있는 것

모든 응답에 request_id가 있습니다

/api/v1의 모든 동작에서 봉투는 {code, msg, data, request_id}입니다. 페이로드 버전 2에서는 전달의 request_id가 재전송 사이에도 유지되므로 그대로 중복 제거 키가 됩니다.

보류는 레코드에서 보입니다

settled: false인 cost는 묶어 둔 견적액이고, settled: true가 최종 청구입니다. 성공해도 보류액을 넘지 않으며, 실패나 만료는 전액 해제합니다.

실패는 닫힌 어휘를 씁니다

errorCode는 아홉 개의 안정된 값 가운데 하나입니다. invalid_request, unsupported_combination, content_rejected, rate_limited, upstream_unavailable, generation_failed, timeout, invalid_asset, upstream_failed. 그 밖의 값은 upstream_failed로 처리하세요.

출시 전

확인해 둘 플랫폼 동작

  • 멱등 키는 프로세스 메모리가 아니라 작업과 함께 저장하세요.
  • 콜백 처리를 멱등하게 만드세요. 전달은 설계상 재시도됩니다.
  • 폴링에 기한을 두고, 기한을 넘긴 작업에 알림을 거세요.
  • expiresAt 전에 결과물을 복사하고, 서명된 URL을 캐시하지 말고 다시 요청하세요.

자주 묻는 질문

이 페이지에서 자주 나오는 질문

같은 Idempotency-Key로 같은 요청을 다시 보내세요. 24시간 안에 같은 API 키라면 두 번째 보류 없이 원래 작업이 돌아옵니다. 타임아웃 뒤에 새 본문을 보내면 안 됩니다. 그건 따로 과금되는 새 작업입니다.

다음

배포를 넘겨도 살아남는 작업 흐름 만들기

오래 걸리는 미디어 작업을 서버에서 보내고 확실히 끝내기. 멱등 키, 서명된 콜백, 상한을 둔 폴링, 다섯 상태의 상태 기계.

관련 가이드