非同期タスク
createTask・recordInfo の契約、6 つの状態の意味、コールバックとポーリングの使い分けを説明します。
生成は非同期で、送信と結果取得は別の 2 呼び出しです。
基本の流れ:
- ジョブを送ると、すぐ
taskIdが返ります。まだ完了しておらず、料金相当を残高から確保します(仮押さえ)。 - 後で「この
taskIdは完了したか」と尋ねるのがポーリングです。数秒おきで十分です。またはウェブアドレスを渡し、完了後に結果を送るコールバック(Webhook)を使います。 stateがsucceededなら、リンクはdata.output.assets[0].urlです。約 20 分以内に開くか保存し、期限切れなら再取得します。成功タスクの請求は仮押さえ以下で、失敗・期限切れは通常解放します。ただしモデルページに明記した一部のコンテンツ拒否は課金されます。最終請求はsettledとcostを確認します。
画像は数秒、動画は通常数分かかります。同期エンドポイントではプロセスが接続で待ち続け、通信が乱れると結果も安全な再送方法もなくなり、前回が実行中か分かりません。処理を分けるとタスク ID が基準になり、再接続後も同じ ID を確認できます。
1. タスクを作成する
POST /api/v1/jobs/createTaskこの呼び出しには料金がかかります
受理されたタスクは直ちに estimatedCost を残高から仮押さえし、キャンセルできません。作らずに価格を見るには リクエストを見積もります。キーの支出上限で使用額を制限できます。課金を参照してください。
ヘッダー
| ヘッダー | 必須 | 説明 |
|---|---|---|
Authorization | はい | Bearer sk-spicy-… |
Content-Type | はい | application/json |
Idempotency-Key | いいえ | 強く推奨。通信エラー後の再送を安全にします。冪等性を参照 |
X-Spicy-Retention | いいえ | このタスクの出力とプロンプトの保持を秒単位で短縮。保持と破棄を参照 |
本文
プロパティ
型
callBackUrl はインターネット経由で到達できる公開 HTTPS です。平文の http://、443・80 以外の明示ポート、ユーザー名・パスワード付き URL、プライベート・ループバック IP、名前解決できないホストは拒否します。本文にプロンプトと署名付き結果リンクがあるためです。ローカルの http://localhost は使えず、ポーリングか公開 HTTPS を提供するトンネルを使います。不正な値は HTTP 400、業務 code: 400、msg: "Invalid callback URL"、request_id を返します。URL、ホスト名、IP、DNS 情報は返しません。接続時にも再確認します。Webhookを参照してください。
callBackUrl の B は大文字です
項目ごとに検証するのは input だけです。現在、callback_url や webhookUrl など未知のトップレベル項目は無視するため、タスクは受理されても通知は来ません。通知が届かない場合、先にこの綴りを確認してください。
レスポンス
HTTP/1.1 202 Accepted
Location: /api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3
Retry-After: 2HTTP は 202 Accepted です。本文は標準の共通形式なので、成功の業務 code は 200 のままです。Location は照会先、Retry-After: 2 は初回照会までの推奨待機秒数です。
{
"code": 200,
"msg": "success",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"state": "queued",
"estimatedCost": "0.008",
"deadlineAt": "2026-08-28T09:42:04Z"
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}| フィールド | 説明 |
|---|---|
taskId | job_ で始まるタスク ID。後のすべての処理に必要なので保存する |
state | 新規では queued。同じキーで冪等な再送をすると元タスクの現在の状態 |
estimatedCost | 受理時の仮押さえ額。例:"0.008"(米ドルの文字列)。請求上限でもあり、未使用分を解放し、後で超過徴収しない |
deadlineAt | サーバーが処理を続ける最終時刻。それまでに終わらなければ expired で終了し課金なし。待機時間の判断に使う |
金額は通貨記号なしの文字列です
estimatedCost、cost、残高の 3 フィールドは "0.008" のような小数文字列です。$ や桁区切りはありません。parseFloat / float() を直接使えます。
文字列なのは、JSON 数値が多くの言語で float64 になり、0.008 を正確な二進数で表せないためです。float を経由した価格では、正確に何を請求したか断定できなくなります。会計に使う場合は decimal.Decimal、BigDecimal などの decimal 型で解析してください。
通貨は常に USD です。課金を参照してください。
作成に失敗した場合
最初の連携でよく遭遇するエラーです。createTask がエラーを返した時は資金の仮押さえも請求もありません。
| HTTP | code | 一般的な意味 | 対応 |
|---|---|---|---|
| 400 | 400 | input にスキーマ外の項目、範囲外の値、または不正な callBackUrl | モデルの inputSchema と照合。msg が問題を示す |
| 400 | 40004 | 各項目は有効だが、現在この組み合わせを処理できない | 解像度・時間などの組み合わせを変える |
| 401 | 401 | キーが未指定、誤り、無効 | Authorization を確認 |
| 402 | 40201 | estimatedCost の仮押さえに残高が足りない | コンソールで入金し、同じ Idempotency-Key で再送 |
| 402 | 40202 | キー・チーム・アカウントの支出上限に到達 | 上限を引き上げるか、日次・月次のリセットを待つ |
| 403 | 40301 | このキーはモデルを呼べない | 別キーを使うか、キーのモデル制限を変える |
| 409 | 409 / 40901 | 冪等性の競合、または見積もり不一致 | 冪等性と 見積もりを参照 |
| 413 | 413 | 本文が大きすぎる。通常はインラインメディアが原因 | アップロードして spicy:// URI を渡す。メディアを参照 |
| 429 | 429 | 短時間のリクエスト過多 | Retry-After を待ち同じキーで再送 |
| 503 | 50301 | モデルが一時的に利用できない | 後で同じキーで再試行 |
全一覧は エラーです。
事前準備:モデルと有効な入力を選ぶ
以下の例には実在のモデル ID と対応する input が必要です。カタログの includeExamples=1 は各モデルのスキーマを通過済みの入力を含め、両方を取得できます。小さな JSON 用 CLI jq が必要です。他の種類は image を video、audio に変えてください。
curl -s "https://api.spicyapi.ai/api/v1/models?modality=image&includeExamples=1" \
-H "Authorization: Bearer $SPICY_API_KEY" \
| jq -c '.data.items[] | select(.enabled and .available) | {model, input: .examples[0].input}'各行は model と input の 2 項目を持つ完成済み本文です。1 行を選びます。"input": null なら保存例がないため、GET /api/v1/models/{model} の inputSchema を読み、自分で入力を書きます。モデルカタログとスキーマを参照してください。
例
以下の MODEL_ID_FROM_CATALOG は仮の値です。本文全体をライブカタログで選んだ行に置き換え、そのまま送らないでください。
# Paste one line printed by the command above between the single quotes.
TASK='{"model":"MODEL_ID_FROM_CATALOG","input":{"prompt":"a folded paper lantern, hard side light"}}'
# One new key per new task. After a network error, rerun only the curl command
# so the same key is sent again and no second task is created.
IDEMPOTENCY_KEY="$(uuidgen)"
curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
--data "$TASK"上の 202 本文と code: 200、taskId が返ります。結果取得に必要な taskId を保存してください。コールバックには自分の HTTPS アドレスで "callBackUrl":"https://…" を JSON に加えます。
const res = await fetch('https://api.spicyapi.ai/api/v1/jobs/createTask', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SPICY_API_KEY}`,
'Content-Type': 'application/json',
// One key per logical request; reuse it across retries
'Idempotency-Key': 'order-8814-render-1',
},
body: JSON.stringify({
model: 'MODEL_ID_FROM_CATALOG',
input: { prompt: 'a folded paper lantern, hard side light' },
// Your own public HTTPS endpoint; delete this line to poll instead
callBackUrl: 'https://your-app.example.com/hooks/spicy',
}),
});
const body = await res.json();
if (res.status !== 202 || body.code !== 200) throw new Error(`${res.status} ${body.code} ${body.msg}`);
console.log(body.data.taskId, body.data.deadlineAt);import os, requests
res = requests.post(
"https://api.spicyapi.ai/api/v1/jobs/createTask",
headers={
"Authorization": f"Bearer {os.environ['SPICY_API_KEY']}",
"Content-Type": "application/json",
# One key per logical request; reuse it across retries
"Idempotency-Key": "order-8814-render-1",
},
json={
"model": "MODEL_ID_FROM_CATALOG",
"input": {"prompt": "a folded paper lantern, hard side light"},
# Your own public HTTPS endpoint; delete this line to poll instead
"callBackUrl": "https://your-app.example.com/hooks/spicy",
},
)
body = res.json()
if res.status_code != 202 or body["code"] != 200:
raise RuntimeError(f'{res.status_code} {body["code"]} {body["msg"]}')
print(body["data"]["taskId"], body["data"]["deadlineAt"])202 が常に queued とは限りません
最初の受理は queued を作ります。同じ API キー、同じリクエスト、同じ Idempotency-Key で再送すると、202 は元タスクの現在の状態を返します。running や最終状態の場合もありますが、別タスク・別の仮押さえは作りません。
2. 状態
queuedrunningsucceededfailedexpired受理したタスクは取り消せません。queued も running もキャンセル操作がありません。過去の canceled は読めますが、現在のユーザー・運用者の操作でこの状態を作ることはありません。
| 状態 | 意味 | 最終状態 | 課金 |
|---|---|---|---|
queued | 作成・資金確保済み、実行待ち | いいえ | 仮押さえ、未精算 |
running | モデルで生成中、結果待ち | いいえ | 仮押さえ、未精算 |
succeeded | 生成成功 | はい | 実使用量で精算し、仮押さえを上限とする |
failed | 生成失敗 | はい | 通常全額解放。モデルページに明示した一部の content_rejected は受理時の見積もり額を課金。settled と cost を確認 |
canceled | 過去の互換記録のみ。キャンセル操作はない | はい | 全額解放 |
expired | モデルの最大実行時間を超過 | はい | 全額解放 |
状態だけでなく精算を確認してください
失敗・期限切れは通常全額解放します。一部のコンテンツ拒否はモデルページで例外を明示する場合だけ受理時の見積もり額を課金します。最終額は settled と cost を確認してください。課金される拒否も state: "failed"、errorCode: "content_rejected" のままです。未完了の精算・解放は復旧処理が自動で完了します。
最終状態の判定には値を列挙せず、state が {queued, running} に含まれないことを使います。過去の canceled も安全に扱え、将来の最終状態でポーリングが終わらなくなるのを防げます。
2.5 wait でポーリングを省く
createTask に ?wait=<seconds> を付けると、最終状態まで接続を待機するため、最初の呼び出しでポーリングループを省けます。
curl -X POST "https://api.spicyapi.ai/api/v1/jobs/createTask?wait=30" \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"MODEL_ID_FROM_CATALOG","input":{"prompt":"a lighthouse at dusk"}}'| 結果 | レスポンス |
|---|---|
| 指定時間内に最終状態 | recordInfo と同じタスク記録で 200 |
| 待機時間を使い切る | wait なしと同じ通常の 202。そこからポーリング |
| 切断する | タスクは変わらず、実行・課金を通常どおり継続 |
最終状態は必ずしも成功ではありません
ここでの 200 は終了を示し、成功ではありません。data.state を確認してください。failed と expired も recordInfo と同じく同じステータスで返ります。
上限は 60 秒です。 大きい値は拒否せず上限に収め、正の整数でない値は拒否せず無視します。最適化用の項目の誤記で、本来成功する生成を失敗させない設計です。
API の手前のエッジは 100 秒でタイムアウトします。それを超えると CDN が切断し、タスクと仮押さえは存在するのに、タスク ID も復旧用状態もない 524 が返り得ます。60 秒上限はリクエスト自体の時間を残すためです。
画像向けに使ってください。動画には適しません
動画は通常数分かかるため、wait が時間切れになり、接続を保持した分だけ待ち時間が増え、結局ポーリングになります。数秒で終わる画像・編集エンドポイントには有効ですが、それ以外では無駄です。意図的にデフォルト設定にはできません。
本番は Webhookが適しています。接続を保たず、どの待機時間より長いタスクも終了時に分かります。
3. タスクを照会する
GET /api/v1/jobs/recordInfo?taskId=job_…必須クエリーは taskId の 1 つです。作成したのと同じ API キーでのみ読めます。同じアカウントの別キーやローテーション後の代替キーでも 404 になります。存在しないタスクと同じ応答にし、他人の ID を探れないようにします。download-url と retry も同じ規則です。ローテーション前に古いキーのタスク結果を回収してください。
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3" \
-H "Authorization: Bearer $SPICY_API_KEY"照会は無料です。例の ID を自分の taskId に置き換えます。
レスポンス項目
| フィールド | 型 | 説明 |
|---|---|---|
taskId | string | タスク ID |
sourceTaskId | string | 再試行で作成したタスクだけ。元タスクの ID |
model | string | モデル識別子 |
state | string | 上の表を参照 |
input | object | 検証済みパラメーター。インライン画像はプラットフォームの URI になる。入力保持期限後は省略 |
output | object | 生成ファイルの説明。意味があるのは succeeded の時だけ |
errorCode | string | state が failed・expired の時(期限切れは timeout)。9 値のいずれか。失敗コードを参照。errorMessage ではなくこれで分岐 |
errorMessage | string | 人向けの失敗理由。別言語の選択がなければ英語。失敗時のみ |
cost | string | 精算済み額、未精算なら仮押さえ額。最終請求はそれを超えない |
settled | boolean | 課金が確定したか。false の間は cost がまだ変わり得る |
createdAt | string | RFC 3339 |
deadlineAt | string | 受理時に保存した実行期限。古い Webhook イベントでは省略の場合あり |
completedAt | string | RFC 3339。最終状態のみ |
contentState | string | present、保持期限で削除した expired、自分で破棄した purged。保持と破棄を参照 |
contentRemovedBy | string | user または system。内容が残っている間は省略 |
purgedAt | string | RFC 3339。内容を破棄した後だけ |
retention | object | 出力削除時刻 outputsExpireAt、プロンプト消去時刻 promptsExpireAt、決定した設定 source |
異なる 4 つの期限
- 自分の待機時間は自分で決めます。待機を止めてもリモートのタスクはキャンセルしません。
taskIdを保存し、後で同じタスクを照会して再開します。 deadlineAtはサーバーの実行期限です。冪等な再送では延びません。明示的な再試行は新タスクと新期限を作ります。ローカルのタイマーから返金を推測せず、最終状態とsettledを読みます。output.assets[].expiresAtは一時リンクの期限で、通常 20 分です。再取得すると新タスクを作らずリンクを更新します。- 生成メディアは完了後最大約 14 日、アップロード入力は最大 1 日です。これらは上限で、保持と破棄で短縮したり、個別タスクを破棄したりできます。リンク更新ではメディア自体の保持は延びません。
JavaScript SDK の client.run({ model, input }, { idempotencyKey, onAccepted, timeoutMs }) は 1 回送信し、2 秒から最大 10 秒まで間隔を延ばして照会します。timeoutMs はローカルの待機だけなので、onAccepted の ID を保存してください。SDKを参照してください。バックグラウンド処理は Webhook を推奨します。完全な結果コールバックを検証した後は、追加照会せず結果を直接使えます。
結果 URL を直接使ってください
準備済みアセットには key、メタデータとともに url、expiresAt があります。認証情報なしで output.assets[].url を直接取得できます。例:curl -L -o result.png "<url>"。期限切れなら再照会します。既存連携の /common/download-urlも任意で使えます。URL は通常 20 分有効で、結果の 14 日保持を延ばすことはできません。
{
"code": 200,
"msg": "success",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"model": "MODEL_ID_FROM_CATALOG",
"state": "succeeded",
"input": { "prompt": "a folded paper lantern, hard side light" },
"output": {
"assets": [
{
"key": "tasks/2026/08/28/job_01k3m8x9q2z4v7n5p6r8s0t1w3/9f3c1d0a7b4e2f68c5a1d3e9b0472fa1.png",
"url": "https://example.r2.cloudflarestorage.com/results/result.png?X-Amz-Signature=SIGNATURE_FROM_RESPONSE",
"expiresAt": "2026-08-28T09:32:11Z",
"mime": "image/png",
"width": 1024,
"height": 1024,
"bytes": 1483920
}
]
},
"cost": "0.008",
"settled": true,
"createdAt": "2026-08-28T09:12:04Z",
"deadlineAt": "2026-08-28T09:42:04Z",
"completedAt": "2026-08-28T09:12:11Z",
"contentState": "present"
},
"request_id": "req_01k3m8xa1b2c3d4e5f6g7h8j9k0m"
}{
"code": 200,
"msg": "success",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"model": "MODEL_ID_FROM_CATALOG",
"state": "failed",
"errorCode": "upstream_failed",
"errorMessage": "Generation failed; the charge has been refunded",
"cost": "0",
"settled": true,
"createdAt": "2026-08-28T09:12:04Z",
"deadlineAt": "2026-08-28T09:42:04Z",
"completedAt": "2026-08-28T09:12:39Z",
"contentState": "present"
},
"request_id": "req_01k3m8xb2c3d4e5f6g7h8j9k0m1n"
}照会の成功とタスクの成功は別です
どちらも code: 200 で、照会が成功したことを示します。タスクの結果は data.state です。業務コードは配信、state はタスクを表します。混同しないでください。
Webhook も同じ構造で、code: 200 と同じ data 項目を使います。この節のパーサーでコールバックもそのまま扱えます。入口は 2 つ、パーサーは 1 つです。
出力の形式
output には独立した 2 部分があります。画像・動画・音声ファイルは output.assets[]、テキスト結果は文字列の output.text に入ります。
ほとんどのモデルはどちらか 1 つで、両方の場合もあります。どちらかは modality ではなくエンドポイントの特性で決まります。例えば speech-to-text は音声を読むためカタログで audio に分類しますが、回答はテキストです。
`output.assets` が常にあるとは限りません
結果がテキストなら**assets キー自体がありません**。空配列ではありません。task.output.assets[0].url で開くコードは、確認できる値を返すのではなく例外になります。output.text があれば先に読み、assets は任意として扱います。
const { text, assets = [] } = task.output ?? {};
if (text) console.log(text);
for (const asset of assets) console.log(asset.url);テキストにはダウンロードも期限切れのリンクもありませんが、タスク出力の一部なので、ほかの結果と同じ予定で削除し、contentState も expired になります。必要な内容はコピーしてください。文字起こしの区間など複数部分の結果は、改行で区切った 1 文字列にまとめます。
以下は assets[] の説明です。
| フィールド | 型 | 説明 |
|---|---|---|
key | string | 任意のリンク更新に使う互換識別子。pending 中は空 |
url | string | 署名付きの直接 GET URL。準備中・利用不可なら省略。キーを付けない |
expiresAt | string | URL の RFC 3339 期限。保持中なら再照会で更新 |
mime | string | コンテンツタイプ。例:image/png、video/mp4 |
width · height | integer | ピクセル寸法。音声・テキストにはない |
durationSeconds | number | 秒単位の長さ。動画・音声のみ |
bytes | integer | バイトサイズ |
pending | boolean | 以下を参照 |
unavailable | boolean | 以下を参照 |
layer | object | レイヤー分解結果のみ。整数の zIndex、任意の boundingBox([x1, y1, x2, y2])、任意の name。以下を参照 |
情報のない項目は省略し、null や 0 にはしません。任意として解析してください。
次の 2 フラグを扱います。
pending: true:ストレージへコピー中で、まだkeyとurlは空です。数秒後に再取得してください。今keyなしでdownload-urlを呼ぶと409になります。unavailable: true:期限切れ、または転送失敗で永続的に利用できません。待っても戻らないため再生成します。成功タスクの全ファイルを保存できなかった場合、そのタスクには課金しません。
レイヤー分解結果には layer もあります。最初は分離した要素を取り除いた基底画像(zIndex 0)、残りは透明 PNG レイヤーです。zIndex の昇順で重ねると復元できます。boundingBox は基底画像のピクセル座標 [x1, y1, x2, y2] で、レイヤーをその枠に合わせて拡縮すると元の位置に戻ります。位置を返さないモデルでは省略しますが、切り抜き単体として利用できます。name はモデル自身が付けたラベルがある場合だけ返します。課金枚数は基底画像を含むすべての返却画像です。
4. コールバックを優先し、ポーリングは代替手段に
ポーリングは完了まで recordInfo を繰り返し呼ぶ方法、コールバックは完了タスクをサーバーへ一度送る方法です。得られるデータは同じです。
| コールバック | ポーリング | |
|---|---|---|
| 遅延 | 完了した瞬間 | 照会間隔の分 |
| リクエスト | タスクごとに SpicyAPI が 1 回 | タスクごとに自分で N 回 |
| 自分のレート枠を消費 | いいえ | はい。レート制限を参照 |
| 公開 HTTPS が必要 | はい | いいえ |
本番ではコールバックを使ってください。 3 分の動画を毎秒照会すると、新情報がほぼない 180 リクエストになります。現在の 10 秒あたり 1,000 回の公開 API 保護制限は通常の有料生成を制約しませんが、過密なポーリングは無駄な通信を生み、暴走ループは自分の読み取り枠を消費して recordInfo が 429 になります。
ポーリングが適するのは、ローカル開発と公開の受信先がないサービスの 2 場面です。
ポーリング間隔を延ばす
recordInfo は読み取り枠、createTask は書き込み枠を使うため、照会で送信枠が不足することはありません。ただし頻繁すぎると recordInfo 自体が 429 を返し、枠が回復するまでタスクの状況が見えなくなります。
推奨する流れ:
- 2~3 秒から開始します。 最速の画像モデルでも数秒かかるため、それ未満は無駄です。
- 1.5 倍ずつ延ばし、10~15 秒を上限にします。
- 固定の分数ではなくタスク自身の期限まで待ちます。動画モデルには最大 1 時間かかるものがあり、一律 10 分で諦めると正常に実行中のタスクを見失います。
createTaskのdeadlineAtに、結果記録のための数分を加えて使います。
待機をやめてもタスクの失敗ではありません
待機終了は待つのをやめただけです。タスクは実行を続け、成功して課金される場合があります。新しい Idempotency-Key で再送すると 2 件目を作って支払うため、再送しないでください。後で同じ taskId を照会するか、履歴で探します。
// created is the data object returned by createTask.
async function waitForTask(created) {
// Stop a few minutes after the server's own deadline.
const giveUpAt = Date.parse(created.deadlineAt) + 5 * 60 * 1000;
let wait = 2000;
while (Date.now() < giveUpAt) {
await new Promise((r) => setTimeout(r, wait));
wait = Math.min(wait * 1.5, 15000);
const res = await fetch(
`https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${encodeURIComponent(created.taskId)}`,
{ headers: { Authorization: `Bearer ${process.env.SPICY_API_KEY}` } },
);
const body = await res.json();
// Being rate-limited is not a task failure — honour Retry-After
if (body.code === 429) {
wait = Number(res.headers.get('retry-after') ?? 1) * 1000;
continue;
}
if (body.code !== 200) throw new Error(`${body.code} ${body.msg}`);
// Terminal means "not one of the two in-flight states"
const { state } = body.data;
if (state !== 'queued' && state !== 'running') return body.data;
}
// Not a failure: query this taskId again later instead of resubmitting.
throw new Error(`stopped waiting for ${created.taskId}`);
}
// created = body.data from the createTask example above
const task = await waitForTask(created);
if (task.state === 'succeeded') console.log(task.output.assets[0].url);
else console.log(task.state, task.errorCode);import os, time, requests
from datetime import datetime
BASE = "https://api.spicyapi.ai/api/v1"
AUTH = {"Authorization": f"Bearer {os.environ['SPICY_API_KEY']}"}
def wait_for_task(created: dict) -> dict:
"""created is the data object returned by createTask."""
# Stop a few minutes after the server's own deadline.
deadline = datetime.fromisoformat(created["deadlineAt"].replace("Z", "+00:00"))
give_up_at = deadline.timestamp() + 5 * 60
wait = 2.0
while time.time() < give_up_at:
time.sleep(wait)
wait = min(wait * 1.5, 15.0)
res = requests.get(f"{BASE}/jobs/recordInfo", headers=AUTH, params={"taskId": created["taskId"]})
body = res.json()
# Being rate-limited is not a task failure — honour Retry-After
if body["code"] == 429:
wait = float(res.headers.get("retry-after", 1))
continue
if body["code"] != 200:
raise RuntimeError(f'{body["code"]} {body["msg"]}')
# Terminal means "not one of the two in-flight states"
if body["data"]["state"] not in ("queued", "running"):
return body["data"]
# Not a failure: query this taskId again later instead of resubmitting.
raise TimeoutError(f'stopped waiting for {created["taskId"]}')
# created = body["data"] from the createTask example above
task = wait_for_task(created)
if task["state"] == "succeeded":
print(task["output"]["assets"][0]["url"])
else:
print(task["state"], task.get("errorCode"))5. 残高を確認する
GET /api/v1/chat/credit は新タスクに使える available、実行中の確保額 held、total を返します。承認済み与信使用中の available は負になることがあり、funding.grants の資金は対象モデルが限定される場合があります。詳細は 残高の確認です。
curl https://api.spicyapi.ai/api/v1/chat/credit \
-H "Authorization: Bearer $SPICY_API_KEY"タスク履歴と復旧
GET /api/v1/jobs は現在の API キーが作った表示中のタスクを一覧にします。アプリ再起動や通知欠落後の作業を探せます。別キーのタスク・非表示記録は除外します。レスポンスには ID、モデル、状態、費用、精算状態、日時、記録されていれば作成元の requestId(テキスト呼び出しでは X-Request-Id と同じ値)を含みます。プロンプト、入力、出力、メディアリンクは含まれず、選んだタスクを recordInfo で取得してください。
state、正確なカタログ model または別名、UTC の from / to(YYYY-MM-DD、開始を含み終了を含まない)で絞れます。デフォルトは UTC の明日までの 7 日間、最大 92 日です。limit はデフォルト 20、最大 100 です。
使わないフィルターは省略してください。model= のような空値、重複パラメーター、未知のパラメーターは 400 です。5 秒を超える照会は 503 を返すため、日付範囲を短くして再試行します。
curl --fail-with-body \
'https://api.spicyapi.ai/api/v1/jobs?from=2026-09-01&to=2026-09-07&state=failed&limit=20' \
-H "Authorization: Bearer $SPICY_API_KEY"data.items を読みます。data.hasMore が true なら、すべてのフィルターと明示した日付を変えず、次の cursor に data.nextCursor を渡します。createdAt の降順、次にタスク ID 順です。ページは実際の状態を反映し、固定スナップショットではありません。カーソルを自分で組み立てないでください。履歴一覧でタスクを作成・再試行することはありません。
SDK の client.listTasks(...)、CLI の spicyapi tasks list、読み取り専用 MCP ツール spicyapi_tasks_list でも使えます。USD の cost は小数文字列として保持します。settled が false ならまだ仮押さえ見積もりで、最終請求ではありません。
作成レスポンスを失った場合、まず元の Idempotency-Key で変更していないリクエストを再送します。空の履歴ページは、同じ有料処理を新しいキーで作り直す許可ではありません。

