spicyapiドキュメント
API で開発する
本文

非同期タスク

createTask・recordInfo の契約、6 つの状態の意味、コールバックとポーリングの使い分けを説明します。

生成は非同期で、送信と結果取得は別の 2 呼び出しです。

基本の流れ:

  1. ジョブを送ると、すぐ taskId が返ります。まだ完了しておらず、料金相当を残高から確保します(仮押さえ)。
  2. 後で「この taskId は完了したか」と尋ねるのがポーリングです。数秒おきで十分です。またはウェブアドレスを渡し、完了後に結果を送るコールバック(Webhook)を使います。
  3. 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: 2

HTTP は 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"
}
フィールド説明
taskIdjob_ で始まるタスク 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 がエラーを返した時は資金の仮押さえも請求もありません。

HTTPcode一般的な意味対応
400400input にスキーマ外の項目、範囲外の値、または不正な callBackUrlモデルの inputSchema と照合。msg が問題を示す
40040004各項目は有効だが、現在この組み合わせを処理できない解像度・時間などの組み合わせを変える
401401キーが未指定、誤り、無効Authorization を確認
40240201estimatedCost の仮押さえに残高が足りないコンソールで入金し、同じ Idempotency-Key で再送
40240202キー・チーム・アカウントの支出上限に到達上限を引き上げるか、日次・月次のリセットを待つ
40340301このキーはモデルを呼べない別キーを使うか、キーのモデル制限を変える
409409 / 40901冪等性の競合、または見積もり不一致冪等性と 見積もりを参照
413413本文が大きすぎる。通常はインラインメディアが原因アップロードして spicy:// URI を渡す。メディアを参照
429429短時間のリクエスト過多Retry-After を待ち同じキーで再送
50350301モデルが一時的に利用できない後で同じキーで再試行

全一覧は エラーです。

事前準備:モデルと有効な入力を選ぶ

以下の例には実在のモデル 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 は仮の値です。本文全体をライブカタログで選んだ行に置き換え、そのまま送らないでください。

curl
# 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 に加えます。

JavaScript
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);
Python
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. 状態

queued
running
succeededfailedexpired
タスクの状態は常に 1 つだけです。課金されるのは succeeded のときだけで、それ以外の終了状態では確保した金額が全額戻ります。

受理したタスクは取り消せません。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
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
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3" \
  -H "Authorization: Bearer $SPICY_API_KEY"

照会は無料です。例の ID を自分の taskId に置き換えます。

レスポンス項目

フィールド型説明
taskIdstringタスク ID
sourceTaskIdstring再試行で作成したタスクだけ。元タスクの ID
modelstringモデル識別子
statestring上の表を参照
inputobject検証済みパラメーター。インライン画像はプラットフォームの URI になる。入力保持期限後は省略
outputobject生成ファイルの説明。意味があるのは succeeded の時だけ
errorCodestringstate が failed・expired の時(期限切れは timeout)。9 値のいずれか。失敗コードを参照。errorMessage ではなくこれで分岐
errorMessagestring人向けの失敗理由。別言語の選択がなければ英語。失敗時のみ
coststring精算済み額、未精算なら仮押さえ額。最終請求はそれを超えない
settledboolean課金が確定したか。false の間は cost がまだ変わり得る
createdAtstringRFC 3339
deadlineAtstring受理時に保存した実行期限。古い Webhook イベントでは省略の場合あり
completedAtstringRFC 3339。最終状態のみ
contentStatestringpresent、保持期限で削除した expired、自分で破棄した purged。保持と破棄を参照
contentRemovedBystringuser または system。内容が残っている間は省略
purgedAtstringRFC 3339。内容を破棄した後だけ
retentionobject出力削除時刻 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 日保持を延ばすことはできません。

succeeded
{
  "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"
}
failed
{
  "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[] の説明です。

フィールド型説明
keystring任意のリンク更新に使う互換識別子。pending 中は空
urlstring署名付きの直接 GET URL。準備中・利用不可なら省略。キーを付けない
expiresAtstringURL の RFC 3339 期限。保持中なら再照会で更新
mimestringコンテンツタイプ。例:image/png、video/mp4
width · heightintegerピクセル寸法。音声・テキストにはない
durationSecondsnumber秒単位の長さ。動画・音声のみ
bytesintegerバイトサイズ
pendingboolean以下を参照
unavailableboolean以下を参照
layerobjectレイヤー分解結果のみ。整数の 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 を照会するか、履歴で探します。

JavaScript
// 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);
Python
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
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 で変更していないリクエストを再送します。空の履歴ページは、同じ有料処理を新しいキーで作り直す許可ではありません。

このページの内容