> 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.

# 開発ガイド

> 同期テキスト、非同期タスク、ポーリング、Webhook、再試行、エラー処理をアプリケーションに組み込みます。

# 開発ガイド

[クイックガイド](/quick-guide)では最初のリクエストを動かします。このページでは、本番で継続して動作させるために必要な対応を説明します。

## 2 種類の呼び出し

BeatAPI のリクエストには 2 つのライフサイクルがあります。連携時の多くの問題は、両者を混同することで起こります。

|         | テキスト               | ワークフロー、画像、動画、エフェクト |
| ------- | ------------------ | ------------------ |
| 返されるもの  | 回答                 | タスク ID             |
| 待ち時間    | 数秒                 | 数秒から数分             |
| ストリーミング | Server-Sent Events | 対象外                |
| 完了の確認   | HTTP レスポンス         | ポーリング、または Webhook  |
| 課金      | レスポンス時             | 精算時。失敗時は返金         |

テキストは同期です。`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}` をポーリングするか、Webhook を登録します。
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

        # ジッターで複数ワーカーの確認タイミングを分散します。
        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")
```

このループでは次の 3 点が重要です。

* **終了状態で停止します。** 経過時間から完了を推測しません。完了したタスクは以後変化しません。
* **ジッター付きのバックオフを使います。** タスクエンドポイントの上限はキーごとに毎分 120 リクエストです。複数のタスクを 1 秒ごとに確認すると、この上限を使い切る場合があります。
* **`429` は失敗ではなく待機として扱います。** `Retry-After` に従ってください。

> **Note**
>
> Music Video の `storyboard_ready` と `requires_action` は、ポーリングではなく判断や操作が必要な未完了状態です。処理中として扱うと永遠にポーリングしてしまいます。全状態と必要な操作は[クイックガイド](/quick-guide)を参照してください。

### ポーリングの代わりに Webhook を使う

`POST /v1/webhooks` でエンドポイントを登録すると、BeatAPI が終了状態を通知します。到達可能な HTTPS エンドポイントが必要ですが、ポーリングループを省けます。

JSON を解析する前に、**生のリクエストバイト列**で署名を検証してください。本文を再シリアライズすると署名が一致しません。[Webhooks](/webhooks) を参照してください。

Webhook を設定しても、低頻度の照合用ポーリングは残してください。通知の配信に失敗する場合があります。モデルの通常の所要時間を大きく超えて `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 を保存します。
    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 は別のタスクを作らず、元のタスクを返します。

## レート制限

独立した 2 つの制限があり、対処方法が異なります。

* **リクエスト数**はキーごとの毎分の上限で、`Retry-After` を伴う `429` を返します。チャージ累計額に応じて増えます。現在の値と次の段階はダッシュボードと `GET /api/user/self` で確認できます。
* **同時実行数**は処理中のタスク数の上限で、`user_concurrency_exceeded` を返します。実行中のタスクの完了を待つ必要があります。即座の再試行はリクエスト数を消費するだけです。

`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 は所定のコンテキスト長を超えると単価が上がります。2 つの DeepSeek HC（高キャッシュ）モデルは平日の北京時間 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` を保存します。
* ポーリングにジッターを付け、終了状態や操作待ち状態で止めます。
* 生のバイト列で Webhook の署名を検証します。
* 仮確保、精算、返金、エラー、最終出力 URL を記録します。
* アカウントの有料操作が一斉に止まる `402` を監視します。