> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.beatapi.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.beatapi.io/_mcp/server.

# 개발 가이드

> 동기식 텍스트, 비동기 작업, 폴링, 웹훅, 재시도, 오류 처리까지 BeatAPI를 애플리케이션에 통합하는 방법을 설명합니다.

# 개발 가이드

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

## 두 가지 호출 형태

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`와 환불 필드를 확인합니다.

### 폴링

```python
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`를 따르세요.

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

### 폴링 대신 웹훅

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

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

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

## 오류

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

```json
{
  "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`처럼 재시도하면 됩니다. 모든 코드는 [오류](/errors)에 나와 있습니다.

```python
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))
```

> **Note**
>
> **제출** 요청에서 받은 `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` 응답에 알림을 걸어 두세요. 이 상태가 되면 계정의 모든 유료 작업이 한꺼번에 중단됩니다.