エラー
すべての業務コード、HTTP ステータス、対処方法と、再試行が有効な条件を説明します。
呼び出しが失敗すると、レスポンスは問題の種類を示す数値(code)と、人が読むための説明文(msg)を返します。このページではすべての数値、意味、再試行の有効性を説明します。症状への対処をすぐ知りたい場合は トラブルシューティングから始めてください。
レスポンスの共通形式
ネイティブのタスク API の JSON レスポンスは、どの回答も同じ 4 フィールドで包みます。/v1 互換レスポンスと SSE は、それぞれのプロトコルに従って解析してください。
{ "code": 200, "msg": "success", "data": { }, "request_id": "req_…" }codeは業務コードで、HTTP ステータスではありません。200は成功です。- 失敗時は
dataがありません(nullではありません)。code、msg、request_idが返ります。ただし例外として、一部のアセットライブラリの失敗ではdata.reasonが付きます。 msgは人向けです。文言も言語も変わることがあります(後述)。この文言で分岐せず、codeを使ってください。request_idはX-Request-Idヘッダーにも入ります。問題報告時に伝えてください。
HTTP ステータスも正しく設定します。どちらで分岐しても構いませんが、片方だけを使ってください。特定のコードでは両者が異なります。例えば残高不足は code: 40201、HTTP は 402 です。混在させると、いずれケースを取りこぼします。
code での分岐を推奨します。404 は「見つからない」としか示せませんが、40201 は利用できる残高の不足を示します。
すべてのレスポンスが JSON とは限りません
エッジネットワークの平文・HTML ページなど、JSON として解析できないレスポンスには code がありません。HTTP ステータスで扱い、本文の先頭部分をログに残してください。403 を無条件に再試行しないでください。地域によるブロックなら、再試行では変わりません。
エラーメッセージの言語
人が読むエラー文は、選択した言語で返せます。デフォルトは英語です。選択した言語を使うのは次の 3 か所だけです。
- レスポンス共通形式の
msg。 - 失敗したタスクの
errorMessage。recordInfoと Webhook コールバックの両方に適用しますが、以下の例外があります。 - OpenAI、Anthropic、Google Gemini(
/v1beta)形式の互換エンドポイントのエラーオブジェクトにあるerror.message。
対応言語コード:en、ja、ko、de、fr、es、pt-BR、ru、it、pl。
例外: モデル自体が具体的な失敗理由を返した場合、例えば参照画像を読み込めなかった、生成音声に著作権上の制約があった場合などは、errorMessage にその理由を英語で載せます。サービス名、ホスト、URL、リクエスト ID などの内部情報は除去し、何が起きたかを確認できる形にします。アカウントや課金状態を含む理由はそのまま渡さず、SpicyAPI のメッセージに置き換えます。Content-Language は引き続き msg の言語を示し、分岐には常に errorCode を使います。
言語の選択
次のうち、最初に該当するものを使います。
- リクエストの
Accept-Languageヘッダー。そのリクエストだけに適用します。HTTP 標準の重みに従い、認識できる言語のうちqが最も高いものを選びます。地域別表記は基本言語にまとめ、de-DEはde、pt-PTはpt-BRと扱います。中国語には対応していないため、中国語タグは一致せず、次の候補を確認します。*は指定なしです。 - アカウント設定。コンソールの 設定 → API エラーの言語 で設定します。アカウントの API キーを使い、認識できる
Accept-Languageがないすべてのリクエストに適用します。多くのサーバー側 SDK と curl はデフォルトでこのヘッダーを送らないため、大半の連携ではこの設定を使います。Webhook コールバックには元になるリクエストヘッダーがないため、常にアカウント設定に従います。 - それ以外は英語です。
エラーレスポンスは、実際のメッセージの言語を示す Content-Language ヘッダーを返します。
変わるのは人が読む説明文だけです
code、errorCode、OpenAI 互換の type と code、Gemini 互換の code と status、すべてのフィールド名は、どの言語でも同じです。文言を解析せずコードで分岐し、ログやアラートに code と request_id を記録してください。
例
同じ 40201(残高不足)は、デフォルトでは英語で返ります。
HTTP/1.1 402 Payment Required
Content-Language: en
{ "code": 40201, "msg": "Insufficient balance", "request_id": "req_…" }Accept-Language: ja ヘッダーを 1 つ追加します。それ以外は クイックスタートと同じタスク作成呼び出しです。
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: $SPICY_IDEMPOTENCY_KEY" \
-H "Accept-Language: ja" \
--data "$TASK_PAYLOAD"msg は日本語になりますが、code は 40201 のままです。
HTTP/1.1 402 Payment Required
Content-Language: ja
{ "code": 40201, "msg": "残高が不足しています", "request_id": "req_…" }共通コード
code | HTTP | 意味 | 再試行 |
|---|---|---|---|
200 | 200 | 成功 | — |
400 | 400 | リクエストの形式やパラメーターが不正 | 不可。 修正して再送 |
40003 | 400 | アップロードファイルのサイズ・タイプ・内容が URL 取得時の宣言と違う | 不可。 正しい contentType とサイズで新しい URL を取得し、再アップロードして確定 |
40004 | 400 | リクエストは有効だが、このモデルは現在この設定の組み合わせを処理できない | 不可。 msg が示す解像度・比率などの設定を変えるか、別モデルを選ぶ。後述 |
401 | 401 | 認証失敗。3 種類のどれかは msg が示す | 不可。 ヘッダーかキーを修正 |
403 | 403 | 許可なし。有効なキーを使う API 呼び出しではアカウントが制限中。後述。メール未確認は別の 40310 | 不可 |
404 | 404 | 存在しない、または自分のものではない | 不可 |
409 | 409 | 現在の状態と競合 | 場合による。 後述 |
40901 | 409 | 見積もり期限切れ、リクエストまたは価格の変更 | 再見積もり・再確認する。受理済みリクエストは元の冪等性キーで復旧 |
413 | 413 | リクエスト本文がサーバーのサイズ上限を超える | 不可。 本文を小さくするか、メディアのアップロード手順を使う |
429 | 429 | リクエスト過多 | 通常は可。 Retry-After に従い待つ。ただし速度が原因でない 429 もある。レート制限 |
500 | 500 | SpicyAPI 側で問題が発生 | 可。 待って再試行 |
503 | 503 | 見積もり、インライン画像保存、メディアアップロードなどの補助サービスが一時停止 | 可。 Retry-After があればその時間待って再試行。createTask では同じ Idempotency-Key を使う。Retry-After なしで続く場合は msg に従う。例えばインライン画像の代わりに公開 HTTPS URL を使う |
503 と 50301 は HTTP ステータスは同じですが、別の業務コードです。503 は補助サービス、50301 はモデル自体の問題です。
3 種類の 401 を見分ける
異なる 3 つの原因で認証結果が 401 になります。対処はそれぞれ異なりますが、code はどれも 401 で、違いは msg が示します。
msg(英語) | 実際の原因 | 変更するもの |
|---|---|---|
Missing credentials: send your API key in the Authorization header as "Bearer sk-spicy-..."(互換 /v1・/v1beta では …as "Authorization: Bearer sk-spicy-...", x-api-key, or x-goog-api-key) | 認証情報がまったく届いていない | クライアントがヘッダーを落としている。インターセプター、プロキシ、実行中プロセスへの環境変数の読み込みを確認 |
API key format is not recognized: keys begin with sk-spicy- | 認証情報は届いたが SpicyAPI のキーではない | コンソールのセッション値や他社のキーを貼っている。SpicyAPI キーは sk-spicy- で始まり、echo "${SPICY_API_KEY:0:9}" はその文字列を正確に表示する必要がある |
Credentials are invalid or expired | 形式は合うが、存在しない、撤回済み、または期限切れ | API キーページで確認し、撤回・期限切れなら新しいキーを作る。再試行しても 200 にはならない |
2 番目はデータベース照会なしで判定するため、3 番目とは異なり、その文字列では最初から動作し得ないことを示せます。存在しないキー、撤回済みキー、期限切れキーに 3 番目の同じメッセージを返すのは、どのキーが存在するかを探れないようにするためです。
ここでは英語の文字列を示しています。msg は上記の言語選択に従い、文言は固定契約ではありません。分岐には code を使い、メッセージは読む・表示する・ログに残すものとして扱い、文字列一致に使わないでください。
アカウントが API を呼び出せない
次のどちらもキー自体は有効ですが、アカウントがまだ API を呼び出せません。
40310 はメールアドレスが未確認という意味です。メッセージは Verify your account email to run models and call the API… です。
- コンソールを開きます。上部のバナーが未確認を示し、確認メールを再送できます。
- 確認メールのリンクを開きます。迷惑メールフォルダーも確認してください。
- 同じ呼び出しを再試行します。新しいキーは不要です。
API がブロックされていてもコンソールは使えるため、履歴は確認できます。
通常の 403 と This account cannot call the API right now はアカウントの制限を意味します。request_id を添えてサポートへ連絡してください。
残高と上限
code | HTTP | 意味 | 対応 |
|---|---|---|---|
40201 | 402 | 残高が見積もり額に足りない | コンソールの「請求」で入金。アカウント設定を参照。GET /api/v1/chat/credit も確認 |
40202 | 402 | 支出上限に達した | msg で対象を確認。キー上限(This key has reached its …)は API キーページで引き上げるかリセットを待つ。チーム上限(Workspace … budget has been reached)はオーナー・管理者・予算管理者に「チーム」ページでの引き上げを依頼。プラットフォーム上限(Platform quota for today is exhausted…)は変更できず、UTC 00:00 以降に再試行。日次は UTC 00:00、月次は 1 日の UTC 00:00 にリセット。累計はリセットしない |
再送だけでは残高や上限は変わらないため、どちらも再試行には意味がありません。支出上限を参照してください。
権限
code | HTTP | 意味 | 対応 |
|---|---|---|---|
40301 | 403 | このキーにモデルの呼び出し権限がない | コンソールでキーのモデル許可リストを広げる |
40302 | 403 | 送信元アドレスがキーの IP 許可リストにない | 追加するか、そのキーの IP 制限を解除 |
40303 | 403 | この地域ではサービスを提供していない | 回避できない。提供地域を参照 |
40310 | 403 | アカウントのメールが未確認のため、モデルを実行できない | 登録時の確認メールのリンクを開くか、コンソールから再送。確認すればキーはそのまま動作し、ユーザー側の変更は不要 |
いずれも再試行では解決しません。
アセットライブラリの原因コード
アセットライブラリのエンドポイント(/api/v1/assets…)の失敗では、通常のエンベロープに機械可読のフィールド data.reason が 1 つ加わることがあります。
{ "code": 400, "msg": "…", "data": { "reason": "asset_library_locked" }, "request_id": "req_…" }code、HTTP ステータス、msg、タスクの errorCode は、原因コードの有無にかかわらず同じなので、既存の処理はそのまま動きます。原因コードが付くのはアセットライブラリのエンドポイントだけで、他のネイティブエンドポイントには付きません。OpenAI 互換などの互換レイヤーのエラー形式も変わりません。値は今後増える可能性があるため、未知の値は原因コードがない場合と同じに扱ってください。
data.reason | HTTP | 意味 | 対処 |
|---|---|---|---|
asset_library_locked | 400 | アセットライブラリは最初のチャージ後に利用できます | チャージしてください。保存済みのアセットは引き続き使えます |
asset_library_full | 400 | 保存枠(件数またはバイト数)がいっぱいです | アセットを削除するか、上のプランに上げてください。次のプランは GET /api/v1/assets/usage で確認できます |
asset_file_too_large | 400 | ファイルがプランの 1 ファイル上限を超えています | より小さいファイルを使うか、上のプランに上げてください |
asset_daily_limit | 429 | 1 日の新規作成数の上限に達しました | UTC 0 時まで待ち、Retry-After に従ってください |
asset_storage_full | 503 | プラットフォーム全体のストレージがいっぱいです | しばらくしてからやり直してください |
asset_permission_denied | 403 | チームでのロールでは、この変更は許可されていません | チームのオーナーか管理者に依頼するか、許可されたロールのメンバーが作成したキーを使ってください |
asset_workspace_forbidden | 403 | このキーはそのライブラリにつながりません(例: チームキーを作成したメンバーがチームを離れた) | 必要なワークスペースで新しいキーを作成してください |
asset_entity_name_taken | 409 | ライブラリに同じ名前(大文字と小文字を区別しない)のキャラクターがすでにあります。キャラクターの名前は Web アプリの Assets ページで付けるもので、読み取り専用の API は今のところこれを返しません | 別の名前を選んでください |
モデルの利用可否
code | HTTP | 意味 | 再試行 |
|---|---|---|---|
50301 | 503 | モデルが現在利用できない | 可。 後で再試行するか別モデルを選ぶ |
50302 | 503 | ストリーミングかどうかを問わず、回答が届く前にテキストリクエストがモデル側で失敗し、仮押さえは返却された | 可。 新しい Idempotency-Key で再送 |
40004 | 400 | モデルは稼働中だが、この設定では使えない | 不可。 msg が示す設定を変更 |
50301 は送信時点でモデルが受け付けられなかったことを意味します。非掲載、有効な価格なし、現在の生成容量なしなどです。一時的な容量問題は回数を限定した再試行で解消することがありますが、利用不可モデルや価格の欠落は再送では直りません。カタログを更新し、モデルを切り替える前にユーザーの確認を得てください。
40004 と 50301 は意図的に区別しています。50301 は今はどのタスクも受け付けられず、待つと改善する可能性があります。40004 はモデルは動くもののこの組み合わせには対応せず、msg の括弧内に設定を示します。例:This model cannot serve this parameter combination right now (…)。待っても変わらず、値を変えると改善します。どちらもタスクを作らず、資金を仮押さえしません。
受理後の生成失敗は state: failed で返ります
createTask がタスク ID を返した後の失敗は、recordInfo の state: "failed" と errorCode / errorMessage に現れます。通常、仮押さえは全額解放しますが、モデルページで課金対象と明示した一部のコンテンツ拒否は例外です。40004 と同じ状況が受理後に分かった場合は errorCode: "unsupported_combination" になります。このページにない同期の業務コード分岐を追加しないでください。
タスクの失敗コード
受理後の失敗は recordInfo の errorCode に入ります。コードの集合は固定で、内部診断値は公開しません。一覧にない値は upstream_failed として扱ってください。
errorCode | 意味 | 再試行 |
|---|---|---|
invalid_request | パラメーターが受け付けられなかった | 不可。 修正して再送 |
unsupported_combination | 現在この組み合わせには対応していない | 不可。 別の設定を選ぶ |
content_rejected | 選択したモデルが内容を拒否。SpicyAPI 自体はリクエストを審査しない。コンテンツと利用の責任を参照 | 不可。 プロンプトを変えるか別モデルを選ぶ |
rate_limited | 生成サービスが混雑 | 可。 待って再試行 |
upstream_unavailable | 生成が一時的に利用できない | 可。 少し後に再試行 |
invalid_asset | 参照素材がもう利用できない | 元タスクの再試行は不可。 retry は元入力を再利用するため、素材を再アップロードし、新 URI で新規タスクを作る |
generation_failed | 生成は始まったが出力がなかった | 場合による。 同じ入力では通常同じ失敗になる |
timeout | 未完了のまま期限を超えた | 場合による。 短い時間や低い解像度を試す |
upstream_failed | その他の失敗 | 可。 待って再試行 |
失敗・期限切れは通常、仮押さえを全額解放します。一部のコンテンツ拒否は、モデルページで例外を明示している場合に限り、受理時の見積もり額を課金します。最終請求は settled と cost を確認してください。errorMessage は人向けの説明文です。モデルの理由を英語で載せる場合があり(上記)、文言も変わるため、分岐には errorCode だけを使います。ここでの「再試行」は新しいタスクの送信で、成功すれば再び課金されます。
料金がかかるエラー
タスクの状態と接続の状態は別の事実です
明示的に拒否された作成、期限切れタスク、過去のキャンセル記録には生成料金がかかりません。失敗タスクも通常は無料ですが、モデルページで課金対象と明示した一部のコンテンツ拒否は例外です。クライアントのタイムアウトや切断はタスク失敗ではありません。受理済みタスクはキャンセルできないため、元タスクを復旧して最終状態と精算を確認してください。有効な使用量がすでに記録されたテキストストリームは、切断しても課金される場合があります。
| 状況 | 課金 |
|---|---|
API がタスクを作らず createTask を明示的に拒否(例:40201、40202、40004、50301) | なし。 作成と仮押さえは同一トランザクション。ゲートウェイエラーや応答欠落だけでは拒否と判断できない |
| 送信のタイムアウト、タブの閉鎖、接続切断 | まだ不明。 元の冪等性キーで受理状況を復旧し、無料の再試行だと扱わない |
createTask が 202 を返し、後で failed / expired | 通常は課金なし。モデルページで明示した一部の failed / content_rejected は受理時の見積もり額を課金。settled と cost を読む |
過去のタスクが canceled | なし。 互換用の状態であり、現在使える操作ではない |
createTask が 202 を返し、タスクが succeeded | あり。 実際の使用量で精算し、受理時の仮押さえ額を上限とする。未使用分を解放し、後で超過徴収しない |
recordInfo、chat/credit、common/* のエラー | なし。 これらのエンドポイントは課金しない |
レート制限(429)、または冪等性キーによる重複排除 | なし。 前者は未受理、後者は元タスクを返し、課金は 1 回だけ |
精算・解放はプロセス中断後も自動で完了します。通常の失敗は全額解放し、モデルページに明記した一部のコンテンツ拒否は受理時の見積もり額を課金します。タスクの精算と残高で結果を確認してください。課金を参照してください。
409 の再試行が有効な場合
| 状況 | 再試行 |
|---|---|
同じキーと変更していないペイロードで同時に createTask を呼び、まれに競合した | 可。 数百ミリ秒待って同じキーで再送。冪等性を参照 |
| 同じキーをすでに別ペイロードや別 API キーで使っている | 不可。 処理を止めキーの所属を修正。再試行では競合は変わらない |
インライン画像やアップロード確定の処理中(Retry-After が付く場合あり) | 可。 Retry-After または数秒待ち、同じ Idempotency-Key で再送 |
| 処理中にアカウントの価格や設定が変更された | 可。 見積もりを使う場合は取り直してから再送 |
| 生成ファイルのコピー中 | 可。 数秒待ってダウンロードリンクを再取得 |
未完了または未精算タスクへの jobs/retry | 後で可。 元タスクが最終状態かつ settled が true になるまで待つ |
成功タスク、または保持処理で入力削除済みのタスクへの jobs/retry | 不可。 createTask で新しいタスクを作る |
| アップロード URL またはアップロードファイルの期限切れ | 不可。 ファイルを再アップロードする |
再試行の実装
無条件に再試行しないでください
400、401、40004、40201 などの入力・認証エラーは何度送っても同じです。上の「不可」の再送は無駄になり、毎回自分のレート制限枠を消費します。
指数バックオフに揺らぎを加え、まとめて失敗したリクエストが同時に殺到しないようにします。
// 409 is operation-specific and must be handled at the call site.
const RETRYABLE = new Set([429, 500, 503, 50301]);
async function callWithRetry(path, init, { attempts = 4 } = {}) {
for (let i = 0; i < attempts; i++) {
const res = await fetch(`https://api.spicyapi.ai/api/v1${path}`, init);
// A page from the edge network is not our JSON envelope
if (!res.headers.get('content-type')?.includes('json')) {
const text = await res.text();
throw new Error(`HTTP ${res.status}, not JSON: ${text.slice(0, 200)}`);
}
const body = await res.json();
if (body.code === 200) return body.data;
if (!RETRYABLE.has(body.code)) {
throw new Error(`${body.code} ${body.msg} (request_id=${body.request_id})`);
}
// When the server says how long to wait, trust it
const retryAfter = Number(res.headers.get('retry-after'));
const backoff = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: 2 ** i * 1000 + Math.random() * 500;
await new Promise((r) => setTimeout(r, backoff));
}
throw new Error('out of retries');
}import random, time, requests
# 409 is operation-specific and must be handled at the call site.
RETRYABLE = {429, 500, 503, 50301}
BASE = "https://api.spicyapi.ai/api/v1"
def call_with_retry(method: str, path: str, attempts: int = 4, **kwargs) -> dict:
for i in range(attempts):
res = requests.request(method, f"{BASE}{path}", **kwargs)
# A page from the edge network is not our JSON envelope
try:
body = res.json()
except ValueError:
raise RuntimeError(f"HTTP {res.status_code}, not JSON: {res.text[:200]}")
if body["code"] == 200:
return body["data"]
if body["code"] not in RETRYABLE:
raise RuntimeError(f'{body["code"]} {body["msg"]} (request_id={body.get("request_id")})')
# When the server says how long to wait, trust it
retry_after = res.headers.get("retry-after")
backoff = float(retry_after) if retry_after else 2**i + random.random() * 0.5
time.sleep(backoff)
raise RuntimeError("out of retries")package main
import (
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"strconv"
"time"
)
// retryable lists the codes worth another attempt. Anything else returns
// the same answer however many times you ask — and each ask eats your own
// rate limit.
// Whether the generation itself failed is read from recordInfo's state.
var retryable = map[int]bool{429: true, 500: true, 503: true, 50301: true}
type envelope struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data json.RawMessage `json:"data"`
RequestID string `json:"request_id"`
}
func callWithRetry(newReq func() (*http.Request, error), attempts int, out any) error {
for i := 0; i < attempts; i++ {
req, err := newReq()
if err != nil {
return err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
// Transport-level failure: the request may well have arrived.
// On createTask, send the same Idempotency-Key or this retry
// turns into a second generation and a second charge.
time.Sleep(backoff(i, ""))
continue
}
var env envelope
decErr := json.NewDecoder(res.Body).Decode(&env)
io.Copy(io.Discard, res.Body)
res.Body.Close()
if decErr != nil {
// Not our JSON envelope, for example a page from the edge network.
return fmt.Errorf("HTTP %d, response is not JSON: %w", res.StatusCode, decErr)
}
if env.Code == 200 {
return json.Unmarshal(env.Data, out)
}
if !retryable[env.Code] {
return fmt.Errorf("%d %s (request_id=%s)", env.Code, env.Msg, env.RequestID)
}
// When the server says how long to wait, trust it
time.Sleep(backoff(i, res.Header.Get("Retry-After")))
}
return fmt.Errorf("out of retries")
}
// backoff trusts Retry-After when present, otherwise backs off
// exponentially with jitter so a batch of failures does not come back
// as one thundering herd.
func backoff(attempt int, retryAfter string) time.Duration {
if s, err := strconv.Atoi(retryAfter); err == nil && s > 0 {
return time.Duration(s) * time.Second
}
base := time.Duration(1<<attempt) * time.Second
return base + time.Duration(rand.Intn(500))*time.Millisecond
}createTask の再試行では、必ず同じ Idempotency-Key を送ってください。別のキーにすると「実際は成功していたが応答を失った」が二重生成・二重課金になります。
原因の調査
エラーの理由が分からないときは、次の順で確認してください。
request_id:これを伝えれば SpicyAPI が該当リクエストを特定できます。code:上の表を確認します。- コンソールのログ:ログページの検索欄にタスク ID または
request_idを貼り、タスクを開きます。状態、費用、遅延、送信したキー、エラーコード、理由を確認できます。パラメーターと結果は保持中だけ表示し、チームでは内容の閲覧権限があるロールだけに表示します。
500 の msg は常に内部情報を含まない汎用の「サービスが一時的に利用できません」という説明です。内部構造、コンポーネント名、スタックトレースをレスポンスに出すことはありません。調査は request_id から行います。
問い合わせはコンソールの サポートから会話を開始するか、support@spicyapi.ai にメールしてください。request_id またはタスク ID、時刻(UTC)、code を添え、API キーは絶対に送らないでください。

