탐색으로 건너뛰기

개발 가이드

개발 가이드

빠른 시작에서는 요청 하나를 동작시키는 데 집중합니다. 이 페이지에서는 코드가 프로덕션에서 계속 안정적으로 동작하려면 무엇이 달라지는지 다룹니다.

두 가지 호출 형태

BeatAPI의 요청 수명 주기는 정확히 두 가지이며, 통합 과정의 실수는 대부분 한쪽을 다른 쪽처럼 다루는 데서 생깁니다.

텍스트워크플로, 이미지, 영상, 이펙트
반환값답변을 즉시 반환작업 ID를 즉시 반환
지연 시간수 초수 초~수 분
스트리밍서버 전송 이벤트(SSE)해당 없음
완료 시점HTTP 응답폴링 또는 웹훅 수신
과금 시점응답 시정산 시, 실패하면 환불

텍스트는 동기식입니다. POST /v1/chat/completions, /v1/responses, /v1/messages 또는 Gemini 호환 엔드포인트가 결과를 반환합니다. 그 밖의 요청은 작업을 접수하고 ID를 돌려줍니다.

비동기 수명 주기

  1. 제출 — POST /v1/images/tasks(또는 videos, effects, video-analysis, music-video, ecommerce-video). 201은 접수되었다는 뜻이지 완료되었다는 뜻이 아닙니다.
  2. ID 저장 — 백그라운드 작업을 시작하기 전에 data.id와 data.request_id를 저장하세요. 응답을 받은 뒤 저장하기 전에 프로세스가 중단되면, 비용은 냈지만 찾을 수 없는 작업이 남습니다.
  3. 대기 — GET /v1/tasks/{task_id}를 폴링하거나 웹훅을 등록합니다.
  4. 결과 확인 — succeeded이면 data.output.media를 사용합니다. failed이면 data.error_code와 환불 필드를 확인합니다.

폴링

import time
import random
import requests
TERMINAL = {"succeeded", "failed"}
def wait_for_task(api_key, task_id, timeout=900):
url = f"https://api.beatapi.io/v1/tasks/{task_id}"
headers = {"Authorization": f"Bearer {api_key}"}
deadline = time.time() + timeout
delay = 5.0
while time.time() < deadline:
response = requests.get(url, headers=headers, timeout=30)
if response.status_code == 429:
time.sleep(float(response.headers.get("Retry-After", 10)))
continue
response.raise_for_status()
task = response.json()["data"]
if task["status"] in TERMINAL:
return task
# Jitter keeps a fleet of workers from polling in lockstep.
time.sleep(delay + random.uniform(0, delay * 0.3))
delay = min(delay * 1.5, 30.0)
raise TimeoutError(f"task {task_id} did not finish within {timeout}s")

이 루프에서 중요한 세 가지 특성은 다음과 같습니다.

  • 종료 상태에서 멈춥니다. 경과 시간을 추측해 멈추지 않습니다. 완료된 작업은 다시 바뀌지 않습니다.
  • 지터를 넣어 백오프합니다. poll_after_seconds를 지키고, 동시에 실행되는 여러 작업의 폴링 시점이 겹치지 않게 하세요. 더 자주 폴링한다고 작업이 빨리 끝나지는 않습니다.
  • 429를 ‘실패’가 아닌 ‘대기’로 처리합니다. Retry-After를 따르세요.

Music Video 작업에는 계속 폴링하는 대신 판단이 필요한 비종료 상태(storyboard_ready, requires_action)가 있습니다. 이를 ‘아직 처리 중’으로 취급하면 폴링이 끝나지 않습니다. 모든 상태와 각 상태에서 해야 할 조치는 빠른 시작에 정리되어 있습니다.

폴링 대신 웹훅

POST /v1/webhooks로 엔드포인트를 등록하면 BeatAPI가 종료 상태를 해당 엔드포인트로 전송합니다. 외부에서 접근 가능한 HTTPS 엔드포인트가 필요하지만, 폴링 루프를 완전히 없앨 수 있습니다.

JSON을 파싱하기 전에 원본 요청 바이트로 서명을 검증하세요. 본문을 다시 직렬화하면 내용이 바뀌어 서명이 일치하지 않습니다. 웹훅을 참고하세요.

웹훅을 설정했더라도 느린 주기의 대조용 폴링은 유지하세요. 전달이 실패할 수 있으며, 모델의 일반적인 처리 시간보다 훨씬 오래 processing 상태인 작업은 직접 확인해 볼 필요가 있습니다.

오류

모든 실패에는 고정된 error.code, 원인을 설명하는 영어 message, retryable, request_id가 포함됩니다. request_id를 로그에 남기세요. 지원팀이 정확한 호출을 찾는 데 사용합니다.

{
"error": {
"code": "rate_limit_exceeded",
"message": "Too many requests. Wait 12 seconds, then retry.",
"retryable": true,
"request_id": "req_abc123",
"retry_after_seconds": 12
}
}
HTTP재시도 여부의미
400아니요요청이 잘못되었습니다. 재시도해도 같은 잘못된 요청이 전송됩니다
401아니요missing_api_key 또는 invalid_api_key: 키가 없거나 잘못되었거나 만료, 비활성화, 소진되었습니다
402아니요insufficient_credits: 잔액이 0이거나 가격보다 적습니다. 충전하세요
403아니요forbidden: 이 키나 계정으로는 이 작업을 수행할 수 없습니다
404아니요not_found: 알 수 없는 모델이거나, 잘못된 ID 또는 다른 계정의 ID입니다
409아니요idempotency_conflict: 멱등성 키를 다른 본문에 재사용했습니다
429예, Retry-After 이후요청 속도 또는 동시 실행 한도
500, 503예, 백오프 적용일시적인 오류입니다. 다시 제출하지 말고 이미 알고 있는 작업을 폴링하세요

