# 非同期タスク

画像・動画タスクの送信、ポーリングまたはプッシュでの受け取り、結果のダウンロード、各ステータスとフィールドの意味、返金の仕組み。

> https://nezhagate.com/ja/docs/guide/async-jobs

## 非同期で動くエンドポイント

画像と動画の生成は非同期タスクとして動きます。呼び出すとすぐにタスク ID が返り、生成はバックグラウンドで進みます。対話は同期処理で、タスクは使いません。

| メソッド | パス | 説明 |
| --- | --- | --- |
| POST | `/v1/images/generations` | テキストから画像。すぐにタスク ID を返します（HTTP 202）。 |
| POST | `/v1/images/edits` | 画像から画像。上のエンドポイントに `image` を渡しても同じです。 |
| GET | `/v1/images/jobs/{id}` | 画像タスクの状態と結果を取得します。 |
| POST | `/v1/videos/generations` | 動画を生成。すぐにタスク ID を返します（HTTP 202）。 |
| GET | `/v1/videos/jobs/{id}` | 動画タスクの状態と結果を取得します。 |

## 処理の流れ

1. 送信：生成エンドポイントを呼び出すと、すぐに HTTP 202 とタスク ID の `id` が返ります。`img_...` のような形式で、動画タスクも同じ接頭辞を使います。

2. 待機：そのタスク ID で照会エンドポイントをポーリングするか、送信時に `callback_url` を付けて結果をプッシュで受け取ります。

3. 完了：`status` が `succeeded` になり、結果のリンクが `data[0].url` に入ります。`failed` になった場合は理由が `error` に入り、このタスクのために確保したクレジットは残高に戻っています。

4. ダウンロード：結果のリンクは 60 日間保存されます。残しておきたいものは、ご自身のストレージにコピーしてください。

```
curl https://nezhagate.com/v1/videos/generations \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "seedance-2.0", "prompt": "a paper boat drifting down a rainy street", "duration": 5, "size": "16:9"}'

# HTTP 202
{"id": "img_9c1e...7a", "object": "video.generation.job", "status": "queued", "created": 1791199400, "model": "seedance-2.0"}
```

```
curl https://nezhagate.com/v1/videos/jobs/img_9c1e...7a -H "Authorization: Bearer YOUR_API_KEY"
```

## タスクのステータス

| ステータス | 意味 |
| --- | --- |
| `queued` | 順番待ち。`queue_position`（前にあるタスク数）と `eta_seconds`（予想待ち時間）が付きます。 |
| `processing` | 生成中。 |
| `succeeded` | 完了。結果は `data[0].url`、課金の内訳は `usage` にあります。 |
| `failed` | 失敗。理由は `error` にあり、予約したクレジットは全額返金されます。 |

`succeeded` と `failed` は最終状態で、その後タスクが変化することはありません。

## レスポンスのフィールド

| フィールド | 意味 |
| --- | --- |
| `id` | タスク ID。照会と Webhook で使います。 |
| `object` | 画像タスクは `image.generation.job`、動画タスクは `video.generation.job`。 |
| `status` | `queued` / `processing` / `succeeded` / `failed`。 |
| `created` | 送信日時（Unix 秒）。 |
| `model` | 送信時に指定したモデル ID。 |
| `queue_position` | `queued` のときのみ：前に並んでいるタスクの数。 |
| `eta_seconds` | `queued` のときのみ：開始までのおおよその秒数。 |
| `data[].url` | `succeeded` のときのみ：結果ファイル（画像または mp4）のリンク。60 日間保存されます。 |
| `usage` | `succeeded` のときのみ：課金の内訳。画像は `images`（実際に納品した枚数）と `resolution`、動画は `clips` を返します。 |
| `error` | `failed` のときのみ：`message`、`type`、`code`、`param`。原因は `code` で判定してください。 |

```
{
  "id": "img_9c1e...7a",
  "object": "video.generation.job",
  "status": "succeeded",
  "created": 1791199400,
  "model": "seedance-2.0",
  "data": [{"url": "https://img.nezhagate.com/i/5d41402abc....mp4"}],
  "usage": {"clips": 1, "model": "seedance-2.0"}
}
```

```
{
  "id": "img_9c1e...7a",
  "object": "video.generation.job",
  "status": "failed",
  "created": 1791199400,
  "model": "seedance-2.0",
  "error": {"message": "...", "type": "invalid_request_error", "code": "content_policy", "param": null}
}
```

## ポーリングのポイント

- 画像は 2〜3 秒ごと、動画は 5〜10 秒ごとにポーリングしてください。ポーリングは無料です。

- `succeeded` か `failed` になったら、すぐにポーリングを止めてください。

- プログラムには全体の待ち時間の上限を設けてください。画像はふつう数十秒から数分、動画は 1〜2 分から 1 時間以上までさまざまです（長い動画や順番待ちがあるとさらに長くかかります）。目安の時間は各モデルのドキュメントに載っています。上限を過ぎても再送信はしないでください。タスクは残っているので、しばらくしてから照会を続けてください。

- タスクは送信したアカウントに属します。そのアカウントのキーならどれでも照会でき、ほかのアカウントからは 404 `job_not_found` が返ります。タスクの記録は 3 日間保存され、それを過ぎるとそのタスク ID でも 404 が返りますが、結果のリンクは 60 日間の保存期間中は引き続き使えます。

