블로그/developers·2026년 9월 1일·4분·eroq 팀 작성
비동기 동영상 생성 API: 작업, 폴링, 서명된 웹훅
돈과 클립을 잃지 않는 비동기 동영상 생성 API 연동법. 작업 제출, 폴링 또는 서명된 웹훅, 안전한 환불과 재시도 처리.
동영상 렌더는 1분에서 5분이 걸려요. 그렇게 오래 열려 있어도 되는 HTTP 요청은 없어요. 로드 밸런서는 타임아웃되고, 모바일 통신은 끊기고, 사용자는 새로고침을 누르니까요. 그래서 제대로 된 동영상 생성 API는 모두 비동기예요. 작업을 제출하면 ID를 받고, 결과는 나중에 알게 돼요. 그 “나중”의 세부 사항에서 연동이 돈과 클립을 잃어요. eroq 동영상 엔드포인트가 어떻게 생겼는지, 그리고 어떻게 제대로 쓰는지 정리했어요.
한 문단으로 보는 계약
POST /v1/videos/generations는 고정 크레딧 가격을 차감하고, 렌더를 시작하고, 작업 ID와 함께 202로 응답해요. 그다음엔 GET /v1/videos/generations/{id}를 폴링하거나(무료, 몇 초마다) 작업이 끝났을 때 서명된 웹훅을 받으면 돼요. 실패한 렌더, 또는 10분 안에 끝나지 않은 렌더는 자동으로 환불되고, 이용 정책을 벗어난 프롬프트는 content_blocked를 반환하며 청구되지 않아요. 비용은 실제로 전달된 클립에 대해서만 내요. 작업 ID는 24시간 동안 유지돼요.
작업 제출하기
curl https://eroq.ai/v1/videos/generations \
-H "Authorization: Bearer $EROQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "eroq-motion-one",
"prompt": "Slow push-in on a lighthouse keeper in a yellow raincoat climbing a spiral stone staircase at night, lantern swinging, warm practical light against cold blue storm windows, rain streaking the glass, tense and patient mood, 35mm film grain.",
"seconds": 10,
"aspect": "9:16",
"store": true
}'
응답은 이래요.
{
"id": "b7e6c2d4-…",
"status": "processing",
"usage": { "credits_spent": 60, "credits_remaining": 887 }
}
짚어 둘 파라미터가 두 개 있어요.
seconds는 클립 길이로, 1에서 30까지예요. 플랜이 상한을 정하고(종량제 10초, Hobby 15초, Creator 20초, Studio 이상 30초), 엔진이 지원하는 길이 단위에 맞춰 값이 조정돼요. Motion One은 5초 또는 10초를 렌더하고, Seedance 2.5는 4초에서 30초 사이 아무 길이나 렌더해요. 조정은 청구 전에 일어나기 때문에 가격은 언제나 실제로 렌더되는 클립과 일치해요. Motion One에 seconds: 8을 보내면 엔진의 길이 단위에 맞게 조정되고, 실제로 만든 클립 기준으로 청구돼요. 길이 단위를 미리 확인하고 싶다면 /v1/engines를 보세요.
aspect는 16:9, 9:16, 1:1, 21:9, 4:3, 3:4를 받아요. 엔진이 해당 비율을 기본 지원하면 그대로 전달되고, 그렇지 않으면 프롬프트에 녹여 넣어요. 엔진에 대한 강한 요청이지, 확실한 보장은 아니에요. 엔진별 세부 사항은 화면 비율 항목에 있어요.
store: true는 선택 사항이지만 프로덕션에서는 쓸 만한 가치가 있어요. 완성된 클립이 eroq Store로 가고, 작업에는 작업과 함께 만료되는 인라인 base64 대신 영구 CDN URL이 담겨요. 10 MB 블록이 새로 시작될 때마다 2 크레딧이 추가돼요(요금제).
방법 A: 폴링
폴링은 무료이고, 몇 초마다 하는 게 의도된 주기예요. 막힌 작업이 루프가 포기하기 전에 스스로 정리되도록, 기한은 10분 환불 기간보다 조금 길게 잡으세요.
const BASE = 'https://eroq.ai/v1'
const headers = { Authorization: `Bearer ${process.env.EROQ_API_KEY}` }
async function waitForClip(jobId, { every = 4000, deadline = 11 * 60_000 } = {}) {
const started = Date.now()
while (Date.now() - started < deadline) {
const job = await fetch(`${BASE}/videos/generations/${jobId}`, { headers }).then(r => r.json())
if (job.status === 'succeeded') return job.data[0].url // with store: true
if (job.status === 'failed') throw new Error(job.error?.code ?? 'generation_failed')
await new Promise(r => setTimeout(r, every))
}
throw new Error('timeout')
}
status는 processing에서 succeeded 또는 failed로 바뀌어요. failed일 때는 크레딧이 이미 지갑에 돌아와 있으니, “재시도”는 “청구서를 두고 다투기”가 아니라 “다시 제출하기”를 뜻해요.
방법 B: 서명된 웹훅
개발자 대시보드에서 엔드포인트를 등록하고, video.generation.succeeded와 video.generation.failed를 구독한 다음, whsec_ 시크릿을 복사하세요. 딱 한 번만 보여 줘요. 전달은 매번 JSON POST로 와요.
{
"id": "evt_9f1c…",
"type": "video.generation.succeeded",
"data": {
"id": "b7e6c2d4-…",
"result_url": "https://…/b7e6c2d4.mp4"
}
}
실패한 작업에는 result_url 대신 error 객체가 담겨요. 미디어는 절대 페이로드에 실려 오지 않아요. URL을 받거나 작업을 조회해야 해요.
서명은 Stripe 방식이고 eroq-signature 헤더에 담겨 와요. 형식은 t=<unix timestamp>,v1=<hex>이고, v1은 시크릿으로 ${t}.${rawBody}를 HMAC-SHA256한 값이에요. 원시 바이트로 검증하고, JSON 파싱은 검증이 끝난 뒤에 하세요.
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')))
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < toleranceSec
const a = Buffer.from(expected)
const b = Buffer.from(String(parts.v1 ?? ''))
return fresh && a.length === b.length && timingSafeEqual(a, b)
}
전달 시도는 10초 제한으로 한 번뿐이고, 고객의 서버가 다운돼도 고객의 렌더가 실패하지는 않아요. 그러니 바로 2xx로 응답하고, 작업은 그 뒤에 처리하고, 소식을 듣지 못한 건이 있을 때를 대비해 폴링을 대체 수단으로 유지하세요. GET /v1/videos/generations의 24시간 대기열 목록은 정확히 그런 대조를 위해 있어요.
멱등성: 실제로 돈이 새는 부분
여기서 생기는 실패는 전부 중복이에요. 폴러가 이미 성공을 확인한 뒤에 도착하는 웹훅, 제출 단계의 네트워크 오류 뒤에 하는 재시도, 202와 데이터베이스 쓰기 사이에 죽는 워커 같은 것들이요.
- 작업 ID는 사용자에게 보이는 상태와 같은 트랜잭션에서 저장하세요.
202를 받는 순간 청구가 발생해요. 사용자에게 “렌더 중”이라고 알리기 전에 그 ID가 디스크에 없으면, 크래시 한 번에 찾을 수 없는 클립 하나를 잃어요. 재시작할 때 대기열 목록과 대조하세요. - 작업 ID로 upsert하고, 이벤트는
evt_ID로 중복 제거하세요. 두 채널이 같은 완료를 보고할 수 있어요. 먼저 온 쪽이 이기고, 두 번째는 아무것도 하지 않아요. - 제출 단계에서 네트워크 오류가 났다면 확인하지 않고 다시 제출하지 마세요. 요청이 이미 처리됐을 수 있어요. 자신에게 두 번 청구하기 전에, 대기열 목록에서 최근 1분 안에 같은 프롬프트로 만든 작업이 있는지 확인하세요.
- 재시도는
failed일 때만 하세요. 환불은 이미 끝났으니, 새로 제출하면 새로운 청구가 한 번 생길 뿐이에요. 키별 요청 한도는 지키세요. 동영상은 기본 티어에서 분당 6건이고, 플랜에 따라 배수가 붙어요.
고정 크레딧 요금제 덕분에 이 모든 걸 감사하기 쉬워요. 작업 하나는 이미 알려진 청구 하나와 대응하고, 응답의 usage 블록이 그 시점의 잔액을 알려 줘요.
자주 묻는 질문
클립을 가져가기까지 시간이 얼마나 있나요?
작업 ID는 생성 후 24시간이 지나면 만료되고, 인라인 base64도 함께 사라져요. 계속 남는 CDN URL이 필요하면 store: true를 쓰거나, 완료되는 즉시 다운로드하세요.
웹훅 엔드포인트가 다운되면 어떻게 되나요?
한 번 시도하고 끝이에요. GET /v1/videos/generations/{id}를 폴링하거나 최근 24시간 목록을 조회해서 따라잡으세요. 웹훅은 편의 기능이고, 진실의 원천은 폴링이에요.
사운드트랙도 받을 수 있나요?
veo-3-fast에서만 가능해요. 고정 8초 클립에 앰비언스, 효과음, 대사를 자체 렌더해요. 업스트림 제공사가 검수하고, Creator 플랜이 필요해요. 다른 엔진은 모두 무음 동영상을 렌더해요. 엔진 라인업을 참고하세요.
무료 크레딧 50개로 시작하세요. API 키를 발급받고 작업 하나를 제출해 보세요.
이 글에 나온 모델로 직접 만들어 보세요 — 무료 크레딧 50개로 시작하거나, 모든 엔진과 가격도 살펴보세요.