오류의 retryable도 코드별로 같은 정보를 알려 주므로, 상태 코드 대신 이 값으로 분기할 수 있습니다. BeatAPI는 502나 504를 반환하지 않습니다. 이런 응답을 받았다면 네트워크 엣지에서 발생한 것이며 503처럼 재시도하면 됩니다. 모든 코드는 오류에 나와 있습니다.

import requests
response = requests.post(
"https://api.beatapi.io/v1/images/tasks",
headers={"Authorization": f"Bearer {api_key}"},
json={"model": "nano-banana", "prompt": "a cute panda"},
timeout=30,
)
if not response.ok:
error = response.json().get("error", {})
# request_id is the only thing that identifies this call later.
log.error("beatapi failed", code=error.get("code"), request_id=error.get("request_id"))
if response.status_code == 402:
alert_billing()
elif response.status_code == 429:
schedule_retry(after=error.get("retry_after_seconds", 10))

제출 요청에서 받은 500은 판단하기 애매합니다. 작업이 생성되었을 수도, 생성되지 않았을 수도 있습니다. 무작정 다시 제출하면 같은 작업에 두 번 비용을 낼 수 있습니다. 작업 생성 시 Idempotency-Key를 보내고 재시도할 때 같은 값을 다시 사용하세요. BeatAPI는 두 번째 작업을 만들지 않고 원래 작업을 반환합니다.

요청 속도 제한

두 가지 제한이 따로 적용되며, 실패 방식도 다릅니다.

  • 요청 속도 — 키별 분당 요청 수입니다. 초과하면 Retry-After와 함께 429를 반환합니다. 한도는 누적 충전액에 따라 올라가며, 현재 값과 다음 단계는 대시보드와 GET /api/user/self에서 확인할 수 있습니다.

GET /v1/usage는 두 가지를 모두 보고합니다: concurrency.limit과 concurrency.active.

비용이 들지 않는 재시도

  • 429와 5xx는 백오프를 적용해 재시도하세요(BeatAPI가 반환하는 5xx는 모두 500 또는 503입니다). 400, 401, 402, 403, 404, 409는 재시도하지 마세요. 이 응답에는 retryable: false가 포함됩니다.
  • 지터를 넣은 지수 백오프를 사용하고 상한을 두세요.
  • 모든 작업 생성 요청에 Idempotency-Key를 넣어, 제출을 재시도해도 중복 과금되지 않게 하세요.
  • 제출이 실패했는데 ID가 있다면 다시 제출하기 전에 폴링하세요.
  • 전체 시도 횟수에 상한을 두세요. 계속 실패하는 요청을 끝없이 재시도하면 비용이 계속 나가는 장애가 됩니다.

비용 관리

  • 텍스트 호출에는 출력 상한을 설정하세요. 비용이 큰 쪽은 출력이며, max_tokens가 가장 간단한 조절 수단입니다.
  • 단계에 맞는 모델을 고르세요. 분류나 라우팅에는 최상위 모델이 필요하지 않습니다.
  • 구간별 요금 모델에 주의하세요. GPT-5.6 계열과 Grok 계열은 컨텍스트가 일정 기준을 넘으면 더 높은 요금이 적용됩니다. DeepSeek HC(high-cache) 모델 두 개는 평일 베이징 시간 09:00–12:00와 14:00–18:00에, Hunyuan hy3는 공개된 피크 시간대에 더 높은 요금이 적용됩니다. 일반 유료 DeepSeek 모델은 시간과 관계없이 같은 요금입니다. 오래 실행되는 에이전트나 예약된 배치 작업은 모르는 사이에 이 기준을 넘을 수 있습니다. 각 응답의 사용량 기록에서 어떤 요금이 적용되었는지 확인할 수 있습니다.
  • 구성 요소마다 키를 하나씩 사용하세요. 그러면 별도 작업 없이 GET /v1/usage의 by_api_key로 비용을 구분해 볼 수 있습니다.
  • 환불 내역을 대조하세요. 실패한 작업은 환불됩니다. 장부에 보류 금액만 기록하면 실제 청구서와 숫자가 어긋납니다.

프로덕션 배포 전 확인 사항

  • 정확한 https://api.beatapi.io 오리진을 사용하세요.
  • 영구 키는 서버에만 두세요. 브라우저에는 API 키가 아니라 수명이 짧은 Realtime client_secret을 전달하세요.
  • 백그라운드 작업을 시작하기 전에 data.id와 request_id를 저장하세요.
  • 지터를 넣어 폴링하고, 종료 상태나 조치가 필요한 상태에서 멈추세요.
  • 웹훅 서명은 원본 바이트로 검증하세요.
  • 보류, 정산, 환불, 오류, 최종 출력 URL을 기록하세요.
  • 402 응답에 알림을 걸어 두세요. 이 상태가 되면 계정의 모든 유료 작업이 한꺼번에 중단됩니다.