- 送信がクライアント側でタイムアウトしてタスク ID を受け取れなかった場合は、再送信する前にコンソールの「コールログ」でタスクが作成されたかを確認してください。二重に課金されるのを防げます。

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

送信時に `callback_url` を付けると、タスクが終わった時点で（成功・失敗とも）、照会エンドポイントが返すものとまったく同じ内容をゲートウェイがその URL へ送ります。署名付きで、届かなかった場合は自動で再試行します。コンソールの「設定 → Webhook」で、アカウント全体の送信先を設定することもできます。

```
{"model": "seedance-2.0", "prompt": "...", "duration": 5, "callback_url": "https://example.com/webhooks/nezhagate"}
```

[Webhook のドキュメント →](https://nezhagate.com/ja/docs/webhooks)

## 課金

- 送信時に、タスクの料金分を残高から確保します。残高が足りなければ 402 が返り、タスクは作成されません。

- 成功時は実際に納品した分で精算します。画像は納品した枚数、動画は注文した長さまたは本数（確保額と同じ）で、それを超えて確保した分は自動で戻ります。失敗時は確保額を全額返します。

- ポーリングと Webhook の配信は無料です。

## 結果の保存期間

`data[0].url` は当サイトのストレージ上のリンクで、60 日後に自動で削除されます。結果を使い続けるには、ご自身のストレージや CDN にダウンロードしてください。

## タスクが失敗する理由

失敗したタスクは必ず全額返金されます。失敗したタスクでよく見られるエラーコードは次のとおりです：

| code | 発生する場面 | 対処 |
| --- | --- | --- |
| `content_policy` | 動画：プロンプト、参照素材、または生成された映像や音声がコンテンツ審査を通りませんでした（音声が引っかかった場合は `param` が `audio` になります）。 | プロンプトを書き直すか参照素材を差し替えてから、もう一度送信してください。同じ内容のままでは再び拒否されます。 |
| `moderation_blocked` | 画像：モデル提供元の安全審査がプロンプトまたは参照画像を拒否しました（コードが `content_policy_violation` の場合もあります）。 | プロンプトを書き直すか、参照画像を差し替えてください。そのまま再送信しても、たいてい再び拒否されます。 |
| `render_failed` | 動画：今回は動画が生成されませんでした。多くは一時的なものです。 | 同じリクエストをそのまま再送信すれば、たいてい成功します。 |
| `invalid_material` | 動画：上流が入力素材に問題があると判断しました。大まかな分類のエラーで、一時的なことも多くあります。 | まずはそのまま再送信してください。繰り返し起きる場合は、参照素材とプロンプトの長さを確認してください。 |
| `render_timeout` | 動画：順番待ちまたはレンダリングに時間がかかりすぎ、上流がこのタスクを打ち切りました。 | もう一度送信してください。 |
| `wait_timeout` | 動画：この回線で許される最長の待ち時間を過ぎても、動画が届きませんでした。 | もう一度送信してください。 |
| `upstream_unstable` | 動画：レンダリング中に接続が切れました。 | もう一度送信してください。 |
| `result_fetch_failed` | 動画は生成されましたが、当サイト側でのダウンロードまたは保存が繰り返し失敗しました。 | もう一度送信してください。 |
| `invalid_request` | 動画：この回線はこのパラメータの組み合わせ（長さ、比率、解像度など）を受け付けません。 | `message` の内容に沿ってパラメータを調整し、もう一度送信してください。 |
| `upstream_error` | その他の失敗：すべての回線を試しても失敗した、順番待ちが 15 分を過ぎても始まらなかった、上流が理由を示さずに拒否した、などです。具体的な状況は `message` に書かれています。 | たいていは再送信すれば成功します。 |

[エラーコードの完全な一覧 →](https://nezhagate.com/ja/docs/guide/errors)

## 完全なサンプル

送信、ポーリング、ダウンロードまでを一度に行うサンプルで、全体の待ち時間の上限と失敗時の処理も含みます。YOUR_API_KEY を置き換えて実行してください。

```
import time
import requests

BASE = "https://nezhagate.com/v1"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}

def run_job(kind, payload, every=5, give_up=5400):
    """kind is "images" or "videos". Returns the finished job (succeeded or failed)."""
    job = requests.post(f"{BASE}/{kind}/generations", headers=HEADERS, json=payload, timeout=120)
    job.raise_for_status()                       # 4xx here: fix the request; nothing was billed
    job_id = job.json()["id"]
    deadline = time.time() + give_up
    while time.time() < deadline:
        time.sleep(every)
        r = requests.get(f"{BASE}/{kind}/jobs/{job_id}", headers=HEADERS, timeout=30).json()
        if r["status"] in ("succeeded", "failed"):
            return r
    raise TimeoutError(f"{job_id} still running; poll it again later, it is not lost")

job = run_job("videos", {"model": "seedance-2.0", "prompt": "a paper boat drifting down a rainy street",
                         "duration": 5, "size": "16:9"})
if job["status"] == "succeeded":
    url = job["data"][0]["url"]
    with open("clip.mp4", "wb") as f:            # keep your own copy: links expire after 60 days
        f.write(requests.get(url, timeout=300).content)
else:
    print(job["error"]["code"], job["error"]["message"])
```
