TypeScript SDK
TypeScript SDK(@spicyapi/sdk)を一歩ずつ解説するガイドです。インストールして最初の結果を得るまでの手順と、すべてのメソッド・オプション・エラーを、わかりやすい言葉でまとめています。Python、Go、PHP、Java の SDK は、それぞれ専用のページで説明しています。
@spicyapi/sdk は、SpicyAPI 公式の TypeScript/JavaScript ライブラリです。このページは、SDK を一度も使ったことがない方を想定しています。空のフォルダーから始めて画像を保存するところまでを案内し、そのあとでパッケージのすべての関数について、何のためのものか、何を渡すか、何が返ってくるか、お金がかかるか、どんな問題が起こりうるかを説明します。
SDK とは
SDK とは、自分のプログラムに組み込んで使う、出来合いのコードライブラリです。SDK がなければ、プログラムはすべての HTTP リクエストを手で組み立てなければなりません。API キーを付ける、JSON で返ってきた答えをデータに変換する、タスクが終わったかどうかを何度も問い合わせる、ネットワークが切れたら再試行する、ファイルを 3 つのステップに分けてアップロードする、Webhook が届くたびに署名を確認する、といった作業です。SDK はそのすべてを引き受けるので、生成 1 回分がまるごと 1 回の呼び出しに収まります。
const task = await client.run({ model: modelId, input: { prompt: "A paper lantern at night" } });Node.js(JavaScript または TypeScript)で、サーバー上で動くプログラム、自分のパソコンで動かすスクリプト、バックグラウンドジョブを書いている方や、これから書こうとしている方に向いています。TypeScript を使えば、すべてのフィールドで入力補完と型チェックも効きます。
このツールで合っている?
| やりたいこと | 使うもの |
|---|---|
| 自分の Node.js や TypeScript のコードから SpicyAPI を呼び出す | この SDK |
| コードを書かずに、ターミナルから生成する | CLI |
| Claude Code や Cursor などの AI アシスタントに、SpicyAPI を代わりに呼び出してもらう | MCP サーバーと Agent Skill |
| チャットモデルやテキストモデルを使う | SpicyAPI に向けた OpenAI 公式クライアント。テキストとストリーミングを参照 |
| Python、Go、PHP、Java を使う | 公式の Python SDK、Go SDK、PHP SDK、Java SDK |
| そのほかの言語を使う | HTTP を直接呼び出すか、OpenAPI とエンドポイント一覧から生成したクライアント |
SDK がしないこと
- ブラウザーやモバイルアプリでは動きません。 Node.js が必要です。それに、API キーは絶対にサーバーの外に出してはいけません。Web ページやアプリに入れたキーは、誰でも読み取れてしまうからです。Web サイトからは自分のバックエンドを呼び出し、バックエンドから SpicyAPI を呼び出す形にしてください。
- チャットモデルは扱いません。 テキスト生成には OpenAI 互換のエンドポイントを使い、それは公式の
openaiパッケージがすでに十分うまくこなします。テキストとストリーミングを参照してください。 - モデルを選んだり、フィールドを埋めたりはしません。 受け付けるフィールドはモデルごとに異なります。SDK が提供するのはモデルの一覧と、各モデルのスキーマ(受け付けるフィールドの一覧)です。何を送るかは、あなたが決めます。
- タスクをキャンセルできません。 SpicyAPI がいったん受理したタスクは、最後まで実行されます。プログラムを止めても、止まるのはあなたの側の待機だけです。
- アカウントの管理はしません。 チャージ、キーの作成、利用上限、Webhook 署名キーは、コンソールで設定します。
API キーはサーバーから出さない
SPICY_API_KEY を、ブラウザーの JavaScript、モバイルアプリ、公開リポジトリ、スクリーンショット、チャットのメッセージに入れないでください。漏えいしたキーは、無効にするまで残高を使い続けられてしまいます。SDK はキーを環境変数から読み取るので、キーをコードに書く必要はありません。
このページで使う言葉
| 言葉 | このページでの意味 |
|---|---|
| モデル ID | モデルの正確な名前で、publisher/model/task の形をしています。必ずライブカタログ(listModels())からコピーし、記憶に頼らないでください |
| 入力(input) | タスクと一緒に送る設定で、prompt や image_url などです。使えるフィールドの一覧はモデルごとに決まっていて、これをスキーマと呼びます |
| タスク | 1 回分の生成作業です。あなたが作成し、SpicyAPI がバックグラウンドで処理し、あなたが結果を受け取ります。タスクには job_ で始まる ID が付きます |
| 仮押さえ | タスクが受理されると、その最大価格が残高から確保されます。 失敗・期限切れのタスクでは通常、仮押さえ額が全額解放されます。ただし、モデルページに明記された一部のコンテンツ拒否は、受理時の見積額で課金されます。最終請求額は settled と cost で確認してください。 |
| 冪等キー | 1 つの作業に対して自分で考えて付けるラベルで、order-8814-image-1 のようなものです。同じリクエストを同じラベルで 2 回送ると、SpicyAPI は 2 つ目のタスクを作って課金するのではなく、最初のタスクを返します |
| Webhook | タスクが終わったときに SpicyAPI が呼び出す、あなたの Web アドレスです。これがあれば、何度も問い合わせる必要がありません |
できること
特に断りがない限り、以下の関数はすべてクライアントのメソッドです(client.listModels() のように呼び出します)。「お金がかかる」とは、その呼び出しによって残高に課金が発生しうることを意味します。
| やりたいこと | メソッド | お金がかかる? |
|---|---|---|
| SpicyAPI に接続できるか確認する(キー不要) | getStatus | いいえ |
| ライブのモデル一覧、あなたの価格、各モデルのフィールドを見る | listModels、getModel | いいえ |
| お金を使う前に、リクエストの正確な価格を知る | quoteTask | いいえ |
| 生成を開始する | createTask | はい |
| 生成を開始し、1 回の呼び出しで結果まで待つ | run | はい |
| タスクを 1 回照会する | getTask | いいえ |
| タスクが終わるまで待つ | waitForTask | いいえ |
| 失敗したタスクをもう一度試す | retryTask | はい(新しいタスク) |
| 自分のキーで作成したタスクを一覧する | listTasks | いいえ |
| 残高を確認する | getBalance | いいえ |
| 使った金額を日別・モデル別に見る | getUsage | いいえ |
| 手元の画像・動画・音声ファイルをアップロードする | uploadFile、uploadBytes、uploadBase64 | いいえ |
| 結果の新しいダウンロードリンクを得る | createDownloadUrl | いいえ |
| 終了したタスクの保存ファイルとプロンプトを削除する | purgeTask | いいえ(返金もなし) |
| Webhook が本当に SpicyAPI から来たものか確認する | verifyWebhook(通常の関数) | いいえ |
| ドキュメントを検索する、API 仕様をオフラインで読む | searchDocumentation、readOpenApiContract(通常の関数) | いいえ |
始める前に
所要時間は 10 分ほどです。各ステップでコマンドが何をするかを説明するので、わからないまま実行するものはありません。
Node.js 22.13 以降をインストールする
Node.js は、ブラウザーの外で JavaScript を動かすためのプログラムです。nodejs.org から LTS 版をインストールしてください。22.13 以降なら、どのバージョンでも動きます。インストールしたら、ターミナル(macOS ならターミナル、Windows なら PowerShell)を開いて確認します。
node -vインストールされているバージョンが、v22.13.0 や v24.8.0 のように表示されます。数字が 22.13 より小さい場合や、コマンドが見つからない場合は、Node.js をインストールし直してから、新しいターミナルウィンドウを開いてください。
プロジェクトフォルダーを作って SDK をインストールする
次のコマンドを 1 つずつ実行します。macOS、Linux、Windows PowerShell のどれでも同じように動きます。
mkdir spicy-demo
cd spicy-demo
npm init -y
npm pkg set type=module
npm install @spicyapi/sdk| コマンド | すること |
|---|---|
mkdir spicy-demo | プロジェクト用の空のフォルダーを作ります |
cd spicy-demo | ターミナルをそのフォルダーに移動します。以降のコマンドは、このフォルダーに対して実行されます |
npm init -y | package.json を作ります。プロジェクトの設定と、使うライブラリを記録するファイルです |
npm pkg set type=module | ファイルが新しい import 構文を使うことを Node.js に伝えます。この SDK にはこの設定が必要です |
npm install @spicyapi/sdk | SDK を node_modules フォルダーにダウンロードし、package.json に記録します |
SDK は、新しい形式の ES モジュールとしてのみ公開されています。import { SpicyClient } from "@spicyapi/sdk" は動きますが、古い require("@spicyapi/sdk") は ERR_PACKAGE_PATH_NOT_EXPORTED で失敗します。npm pkg set type=module を実行しない場合は、代わりにファイル名の拡張子を .mjs にしてください。
API キーを作成する
- spicyapi.ai にログインします。まだアカウントがない場合は、spicyapi.ai/register で登録してください。新規登録を一時停止している場合は、同じページからウェイトリストに登録できます。
- コンソールの API キーページを開き、キーを作成をクリックします。必須なのは名前だけです。
- すぐにキーをコピーしてください。キーは
sk-spicy-で始まり、一度しか表示されません。なくした場合は、新しいキーを作成してください。
同じページで日次利用上限を設定できますが、新しいキーには追加するまで上限が付きません。有料のタスクを実行するには残高も必要です。残高は 請求ページでチャージします。
キーを環境変数に入れる
環境変数とは、ターミナルが起動するすべてのプログラムに渡す、名前付きの値です。SDK は SPICY_API_KEY という名前の変数を探すので、キーをコードに書き込む必要はありません。
# このターミナルウィンドウで設定します(自分のキーを貼り付けてください)
export SPICY_API_KEY="sk-spicy-paste-your-key-here"
# 確認:sk-spicy- と表示されれば OK です
echo "${SPICY_API_KEY:0:9}"この値は、ウィンドウを閉じると消えます。自分のパソコンでずっと使えるようにするには、export の行を ~/.zshrc(macOS)または ~/.bashrc(Linux)に追加し、新しいウィンドウを開いてください。
.env ファイルを使いたい場合は: プロジェクトフォルダーに .env という名前のファイルを作り、SPICY_API_KEY=sk-spicy-paste-your-key-here という 1 行だけを書きます。プログラムは node --env-file=.env yourfile.mjs で起動してください。SDK 自体は .env ファイルを読みません。読み込むのはこのフラグです。キーが誤ってコミットされないよう、git add を一度でも実行する前に、.env を .gitignore ファイルに追加しておいてください。
本番のサーバーでは、ホスティングサービスのシークレット設定や環境変数の設定で SPICY_API_KEY を設定するか、シークレットマネージャーから取り出したキーを new SpicyClient({ apiKey }) で渡してください。
最初のファイルを実行する
プロジェクトフォルダーで、好きなテキストエディターを使って check.mjs という名前のファイルを作り、次の内容を貼り付けて保存します。
import { SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
const status = await client.getStatus();
console.log(status.operational ? "SpicyAPI is up" : "SpicyAPI is not reachable right now");
console.log("Checked at", status.checkedAt);実行します。
node check.mjsSpicyAPI is up
Checked at 2026-09-17T08:00:00.000Zこの確認はキーが不要で、お金もかからないので、Node.js と SDK が正しくインストールされていることを安心して確かめられます。
TypeScript を使う場合は: 同じコードを check.ts として保存します。最近の Node.js 22 と 24 なら、node check.ts でそのまま実行できます。.ts 拡張子についてエラーが出る場合は、node --experimental-strip-types check.ts か npx tsx check.ts を実行してください。
最初の結果を得る
このプログラムは、一連の流れをすべて行います。サービスの状態を確認し、あなたのキーで使える画像モデルを選び、正確な価格を取得したところで止まります。--yes を付けてもう一度実行すると、画像を作成し、完了を待って、スクリプトと同じフォルダーに保存します。
--yes を付けて実行するとお金がかかります
--yes を付けない場合、プログラムは読み取りしかしません。何も作成されず、何も課金されません。--yes を付けると画像タスクを 1 つ作成し、見積額を仮押さえして、画像の生成に成功した場合に課金します。
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { SpicyApiError, SpicyClient } from "@spicyapi/sdk";
// 環境変数 SPICY_API_KEY からキーを読み取ります。
const client = new SpicyClient();
async function main() {
// ステップ 1 — SpicyAPI に接続できるか。無料で、キーも不要です。
const status = await client.getStatus();
if (!status.operational) {
throw new Error("SpicyAPI is not reachable right now. Try again in a few minutes.");
}
// ステップ 2 — このキーで呼び出せる画像モデルはどれか。無料です。
const catalog = await client.listModels({ modality: "image", includeExamples: true });
const model = catalog.items.find(
(item) => item.enabled && item.available && item.examples?.length,
);
if (!model) throw new Error("No image model is available to this key right now.");
console.log(`Model: ${model.displayName} (${model.model})`);
// ステップ 3 — 入力を組み立てます。サーバーが検証済みの入力例を出発点にし、
// モデルがプロンプトを受け付けるなら、自分のプロンプトに差し替えます。
const input = { ...model.examples?.[0]?.input };
if ("prompt" in input) input.prompt = "A paper lantern glowing on a rainy street at night";
// ステップ 4 — 正確な価格を取得します。無料で、何も作成・確保されません。
const request = { model: model.model, input };
const quote = await client.quoteTask(request);
console.log(`Price: $${quote.estimatedCost} USD`);
if (!process.argv.includes("--yes")) {
console.log("Nothing was charged. Run again with --yes to generate the image.");
return;
}
// ステップ 5 — タスクを作成して完了を待ちます。お金がかかるのはこのステップです。
const task = await client.run(
{ ...request, quoteId: quote.quoteId, expectedCost: quote.estimatedCost },
{
idempotencyKey: randomUUID(),
onAccepted: (accepted) => console.log(`Task accepted: ${accepted.taskId}`),
onUpdate: (update) => console.log(`State: ${update.state}`),
},
);
// ステップ 6 — 失敗したタスクも通常の結果として返ります。精算結果を確認します。
if (task.state !== "succeeded") {
console.log(`The task ended as ${task.state}: ${task.errorCode} ${task.errorMessage ?? ""}`);
console.log("Check settled and cost for the final charge.");
return;
}
// ステップ 7 — すぐに結果をダウンロードします。リンクは約 20 分で期限切れになります。
const asset = task.output?.assets?.find((item) => item.url);
if (!asset?.url) throw new Error("The task succeeded but its file is unavailable.");
const download = await fetch(asset.url); // ここには API キーを絶対に送らないこと
if (!download.ok) throw new Error(`Download failed with HTTP ${download.status}`);
const extension = asset.mime?.split("/")[1] ?? "png";
await writeFile(`result.${extension}`, Buffer.from(await download.arrayBuffer()));
console.log(`Saved result.${extension}. Charged: $${task.cost}${task.settled ? "" : " (not final yet)"}`);
}
main().catch((error) => {
if (error instanceof SpicyApiError) {
console.error(`SpicyAPI error ${error.code}: ${error.message} (request_id ${error.requestId})`);
} else {
console.error(error);
}
process.exitCode = 1;
});2 回実行します。
node first-result.mjs
node first-result.mjs --yesModel: <display name> (<model ID>)
Price: $<amount> USD
Nothing was charged. Run again with --yes to generate the image.
Model: <display name> (<model ID>)
Price: $<amount> USD
Task accepted: job_…
State: running
State: succeeded
Saved result.png. Charged: $<amount>各ステップで何をしているか
getStatus()は、SpicyAPI のヘルスチェック用エンドポイントに、サービスが稼働しているかを問い合わせます。ネットワークの問題で例外を投げることはなく、代わりにoperational: falseを返します。listModels()は、あなたのアカウントの価格が入ったライブのモデルカタログを返します。使うのは、enabledとavailableがどちらもtrueの項目だけにしてください。includeExamples: trueを付けると、サーバーが検証済みの入力例が追加されます。これがいちばん安全な出発点です。あとで送る値は、その項目のmodelフィールド、つまりモデル ID です。- 入力。 受け付けるフィールドはモデルごとに異なり、モデルが知らないフィールドを送ると
400エラーで拒否されます。検証済みの入力例をコピーしてプロンプトだけを変えれば、これを避けられます。モデルが受け付けるすべてのフィールドを見るには、getModel()を呼び出してinputSchemaを読んでください。 quoteTask()は、このリクエストそのものの価格を計算します。何も作成せず、何も確保しません。金額は"0.04"のように文字列で書かれた米ドルなので、丸め誤差が入り込むことはありません。run()はタスクを作成し、終わるまで様子を確認し続けます。quoteIdとexpectedCostを渡すのは、「表示された価格のときだけ受理してほしい」という意味です。価格が変わっていれば、別の金額で課金されるのではなく、呼び出しが40901で失敗します。idempotencyKeyがあれば、ネットワークエラー後の再試行も安全です。実際のアプリでは、再起動したプログラムが同じキーを使い回せるよう、呼び出す前にキーを保存しておいてください。onAcceptedは、タスクができた瞬間にタスク ID を渡してくれます。ここで ID を保存しておけば、プログラムが止まっても、あとからタスクを確認できます。 6.タスクの状態。run()は、失敗したタスクでも普通に値を返します。stateを確認してください。結果があるのはsucceededだけです。failedとexpiredはerrorCodeで理由がわかり、課金されません。失敗・期限切れのタスクでは通常、仮押さえ額が全額解放されます。ただし、モデルページに明記された一部のコンテンツ拒否は、受理時の見積額で課金されます。最終請求額はsettledとcostで確認してください。- ダウンロード。 結果には、それぞれ一時的な
urlが付いています。API キーを付けずにダウンロードし、自分用のコピーを保存してください。リンクの有効期間は約 20 分で(新しいリンクが必要ならgetTask()をもう一度呼び出します)、SpicyAPI は生成したファイルを最長でも約 14 日後に削除します。データ保存と破棄を参照してください。
動画も同じ手順で作れますが、数分かかることがあります。run() には、もっと長い timeoutMs を渡してください(既定の待ち時間は 10 分です)。以下のコード例では、モデル ID を MODEL_ID_FROM_CATALOG と書いています。これは実在するモデルではなく、仮の値です。ライブカタログ(SDK なら listModels()、または認証付きの GET /api/v1/models エンドポイント)から、enabled と available がどちらも true の項目を選び、その model の値に置き換えてください。
API リファレンス
以下の各項目では、メソッドが何のためのものか、どう呼び出すか、何が返ってくるか、お金がかかりうるか、どんなエラーが起こりうるかを説明します。SpicyApiError などのエラークラスは、エラーの扱い方で説明しています。金額はすべて文字列で書かれた米ドル("0.04")で、時刻はすべて UTC の ISO 8601 形式の文字列です。
クライアントを作る
new SpicyClient(options?: SpicyClientOptions)ほとんどのプログラムは何も渡しません。new SpicyClient() は SPICY_API_KEY からキーを読み取り、本番のサービスと通信します。オプションが不正な場合は、リクエストを送る前に、その場で TypeError を投げます。
プロパティ
型
自動再試行
SDK が自動でリクエストを再試行するのは、それによって二重に課金されるおそれがない場合だけです。
| 呼び出し | 自動で再試行される? |
|---|---|
読み取り系の getStatus、listModels、getModel、getTask、listTasks、getBalance、getUsage、および quoteTask、purgeTask、commitUploadedFile | はい |
createTask、run(の作成ステップ)、retryTask | idempotencyKey を渡した場合のみです。渡さない場合、ネットワークエラーはそのままあなたに投げられます。送り直すと、2 つ目のタスクができてしまうおそれがあるからです |
createUploadUrl、バイト列のアップロードそのもの、createDownloadUrl | いいえ |
再試行が行われるのは、応答がまったくない場合(ネットワークエラーやローカルのタイムアウト)か、HTTP 408・429・500・502・503・504 が返った場合です。待ち時間は、サーバーの Retry-After ヘッダーに指定があればそれに従い、なければ 250 ミリ秒、500 ミリ秒、1 秒……と延びていきます。それぞれ最大 25% の幅でばらつきが加わり、maxRetryDelayMs が上限です。すべての試行が失敗した場合は、最後のエラーが返されます。
signal で呼び出しを止める
すべてのメソッドは、標準の AbortSignal である signal オプションを受け付けます。締め切りを設けるなら AbortSignal.timeout(5_000)、好きなときに止めるなら AbortController を使ってください。止めると、呼び出しは中断の理由を投げます。止まるのはあなたのリクエストや待機だけです。SpicyAPI がすでに受理したタスクは実行を続け、課金されることもあります。
getStatus()
SpicyAPI に接続できるかを確認します。スクリプトの最初の 1 行や、自分の監視システムのヘルスチェックとして便利です。
getStatus(options?: { signal?: AbortSignal }): Promise<ServiceStatus>| 返るフィールド | 意味 |
|---|---|
operational | サービスが応答していれば true です。使うべきはこのフィールドです |
health | ヘルスチェックの生の結果です。path、ok、HTTP の status、body の先頭 512 文字、応答が届かなかった場合の error が入ります |
readiness | 2 つ目のチェックの結果で、形は同じです。公開サービスでは 404 を返しますが、これは正常で、サービスが停止しているという意味ではありません |
checkedAt | チェックを行った時刻 |
お金: かかりません。キー: 不要です。エラー: ネットワークの問題では例外を投げません。代わりに operational: false が返り、理由が health.error に入ります。例外を投げるのは、signal で中断した場合だけです。使用例は最初のファイルを実行するを参照してください。
listModels(options?)
ライブのモデルカタログを、あなたのアカウントの価格付きで一覧します。モデル ID は、すべてここから取得します。
listModels(options?: ListModelsOptions): Promise<ModelList>プロパティ
型
戻り値は { total, items } です。各項目のうち、特に役立つフィールドは次のとおりです。
| フィールド | 意味 |
|---|---|
model | モデル ID です。タスクを作成するときは、この値をそのまま送ります |
displayName | 人が読むための名前 |
modality、tasks | 何を出力し、どんな作業をするか |
provider、family | モデルの開発元と、どの製品シリーズに属するか |
enabled、available | モデルを使う前に、両方が true である必要があります |
pricing | 価格の段階ごとに 1 件ずつ入ります。price、unit(per_image、per_second、per_request、per_1k_tokens のいずれか)、variant(どの段階か。解像度であることが多い)です。割引期間中は、regularPrice に割引前の価格が入ります |
startingPrice | いちばん安い段階です。正確な金額は quoteTask で確認してください |
quantityField | 課金される数量を決める入力フィールド(長さなど)です。空なら、1 回の呼び出しにつき 1 単位です |
taskTimeoutSeconds | タスクが expired で終わるまでに実行できる時間 |
maxOutputDurationSeconds | モデルの出力に長さがある場合の、許される最大の長さ |
inputSchema、examples | 要求した場合にだけ含まれます |
updatedAt、version | モデルの詳細が変わるたびに変わります。保存しておいたスキーマをいつ更新すべきかの判断に使えます |
お金: かかりません。キー: 必要です。エラー: SpicyApiError(たとえば、キーの問題なら 401、リクエストが多すぎれば 429)、SpicyTransportError。
const { items } = await client.listModels({ modality: "video", task: "image-to-video" });
for (const item of items) {
if (!item.enabled || !item.available) continue;
const from = item.startingPrice ? `from $${item.startingPrice.price} ${item.startingPrice.unit}` : "";
console.log(item.model, "|", item.displayName, "|", from);
}カタログの読み取りは、タスク作成とは独立した、アカウントの読み取り枠を使います。タスクのたびに読み取るのではなく、一覧を保存しておき、ときどき、あるいは 400 や 50301 が返ったあとに更新してください。
getModel(modelId)
1 つのモデルを、完全なスキーマ付きで返します。スキーマとは、そのモデルが受け付ける入力フィールドの一覧に、どれが必須か、どんな値が許されるかを加えたものです。
getModel(modelId: string, options?: { signal?: AbortSignal }): Promise<ApiModel>結果には listModels で説明したすべてのフィールドが含まれ、inputSchema と examples も必ず入っています。スキーマは標準の JSON Schema です。properties には各フィールドが type とともに並び、多くの場合 enum(許される値)、minimum / maximum、default も付いています。required には、必ず送らなければならないフィールドが並びます。
お金: かかりません。キー: 必要です。エラー: その ID のモデルがなければ 404 の SpicyApiError、ID が空なら TypeError。
const model = await client.getModel("MODEL_ID_FROM_CATALOG");
console.log("Required fields:", model.inputSchema?.required);
console.log(JSON.stringify(model.inputSchema?.properties, null, 2));
console.log("A request that is known to work:", model.examples?.[0]?.input);quoteTask(request)
何も作成せずに、リクエストの正確な価格を返します。人に価格を見せるときや、予算を超える作業を断るときに使います。
quoteTask(request: { model: string; input: object; callBackUrl?: string }, options?: { signal?: AbortSignal }): Promise<TaskQuote>タスクを作成するときに使うつもりの model、input、(使うなら)callBackUrl を、そのまま渡してください。
| 返るフィールド | 意味 |
|---|---|
quoteId | createTask か run に渡すと、この価格で確定させられます |
estimatedCost | いまタスクを作成した場合に仮押さえされる金額 |
maxCharge | このリクエストにかかりうる最大額 |
currency | 常に USD |
quantity、unit | 課金される数量です。たとえば "5" と per_second |
expiresAt | 見積もりの有効期間は 5 分です |
お金: かかりません。キー: 必要です。エラー: SpicyApiError。たとえば、入力がモデルのスキーマと合わなければ 400、モデルが利用できなければ 50301、ほかに 429。
const request = { model: "MODEL_ID_FROM_CATALOG", input: { prompt: "A paper lantern at night" } };
const quote = await client.quoteTask(request);
console.log(`This will cost $${quote.estimatedCost}. Quote valid until ${quote.expiresAt}.`);createTask(request, options?)
生成を開始し、すぐにタスク ID を返します。結果を待つことはしません。続けて waitForTask か Webhook を使うか、run で両方を 1 回の呼び出しで行ってください。
createTask(request: CreateTaskInput, options?: CreateTaskOptions): Promise<CreateTaskResult>リクエスト:
プロパティ
型
オプション:
プロパティ
型
| 返るフィールド | 意味 |
|---|---|
taskId | 新しいタスクの ID です。すぐに保存してください |
state | 新しいタスクなら queued。冪等キーを繰り返し送った場合は、元のタスクの現在の状態 |
estimatedCost | いま仮押さえされている金額で、このタスクで課金されうる最大額でもあります |
deadlineAt | タスクが終わっていない場合に、SpicyAPI が処理を打ち切る時刻 |
お金: かかります。estimatedCost が残高から仮押さえされます。タスクが成功すると、実際の使用量で課金され(仮押さえ額を超えることはありません)、残りは解放されます。失敗した場合や期限切れになった場合は、仮押さえ額が全額解放されます。失敗・期限切れのタスクでは通常、仮押さえ額が全額解放されます。ただし、モデルページに明記された一部のコンテンツ拒否は、受理時の見積額で課金されます。最終請求額は settled と cost で確認してください。
エラー:
error.code | 意味 | 対処 |
|---|---|---|
400 | 入力がモデルのスキーマと合っていないか、callBackUrl が許可されていない | getModel().inputSchema と見比べて、リクエストを直す |
401 | キーがない、形式が正しくない、または失効している | SPICY_API_KEY を確認する |
40201 | 残高が仮押さえ額に足りない | コンソールの請求ページでチャージする |
40202 | 日次・月次・累計のいずれかの利用上限に達した | API キーページで上限を引き上げるか、リセットを待つ |
40301 | このキーには、このモデルの使用が許可されていない | コンソールで、キーのモデル一覧を変更する |
409 | この冪等キーは、すでに別のリクエストに使われている | 新しい作業には新しいキーを使う |
40901 | 見積もりの期限が切れたか、価格またはリクエストが変わった | もう一度見積もりを取り、新しい価格を確認する |
413 | リクエストの本文が大きすぎる | メディアファイルは本文に埋め込まず、アップロードする |
429 | リクエストが多すぎる | error.retryAfterSeconds だけ待ってから、もう一度試す |
50301 | モデルがいまタスクを受け付けられない | 時間をおいて試すか、別のモデルを選ぶ |
SpicyTransportError や SpicyTimeoutError は、応答が届かなかったことを意味します。そのため、タスクが作成されたかどうかはわかりません。同じリクエストを同じ冪等キーでもう一度送ってください。タスクが作成されるか、すでにあるタスクが返ってくるかのどちらかになります。TypeError は、オプションが不正だった(たとえば retentionSeconds が負の値だった)ことを意味し、何も送られていません。
import { randomUUID } from "node:crypto";
const idempotencyKey = randomUUID(); // 実際のアプリでは、まず注文データと一緒にこれを保存します
const accepted = await client.createTask(
{ model: "MODEL_ID_FROM_CATALOG", input: { prompt: "A paper lantern at night" } },
{ idempotencyKey },
);
console.log(accepted.taskId, accepted.state, "on hold: $" + accepted.estimatedCost);run(request, options?)
いちばん簡単な生成方法です。createTask を呼び出してから waitForTask を呼び出し、完了したタスクを返します。
run(request: CreateTaskInput, options?: RunTaskOptions): Promise<TaskRecord>request は createTask と同じです。オプションには、createTask が受け付けるものすべてに加えて、次のものがあります。
プロパティ
型
完了したタスクを返します。フィールドは getTask で説明しているものと同じです。成功したタスクについては、結果ファイルのコピーが終わるまで待つので、output.assets[].url はすぐにダウンロードできる状態になっています。
お金: createTask とまったく同じようにかかります。失敗したタスクは、例外として投げられるのではなく、戻り値として返ります。task.state を確認してください。
エラー: createTask が投げうるすべてのエラーに加えて、次のものがあります。
SpicyWaitTimeoutError:タスクが受理されたあとに、timeoutMsを使い切った場合です。タスクは SpicyAPI 上で実行を続けています。あとでwaitForTask(error.taskId)を呼び出して、結果を受け取ってください。SpicyTimeoutError:SpicyAPI がタスクの受理を確認する前に、時間切れになった場合です。どうなったか確かめるには、同じリクエストを同じ冪等キーで送ってください。- 自分で書いた
onAcceptedやonUpdateの中でエラーが投げられると、待機はそこで止まります。タスク自体は実行を続けます。 timeoutMsやintervalMsが不正な場合のTypeError。これらは、何かを作成する前に確認されます。
完全な例は、最初の結果を得るを参照してください。
getTask(taskId)
タスクを 1 回読み取ります。状態、結果のリンク、かかった金額がわかります。
getTask(taskId: string, options?: { signal?: AbortSignal }): Promise<TaskRecord>| フィールド | 意味 |
|---|---|
taskId、model | どのタスクで、どのモデルで作ったか |
state | 処理中は queued か running、終了すると succeeded、failed、expired のいずれかです(canceled は古い記録にだけ現れます) |
output.assets | 結果ファイルです。内容は次の表を参照してください。succeeded の場合だけ入ります |
output.text | テキストを返すモデルの、テキスト出力 |
errorCode、errorMessage | タスクが失敗した理由です。判断には errorCode を使ってください。メッセージの言い回しは変わることがあります |
cost | settled が false の間は仮押さえ中の金額、true になると最終的な課金額 |
settled | cost が確定しているかどうか |
createdAt、completedAt、deadlineAt | 作成された時刻、終了した時刻、期限切れになる時刻 |
input | 保存されている入力です。プロンプトの保存期間が過ぎると削除されます |
sourceTaskId | retryTask で作成されたタスクに付き、元のタスクを指します |
contentState | present、expired(保存期間のルールで削除された)、purged(purgeTask で自分で削除した)のいずれか |
retention | ファイル(outputsExpireAt)とプロンプト(promptsExpireAt)が削除される時刻 |
output.assets の各項目:
| フィールド | 意味 |
|---|---|
url | 一時的なダウンロードリンクです。API キーは付けずにダウンロードします |
expiresAt | リンクが使えなくなる時刻で、通常は約 20 分後です。新しいリンクが必要なら、getTask をもう一度呼び出します |
mime | image/png や video/mp4 などの、ファイルの種類 |
width、height、durationSeconds、bytes | 該当する場合の、サイズの情報 |
pending | ファイルをまだストレージにコピーしている間は true です。数秒後にもう一度確認してください |
unavailable | ファイルが完全に失われている場合は true です。待っても直らないので、もう一度生成してください |
key | 内部のファイル名です。createDownloadUrl でだけ必要になります |
SpicyAPI 側に値がないフィールドは、null になるのではなく省略されます。上のフィールドは、どれも省略されうるものとして扱ってください。
お金: かかりません。エラー: その ID のタスクがあなたのキーから見えない場合は、404 の SpicyApiError。
const task = await client.getTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3"); // 保存しておいた ID
if (task.state === "succeeded") {
for (const asset of task.output?.assets ?? []) console.log(asset.mime, asset.url);
} else if (task.state === "queued" || task.state === "running") {
console.log("Still working. On hold: $" + task.cost);
} else {
console.log(`Finished as ${task.state}: ${task.errorCode}`);
}waitForTask(taskId, options?)
タスクが終わるまで繰り返し確認し、終わったらそのタスクを返します。プログラムを再起動したあとなど、以前に作成したタスクに対して使います。
waitForTask(taskId: string, options?: WaitForTaskOptions): Promise<TaskRecord>オプションは timeoutMs(既定値 600000、最大 86400000)、intervalMs、onUpdate、signal で、いずれも run で説明したとおりです。
state が succeeded、failed、expired、canceled のいずれかになった時点で返ります。成功したタスクの場合は、それに加えて pending の結果ファイルがなくなるまで待ちます。確認 1 回ごとに getTask リクエストが 1 回送られ、アカウントのリクエスト上限に数えられます。既定の間隔が時間とともに延びていくのは、このためです。
お金: かかりません。エラー: timeoutMs を使い切ると SpicyWaitTimeoutError になります。このエラーは taskId、timeoutMs、lastTask(最後に見た状態)を持っています。タスクはキャンセルされていないので、そのままもう一度待てば大丈夫です。ほかに SpicyApiError(たとえば 404)と SpicyTransportError。
const task = await client.waitForTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3", {
timeoutMs: 30 * 60_000,
onUpdate: (update) => console.log(new Date().toISOString(), update.state),
});
console.log(task.state, task.output?.assets?.[0]?.url);retryTask(taskId, options?)
失敗したタスクを、同じモデルと同じ入力で、まったく新しいタスクとしてもう一度実行します。元のタスクはそのまま残り、新しいタスクは sourceTaskId で元のタスクを指します。
retryTask(taskId: string, options?: { idempotencyKey?: string; signal?: AbortSignal }): Promise<RetryTaskResult>使えるのは failed と expired のタスク(と古い canceled のタスク)です。成功したタスクや、まだ実行中のタスクは 409 で拒否されます。価格、残高、キーの権限はあらためて確認され、新しいタスクは元のタスクの保存期間の設定を引き継ぎます。
入力は変更されずにそのまま使われるので、再試行が役立つのは、失敗が一時的なものだった場合だけです。まず元のタスクの errorCode を読んでください。rate_limited、upstream_unavailable、timeout、upstream_failed は、少し待ってから 1 回再試行する価値があります。generation_failed と invalid_asset は、リクエストを変えて createTask で送る必要があります。content_rejected、invalid_request、unsupported_combination は、そのまま再試行すべきではありません。よくあるエラーコードとエラーを参照してください。
戻り値は taskId(新しいタスク)、sourceTaskId(元のタスク)、state、estimatedCost、deadlineAt です。
お金: かかります。新しいタスクには独自の仮押さえがあり、成功すれば別途課金されます。再試行のコードが 2 回実行されても再試行が 1 回で済むよう、idempotencyKey を渡してください。
エラー: 409(そのタスクは再試行できない)のほか、createTask に記載した、残高・上限・利用可否のエラー。元の入力でアップロードした spicy:// ファイルを使っていた場合、そのファイルは 1 日しか使えません。もう一度アップロードし、createTask を使ってください。
retryTask はネットワークエラー用ではありません
createTask 自体がネットワークエラーで失敗した場合、再試行すべきタスクはまだ存在しません。同じ冪等キーで、createTask をもう一度送ってください。retryTask を使うのは、読み取った state が failed か expired だったタスクに対してだけです。
完全な例は、失敗したタスクを再試行するにあります。
listTasks(options?)
この API キーで作成したタスクを、新しい順に 1 ページずつ一覧します。再起動したあとや、Webhook を受け取り損ねたときに、作業を探すのに役立ちます。
listTasks(options?: ListTasksOptions): Promise<TaskList>プロパティ
型
戻り値は items、hasMore、そして次のページがある場合は nextCursor です。各項目には taskId、model、state、cost、settled、createdAt、deadlineAt、completedAt が入っています。入力、結果、リンクは含まれていません。必要なタスクについては、getTask を呼び出してください。
お金: かかりません。エラー: SpicyApiError(たとえば、日付の範囲が不正なら 400)、SpicyTransportError。
let cursor;
do {
const page = await client.listTasks({
from: "2026-09-01", // どのページでも同じ日付を使う
to: "2026-09-08",
state: "failed",
limit: 100,
cursor,
});
for (const item of page.items) console.log(item.taskId, item.model, item.createdAt);
cursor = page.hasMore ? page.nextCursor : undefined;
} while (cursor);getBalance()
アカウントにいくらお金があるかを示します。
getBalance(options?: { signal?: AbortSignal }): Promise<Balance>| フィールド | 意味 |
|---|---|
available | 新しいタスクに使えるお金です。承認済みの後払い枠を使っているアカウントでは、マイナスになることがあります |
held | 実行中のタスクのために仮押さえされているお金 |
total | available と held の合計 |
funding | 省略されることもある内訳です。前払いの残高、キャンペーンの付与残高(対象モデルと有効期限つき)、後払い枠が入ります。古い応答では省かれていることがあります |
お金: かかりません。タスクのたびに残高を確認する必要はありません。タスクを作成するときに SpicyAPI が確認し、足りなければ 40201 を返します。funding のすべてのフィールドについては、課金を参照してください。
const balance = await client.getBalance();
console.log(`Available: $${balance.available} (on hold: $${balance.held})`);getUsage(options?)
この API キーで使った金額を、日別とモデル別にまとめて報告します。
getUsage(options?: { from?: string; to?: string; signal?: AbortSignal }): Promise<UsageReport>from と to は UTC の日付(YYYY-MM-DD)で、from の日は含まれ、to の日は含まれません。両方省略すると、今日を含む直近 7 日間になります。範囲は最大 92 日です。
| フィールド | 意味 |
|---|---|
totalCalls | 範囲内のタスク数 |
totalSpend | 確定した課金額だけの合計です。まだ仮押さえ中の金額は含みません |
days | 呼び出しがあった日ごとに 1 件ずつ入ります。day、calls、succeeded、failed、spend |
models | モデルごとに 1 件ずつ入ります。model、calls、succeeded、failed、spend |
お金: かかりません。このレポートはほかの読み取りより重いため、1 分あたり 30 リクエストという追加の上限があります。ループの中で呼び出すのではなく、レポートの作成に使ってください。エラー: SpicyApiError(この上限を超えると 429)、SpicyTransportError。
const usage = await client.getUsage({ from: "2026-09-01", to: "2026-09-08" });
console.log(`${usage.totalCalls} tasks, $${usage.totalSpend} spent`);
for (const row of usage.models) console.log(row.model, row.calls, "$" + row.spend);ファイルのアップロード
モデルが画像・動画・音声を入力に取る場合、ファイルへの公開 HTTPS リンクを、入力フィールドに直接入れられます。自分のパソコンにあるファイルは、先にアップロードしてください。spicy:// で始まるアドレスが返ってくるので、代わりにそれを入力フィールドに入れます。アップロードは無料です。
- 画像(JPEG、PNG、WebP、GIF):10 MiB まで。
- 動画(MP4、WebM)と音声(MP3、WAV):90 MiB まで、長さは 600 秒まで。
- モデルによっては、スキーマでもっと低い上限を設けていることがあります。
- アップロードしたファイルを使えるのは 1 日だけで、使えるのはあなたのアカウントだけです。
uploadFile(path, options?)
ディスク上のファイルをアップロードします。
uploadFile(path: string, options?: { contentType?: string; signal?: AbortSignal }): Promise<UploadedFile>ファイルの種類は、拡張子から判定されます。対応する拡張子は .jpg、.jpeg、.png、.webp、.gif、.mp4、.webm、.mp3、.wav です。それ以外のファイル名の場合は、contentType に image/jpeg、image/png、image/webp、image/gif、video/mp4、video/webm、audio/mpeg、audio/wav のいずれかを渡してください。
| 返るフィールド | 意味 |
|---|---|
uri | タスクの入力に入れる spicy:// アドレス |
expiresAt | アドレスが使えなくなる時刻(1 日後) |
fileId、status、bytes、contentType、sha256 | 保存された内容の詳細です。status は ready になります |
width、height、durationSeconds | 該当する場合の、測定された寸法や長さ |
お金: かかりません。エラー: 種類を判定できない場合は TypeError になり、メッセージには認識できる拡張子がすべて並びます(contentType is required unless the file extension is one of: gif, jpeg, jpg, png, webp, mp4, webm, mp3, wav)。パスが間違っている場合は、Node.js の ENOENT エラー。ストレージがバイト列を受け付けなかった場合や、ファイルが許容サイズより大きい場合(413)は、status 付きの SpicyUploadError。SpicyAPI がアップロードを拒否した場合は SpicyApiError。送信に uploadTimeoutMs より長くかかった場合は SpicyTimeoutError。
const file = await client.uploadFile("./photo.jpg");
console.log(file.uri, "can be used until", file.expiresAt);uploadBytes(data, options)
ユーザーがあなたのサーバーに送ってきたファイルなど、すでにメモリ上にあるバイト列をアップロードします。Node.js の Buffer も使えます。
uploadBytes(data: Uint8Array, options: { contentType: string; signal?: AbortSignal }): Promise<UploadedFile>contentType は必須です。戻り値とエラーは、uploadFile と同じです。
const response = await fetch("https://example.com/reference.png");
const bytes = new Uint8Array(await response.arrayBuffer());
const file = await client.uploadBytes(bytes, { contentType: "image/png" });uploadBase64(value, options?)
Base64 のテキストとして手元にあるファイルをアップロードします。data:image/png;base64,iVBOR… のような data URI か、プレーンな Base64 と contentType の組み合わせを渡します。
uploadBase64(value: string, options?: { contentType?: string; signal?: AbortSignal }): Promise<UploadedFile>エンコードとサイズは、まず手元のマシンで確認されます。不正なデータやサイズを超えたデータは、何もアップロードせずに TypeError を投げます。戻り値は uploadFile と同じです。
createUploadUrl と commitUploadedFile
上の 3 つのヘルパーは、この 2 つの低レベルな呼び出しを組み合わせて作られています。これらが必要になるのは、Node.js のプロセス以外の場所からバイト列を送る場合だけです。
createUploadUrl(contentType: string, bytes: number, options?: { signal?: AbortSignal }): Promise<UploadTicket>
commitUploadedFile(fileId: string, options?: { signal?: AbortSignal }): Promise<UploadedFile>createUploadUrlは、fileId、uploadUrl、method(PUT)、headers、maxBytes、expiresAtを返します。uploadUrlにファイルをPUTで送ります。ヘッダーは列挙されたheadersとまったく同じにし、Authorizationヘッダーは付けません。申告したサイズとファイルのサイズが一致している必要があります。commitUploadedFile(fileId)は、保存されたファイルを確認して、spicy://のuriを返します。アップロードしただけで確定していないファイルは使えません。
ブラウザーからのアップロードも含めた全体の流れは、メディアで説明しています。
createDownloadUrl(taskId, key?)
1 つの結果ファイルについて、一時的なダウンロードリンクを新しく作ります。これが必要になることはめったにありません。getTask、waitForTask、run、Webhook には、どの結果にもすぐ使える url がすでに付いています。
createDownloadUrl(taskId: string, key?: string, options?: { signal?: AbortSignal }): Promise<DownloadTicket>ファイルを選ぶには、アセットの key を渡します。省略すると、最初のファイルになります。戻り値は key、url、expiresAt(約 20 分後)です。
お金: かかりません。エラー: ファイルをまだコピーしている間は 409 の SpicyApiError(数秒待ってください)。タスクが成功していない場合や、その key がタスクに属していない場合は 404。
purgeTask(taskId)
終了したタスクの生成メディア、結果ペイロード(保存された結果データ)、プロンプト、その他の入力テキストを、保存期間を待たずに完全に削除します。結果を残しておいてはいけない場合に使います。
purgeTask(taskId: string, options?: { signal?: AbortSignal }): Promise<PurgeTaskResult>| 返るフィールド | 意味 |
|---|---|
contentState | 内容が削除されれば purged です。present は今回何も削除されなかったことを意味するので、あとでもう一度試してください |
purgedAt | 内容が最初に削除された時刻 |
contentRemovedBy | あなたが削除した場合は user |
billingRetained | 常に true です。タスクにかかった金額の記録は残ります |
mediaDeletionPending | 保存されたファイルをバックグラウンドで削除している間は true です。通常は 1 分もかかりません |
お金: かかりません。そして、これは返金ではありません。台帳の記録、課金額、モデル、状態、タイムスタンプ、リクエスト ID は記録として残ります。削除は元に戻せません。同じタスクに対してもう一度呼び出しても安全です。エラー: タスクがまだ queued か running なら 400 の SpicyApiError、存在しなければ 404。受理されたタスクはキャンセルできず、キャンセル用の API もありません。waitForTask で終わるのを待ってから破棄してください。データ保存と破棄を参照してください。
const result = await client.purgeTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3");
console.log(result.contentState, "billing kept:", result.billingRetained);Webhook 用の関数
verifyWebhook と computeWebhookSignature は、クライアントのメソッドではなく通常の関数です。実際に動く例とあわせて、Webhook で説明しています。
オフラインで使えるヘルパーと型
これらは、ネットワーク接続もキーもなしで動きます。
| エクスポート | 得られるもの |
|---|---|
searchDocumentation(query, limit) | このドキュメントサイトの、小さな組み込みインデックスを検索します。最大 limit 件(1〜25、既定値 10)を返し、各項目には slug、title、summary、keywords、url が入っています。url は https://docs.spicyapi.ai/docs/<slug> の形です。一致するものがなければ空の配列を返し、query が空なら先頭から項目を返します |
DOCUMENTATION | その組み込みインデックス全体を配列にしたもので、ドキュメントの 1 ページにつき 1 項目です |
readOpenApiContract() | この SDK バージョンに同梱されている OpenAPI 3.1 の仕様を、YAML のテキストとして返します |
DEFAULT_API_BASE_URL、DEFAULT_SERVICE_BASE_URL、DEFAULT_TIMEOUT_MS、DEFAULT_UPLOAD_TIMEOUT_MS、DEFAULT_WAIT_TIMEOUT_MS、DEFAULT_WAIT_INTERVAL_MS、MAX_API_RESPONSE_BYTES | このページで説明している既定値です。MAX_API_RESPONSE_BYTES(4 MiB)より大きい API 応答は拒否されます |
すべてのリクエストと応答の TypeScript 型もエクスポートされています。たとえば import type { TaskRecord, ApiModel, Balance } from "@spicyapi/sdk" のように使えます。
| インポートパス | 含まれるもの |
|---|---|
@spicyapi/sdk | このページのすべて |
@spicyapi/sdk/webhooks | verifyWebhook、computeWebhookSignature、SpicyWebhookVerificationError だけです。Webhook の受信側で使う、小さなインポートです |
@spicyapi/sdk/openapi | 生の OpenAPI 型の paths、components、operations だけです |
Webhook
Webhook を使うと、プログラムが数秒おきに問い合わせる代わりに、タスクが終わったことを SpicyAPI のほうから知らせてくれます。各タスクに、自分のサーバー上のアドレスである callBackUrl を指定しておくと、SpicyAPI が完了したタスクの記録をそこへ送ります。
Webhook を必ず検証しなければならない理由
Webhook のアドレスは公開 URL です。アドレスを推測した人なら誰でも、「タスクが成功しました、ファイルはこちらです」という偽のメッセージを送れてしまいます。コードがそのメッセージを信じれば、顧客に間違ったファイルを届けたり、注文を完了扱いにしたりしかねません。
本物の配信にはすべて、あなたと SpicyAPI だけが知っている秘密の署名キーで署名が付いています。verifyWebhook は、受け取ったバイト列そのものから署名を計算し直し、2 つを比べます。配信時刻も確認するので、攻撃者が本物のメッセージを傍受して、何時間もあとに送り直すこともできません。メッセージに基づいて処理をするのは、検証に通ってからにしてください。
設定する
署名キーを取得する
署名キーは登録時に自動で作成されますが、その値は表示されないため、コピーできるキーを新しく作る必要があります。コンソールで Webhook ページを開き、発行するをクリックします(カードにキーが設定済みと表示されている場合は再発行をクリックします)。新しいキーは一度しか表示されません。コピーして、サーバーに SPICY_WEBHOOK_SECRET として保存してください。なくした場合は、もう一度再発行します。
再発行には猶予期間がありません。古いキーはすぐに署名に使われなくなり、再試行中の配信も含めて、以降はすべて新しいキーで署名されます。再発行したら、動いている受信側をすべてすぐに更新してください。詳しくは Webhook を参照してください。
公開 HTTPS アドレスで受信側を動かす
SpicyAPI が呼び出せるのは、インターネットから到達でき、https:// で始まるアドレスだけです。平文の http://、localhost、プライベートネットワークのアドレスは拒否されます。開発中は、トンネルサービスを使ってローカルサーバーに公開 HTTPS アドレスを割り当てるか、デプロイするまではポーリングを使ってください。コールバック URL の詳しいルールは、Webhook にあります。
タスクと一緒に callBackUrl を送る
createTask に渡すリクエストに、callBackUrl: "https://your-app.example.com/hooks/spicy" を追加します。ポーリングの代わりに Webhook で結果を受け取るを参照してください。
最小限の受信側
この受信側は、Node.js に組み込まれている http モジュールだけを使うので、追加でインストールするものはありません。receiver.mjs として保存し、node receiver.mjs で起動してください。
import { createServer } from "node:http";
import { SpicyWebhookVerificationError, verifyWebhook } from "@spicyapi/sdk/webhooks";
const secret = process.env.SPICY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set SPICY_WEBHOOK_SECRET first");
// 処理済みの配信 ID。本番環境では、再起動しても一覧が消えないよう、
// データベースのテーブルを使ってください。
const handled = new Set();
const server = createServer(async (request, response) => {
if (request.method !== "POST" || request.url !== "/hooks/spicy") {
response.writeHead(404).end();
return;
}
if (request.headers["x-webhook-payload-version"] !== "2") {
response.writeHead(400).end();
return;
}
// 1. 本文を生のバイト列のまま読み取ります。検証する前にパースしないでください。
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const rawBody = Buffer.concat(chunks);
// 2. 署名とタイムスタンプを確認します。
let event;
try {
event = verifyWebhook({
rawBody,
secret,
timestamp: String(request.headers["x-webhook-timestamp"] ?? ""),
signature: String(request.headers["x-webhook-signature"] ?? ""),
payloadVersion: 2,
});
} catch (error) {
const reason = error instanceof SpicyWebhookVerificationError ? error.reason : String(error);
console.warn("Rejected a webhook:", reason);
response.writeHead(401).end();
return;
}
// 3. すぐに応答します。SpicyAPI は最大 15 秒待ち、それを過ぎると再試行します。
response.writeHead(204).end();
// 4. 同じ配信が複数回届くことがあります。1 件につき 1 回だけ処理します。
if (!event.deliveryId || handled.has(event.deliveryId)) return;
handled.add(event.deliveryId);
if (!("data" in event.payload)) return; // バージョン 2 の本文では、タスクは data の下にあります
const task = event.payload.data; // getTask() が返すのと同じフィールド
console.log("Task", task.taskId, "is", task.state);
if (task.state === "succeeded") {
console.log("Result:", task.output?.assets?.[0]?.url);
}
});
server.listen(8080, () => console.log("Listening on http://localhost:8080/hooks/spicy"));ここで大事なポイントは次のとおりです。
- 生のバイト列を使う。 署名は、SpicyAPI が送ったバイト列そのものに対して計算されています。フレームワークが先に JSON をパースし、それをテキストに戻すと、バイト列が変わってしまい、確認はすべて失敗します。Express では、このルートに
express.raw({ type: "application/json" })を使ってください。 - すぐに応答する。
2xxの応答なら、どれでも「受け取った」という意味になります。15 秒以内に応答がない場合や、ほかのステータスを返した場合、SpicyAPI はその後の数時間にわたって何度か再試行します。時間のかかる処理は、応答したあとに行ってください。 - 重複は起こる。 再試行やネットワークの不調によって、同じメッセージが 2 回届くことがあります。
event.deliveryIdは再試行しても変わらないので、処理済みの ID を覚えておいてください。 - 成功かどうかは
stateで判断する。 失敗したタスクについての配信も、正当な配信です。エンベロープのcodeではなく、必ずtask.stateを読んでください。 - 本番環境では、 検証したイベントをまずデータベースに保存し、保存が成功してから
2xxを返してください。また、自分のアプリケーションが作成していないタスク ID は無視してください。データベースを使う版は、Webhook のガイドにあります。
本番前に受信側をテストする
computeWebhookSignature は本物の署名を作るので、署名付きのテスト配信を自分に送れます。1 つのターミナルで receiver.mjs を動かしたまま、同じ SPICY_WEBHOOK_SECRET を設定した別のターミナルで、次を実行します。
import { computeWebhookSignature } from "@spicyapi/sdk";
const secret = process.env.SPICY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set SPICY_WEBHOOK_SECRET first");
const body = JSON.stringify({
code: 200,
msg: "success",
request_id: "whd_local_test_1",
data: {
taskId: "job_local_test",
state: "succeeded",
cost: "0.01",
settled: true,
output: { assets: [{ url: "https://example.com/result.png" }] },
},
});
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = computeWebhookSignature("job_local_test", timestamp, body, secret);
const response = await fetch("http://localhost:8080/hooks/spicy", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Webhook-Timestamp": timestamp,
"X-Webhook-Signature": signature,
"X-Webhook-Payload-Version": "2",
},
body,
});
console.log("Your receiver answered", response.status); // 204 が返るはずです2 回実行してみてください。2 回目の配信は request_id が同じなので、受信側がタスクを表示するのは 1 回だけです。署名したあとで body を 1 文字でも変えると、受信側は 401 を返します。
verifyWebhook(options)
verifyWebhook(options: VerifyWebhookOptions): VerifiedWebhookプロパティ
型
戻り値は、payload(パースした本文。バージョン 2 では、タスクは payload.data にあります)、payloadVersion、taskId、deliveryId(バージョン 2 の request_id で、再試行しても変わりません)、timestamp です。この関数は同期関数なので、await は不要です。
検証に失敗すると、SpicyWebhookVerificationError を投げます。その reason は invalid_signature、stale_timestamp、invalid_timestamp、invalid_json、invalid_payload、body_too_large、invalid_secret のいずれかです。401 を返し、そのメッセージに基づく処理は何もしないでください。
computeWebhookSignature(taskId, timestamp, rawBody, secret) は、本文に対する Base64 の署名を返します。必要になるのは、上のようなテストのときだけです。ほかの言語で実装するための完全な署名アルゴリズムは、Webhook にあります。
TypeScript のヒント: payload の型は、2 つの形式のどちらでもありうるものになっています。受信側の例のように "data" in event.payload で絞り込むか、期待する型を渡してください。型を渡す場合は、import type { components } from "@spicyapi/sdk" をしたうえで verifyWebhook<components["schemas"]["TaskRecordEnvelope"]>(…) と書きます。
エラーの扱い方
うまくいかないケースには 2 種類あり、それぞれ扱い方が違います。
- 呼び出しが失敗する。 SDK がエラーを投げます。キーが間違っていた、残高が足りなかった、ネットワークが切れた、といった場合です。
try/catchで捕まえてください。 2.タスクが失敗する。 呼び出しはうまくいったものの、生成がうまくいかなかった場合です。例外は投げられず、返ってきたタスクにstate: "failed"とerrorCodeが付いています。失敗・期限切れのタスクでは通常、仮押さえ額が全額解放されます。ただし、モデルページに明記された一部のコンテンツ拒否は、受理時の見積額で課金されます。最終請求額はsettledとcostで確認してください。
エラークラス
| クラス | どんなときに出るか | 役立つフィールド |
|---|---|---|
SpicyApiError | キーの拒否や残高不足など、SpicyAPI がエラーを返した | code(40201 など、対応を決めるための業務コード)、status(HTTP ステータス)、message、requestId、retryAfterSeconds、rateLimitLimit、rateLimitRemaining |
SpicyTransportError | 使える応答が届かなかった(ネットワーク障害、DNS の問題、4 MiB を超える応答) | message、cause(元になったエラー) |
SpicyTimeoutError | 1 回のリクエストが timeoutMs(または uploadTimeoutMs)より長くかかった。SpicyTransportError の一種です | timeoutMs |
SpicyWaitTimeoutError | run や waitForTask が時間切れになった。タスクはまだ実行中です | taskId、timeoutMs、lastTask |
SpicyUploadError | アップロードしたファイルをストレージが受け付けなかった、またはファイルが大きすぎた | status |
SpicyWebhookVerificationError | Webhook の検証に失敗した | reason |
TypeError | 呼び出しそのものが不正だった(キーが設定されていない、オプションが範囲外、ID が空など)。何も送られていません | message |
どう対応するかは、message ではなく必ず code で決めてください。メッセージは人が読むためのもので、言い回しは変わることがあり、アカウントのエラー言語の設定にも従います。サポートに問い合わせるときは、必ず requestId を添えてください。それがあれば、こちらで該当するリクエストを正確に特定できます。
import { randomUUID } from "node:crypto";
import {
SpicyApiError,
SpicyClient,
SpicyTransportError,
SpicyWaitTimeoutError,
} from "@spicyapi/sdk";
const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // listModels() で取得した実際の ID
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");
const idempotencyKey = randomUUID(); // 保存しておくこと:もう一度試すときに同じキーが必要です
try {
const task = await client.run(
{ model: MODEL_ID, input: { prompt: "A paper lantern on a rainy street" } },
{ idempotencyKey, onAccepted: ({ taskId }) => console.log("Task ID:", taskId) },
);
if (task.state === "succeeded") console.log("Done:", task.output?.assets?.[0]?.url);
else console.log(`Finished as ${task.state} (${task.errorCode}). Cost: ${task.cost}; settled: ${task.settled}.`);
} catch (error) {
if (error instanceof SpicyWaitTimeoutError) {
// 終わったのは待機だけで、タスクは終わっていません。あとで同じ ID で確認します。
console.log(`Still running: ${error.taskId}. Call waitForTask() again later.`);
} else if (error instanceof SpicyApiError) {
console.log(`SpicyAPI said no: ${error.code} (HTTP ${error.status}) ${error.message}`);
console.log(`Quote this if you contact support: ${error.requestId}`);
if (error.code === 40201) console.log("Your balance is too low. Top up in the console.");
if (error.code === 40202) console.log("A spend cap was reached. Raise it on the API keys page.");
if (error.code === 429) console.log(`Too many requests. Wait ${error.retryAfterSeconds ?? 1} s.`);
} else if (error instanceof SpicyTransportError) {
// ネットワーク障害かローカルのタイムアウト:タスクはできているかもしれないし、いないかもしれません。
// 安全に確かめるには、同じリクエストを「同じ」idempotencyKey でもう一度実行します。
console.log(`Network problem (${error.message}). Retry with key ${idempotencyKey}.`);
} else {
throw error; // ここでの TypeError は、キーの設定漏れなど、呼び出し自体の不具合です
}
}よくあるエラーコード
error.code | 意味 | 再試行する価値は? |
|---|---|---|
400 | 不正なリクエスト。たいていは、モデルが受け付けない入力フィールドがある | ありません。リクエストを直してください |
401 | キーがない、形式が正しくない、失効している、または有効期限が切れている | ありません。キーを確認してください |
40201 | 残高が仮押さえ額に足りない | ありません。先にチャージしてください |
40202 | 利用上限に達した | ありません。上限を引き上げるか、日次または月次のリセットを待ってください |
40301 | このキーでは、このモデルを使えない | ありません。キーのモデル一覧を変更してください |
404 | このキーから見えるモデルやタスクがない | ありません |
409 | 現在の状態と矛盾している。たとえば、冪等キーを別の作業に使い回した | 場合によります。エラーを参照してください |
40901 | 見積もりの期限切れ、または価格の変更 | もう一度見積もりを取ってから、確認してください |
429 | リクエストが多すぎる | あります。retryAfterSeconds だけ待ってから。読み取り系は、SDK がすでに自動で再試行しています |
500 | SpicyAPI 側で何かが壊れた | あります。間をおいてから |
50301 | モデルがいま利用できない | あります。時間をおいてから試すか、別のモデルを選んでください |
タスクが失敗した場合、task.errorCode は invalid_request、unsupported_combination、content_rejected、rate_limited、upstream_unavailable、generation_failed、timeout、invalid_asset、upstream_failed のいずれかです。retryTask は元の入力をそのまま送り直すので、次に何をするかは、コードに応じて決めてください。
task.errorCode | 対処 |
|---|---|
rate_limited、upstream_unavailable、timeout、upstream_failed | 少し待ってから、retryTask を 1 回実行します。timeout が繰り返される場合は、長さを短くするか解像度を下げて、createTask で試してください |
generation_failed | プロンプトや画像を変えて、createTask で新しいタスクを送ります。同じ入力だと、たいてい同じように失敗します |
invalid_asset | ファイルをもう一度アップロードし、新しい uri を使って、createTask で新しいタスクを送ります。再試行では古いファイルが使い回されてしまいます |
invalid_request、unsupported_combination | そのまま再試行してはいけません。設定を変えてから、createTask を使ってください |
content_rejected | そのまま再試行してはいけません。選んだモデルが、そのモデル自身のポリシーに基づいてリクエストを断りました。プロンプトを変えるか、別のモデルを選んでください |
完全な表は、エラーにあります。
結果を手元に残す
- 結果の
urlが使えるのは約 20 分です。新しいリンクが必要なら、もう一度getTaskを呼び出してください。 - 生成されたファイルは、最長でも約 14 日で削除されます。アカウントの設定や
retentionSecondsによっては、もっと早く削除されます。アップロードした入力ファイルは 1 日、保存されたプロンプトは 30 日で削除されます。 - 残しておきたいものは、タスクが成功したらすぐに、自分のストレージへダウンロードしてください。一時的な
urlそのものを、データベースに保存してはいけません。詳しくはデータ保存と破棄を参照してください。
よく使うレシピ
レシピでは、MODEL_ID_FROM_CATALOG を仮の値として使っています。実行する前に、ライブカタログ(listModels() または GET /api/v1/models)から、目的に合い、enabled と available がどちらも true の項目を選び、その model の値に置き換えてください。置き換え忘れた場合は、どのレシピもわかりやすいメッセージを出して止まります。
画像から動画:まず手元の画像をアップロードする
import { randomUUID } from "node:crypto";
import { SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
// ライブカタログにある image-to-video のモデル ID に置き換えてください:
// await client.listModels({ modality: "video", task: "image-to-video" })
const MODEL_ID = "MODEL_ID_FROM_CATALOG";
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");
// 1. 手元の画像をアップロードします。無料です。spicy:// アドレスは 1 日使えます。
const file = await client.uploadFile("./photo.jpg");
console.log("Uploaded:", file.uri);
// 2. モデルのスキーマを表示します:画像を受け取るのはどのフィールドか、必須なのはどのフィールドか。
const { inputSchema } = await client.getModel(MODEL_ID);
console.log(JSON.stringify(inputSchema, null, 2));
// 3. 動画を作成して完了を待ちます。動画は数分かかることがよくあります。
const task = await client.run(
{
model: MODEL_ID,
input: {
prompt: "Slow push-in, light rain, neon reflections on wet stone",
image_url: file.uri, // 上で表示したスキーマにあるフィールド名を使います
},
},
{
idempotencyKey: randomUUID(),
timeoutMs: 30 * 60_000,
onAccepted: ({ taskId }) => console.log("Task ID (save it):", taskId),
},
);
console.log(task.state, task.output?.assets?.[0]?.url ?? task.errorCode);フィールド名と必須フィールドについては、表示されたスキーマが正解です。動画モデルでは長さや解像度が必須のこともあり、価格もそれによって変わることがよくあります。画像がすでに公開 HTTPS アドレスでオンラインにあるなら、アップロードを省いて、そのアドレスを画像のフィールドに直接入れてください。価格を見たい場合は、先に同じリクエストで quoteTask を実行してください。
同時実行数を絞ってバッチを実行する
数百のタスクを一度に開始すると、利用上限やリクエスト上限に引っかかることがあります。このスクリプトは、プロンプトの一覧を、同時に進むタスクが最大 2 つになるように実行します。
import { SpicyApiError, SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // listModels() で取得した実際の ID
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");
// プロンプトを変えたら、必ずバッチ名も変えてください。
const BATCH_NAME = "lanterns-2026-09-17";
const prompts = [
"A paper lantern on a rainy street",
"A paper lantern floating on a lake",
"A paper lantern in a snowy forest",
"A paper lantern above a night market",
];
const CONCURRENCY = 2; // 同時に実行してよいタスクの数
const results = prompts.map((prompt) => ({ prompt, outcome: "not started", detail: "" }));
let nextIndex = 0;
async function worker() {
while (nextIndex < prompts.length) {
const index = nextIndex++;
try {
const task = await client.run(
{ model: MODEL_ID, input: { prompt: prompts[index] } },
{
// バッチ名と位置が同じなら、キーも同じになります。スクリプトが異常終了しても、
// 24 時間以内にもう一度実行すれば、二重に支払うのではなく、
// すでにあるタスクが返ってきます。
idempotencyKey: `${BATCH_NAME}-${index}`,
timeoutMs: 20 * 60_000,
},
);
results[index].outcome = task.state;
results[index].detail = task.output?.assets?.[0]?.url ?? task.errorCode ?? "";
} catch (error) {
results[index].outcome = "error";
results[index].detail = error instanceof Error ? error.message : String(error);
// 残高不足(40201)か利用上限の超過(40202):新しいタスクの開始をやめます。
if (error instanceof SpicyApiError && (error.code === 40201 || error.code === 40202)) {
nextIndex = prompts.length;
}
}
}
}
await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
console.table(results);- キーをバッチ名から作る理由。 「このバッチの 3 番目の項目」に結び付いた冪等キーなら、異常終了のあとに再実行しても、すでに作成されたタスクが使い回されます。実行のたびにランダムなキーを使うと、同じ画像にもう一度お金を払うことになります。
- プロンプトを変えたらバッチ名も変えなければならない理由。 キーを別のリクエストに使い回すと、
409で拒否されます。 CONCURRENCYの選び方。 実行中のタスクはそれぞれ残高の一部を仮押さえし、状態の確認は 1 回ごとにアカウントのリクエスト上限に数えられます。小さい値から始めて、バッチが問題なく動くようになったら引き上げてください。
ポーリングの代わりに Webhook で結果を受け取る
Web アプリやバックグラウンドワーカーでは、リクエストの中で結果を待つのは無駄です。callBackUrl を付けてタスクを作成し、タスク ID を保存しておき、結果が届いたら受信側に処理させましょう。
import { randomUUID } from "node:crypto";
import { SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // listModels() で取得した実際の ID
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");
// 実際のアプリでは、呼び出す前にこのキーを顧客の注文と一緒に保存します。
const idempotencyKey = randomUUID();
const accepted = await client.createTask(
{
model: MODEL_ID,
input: { prompt: "A paper lantern on a rainy street" },
callBackUrl: "https://your-app.example.com/hooks/spicy",
},
{ idempotencyKey },
);
console.log(`Accepted ${accepted.taskId}; $${accepted.estimatedCost} is on hold.`);
// ここで accepted.taskId を保存します。タスクが終わると、SpicyAPI が受信側を呼び出します。配信がいつまでも届かない場合に備えて、安全策も用意しておいてください。たとえば、再試行の間ずっとサーバーが停止していた場合です。ときどき、保存したタスクのうち deadlineAt を大きく過ぎても Webhook が届いていないものについて getTask を呼び出すか、listTasks でページをめくって確認します。Webhook の配信履歴と、手動で再配信するボタンは、コンソールの Webhook ページにあります。
失敗したタスクを再試行する
import { SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
const taskId = process.argv[2]; // 実行方法:node retry.mjs job_...
if (!taskId) throw new Error("Usage: node retry.mjs <taskId>");
// 再試行は元の入力をそのまま送り直すので、価値があるのは一時的な失敗のときだけです。
// それ以外のコードでは、リクエストを変えて createTask を使ってください。
const WORTH_ONE_RETRY = new Set(["rate_limited", "upstream_unavailable", "timeout", "upstream_failed"]);
const source = await client.getTask(taskId);
if (
(source.state !== "failed" && source.state !== "expired") ||
!WORTH_ONE_RETRY.has(source.errorCode ?? "")
) {
console.log(`Not retrying: the task is ${source.state} (${source.errorCode ?? "no error code"}).`);
} else {
// 再試行 1 回ごとに固定のキーを使います。このスクリプトを 2 回実行しても、再試行は 2 回ではなく 1 回です。
const retry = await client.retryTask(source.taskId, {
idempotencyKey: `retry-${source.taskId}-attempt-1`,
});
console.log(`New task ${retry.taskId}; $${retry.estimatedCost} is on hold.`);
const finished = await client.waitForTask(retry.taskId, { timeoutMs: 20 * 60_000 });
console.log("The retry finished as", finished.state);
}このスクリプトが再試行するのは、次は結果が変わりうる失敗、つまりサービスが混雑しているか一時的に使えない場合、タイムアウトした場合、原因不明のエラーの場合だけです。しかも、固定の冪等キーで 1 回だけ再試行します。invalid_asset の場合は、再試行だと期限切れのアップロードが使い回されてしまうので、ファイルをもう一度アップロードして新しいタスクを作成してください。generation_failed の場合は、プロンプトや画像を変えて新しいタスクを作成します。content_rejected、invalid_request、unsupported_combination の場合は、再試行するのではなく、リクエストを変えてください。その 1 回の再試行も失敗したら、いったん止めてエラーを確認してください。timeout が繰り返される場合は、長さを短くするか解像度を下げて、createTask で試します。エラーを参照してください。
トラブルシューティングとよくある質問
ソースコード
このパッケージは MIT ライセンスで、github.com/spicyapi-ai/spicy-sdk で公開のまま開発しています。このページで説明したメソッドの実装を読む、書かれている動作と食い違う点を報告する、変更を提案する——いずれもそこからどうぞ。
次のステップ
モデルカタログとスキーマ
モデルの inputSchema の読み方と、有効な入力の選び方
非同期タスク
タスクの状態、ポーリング、各フィールドの意味
Webhook
配信の形式、再試行のスケジュール、ほかの言語での検証方法
メディア
アップロード、Base64 の上限、結果のダウンロード
エラー
すべてのエラーコードと、再試行する価値があるもの
課金
仮押さえ、精算、利用上限、残高
冪等性
二重に支払わずに、安全に再試行する
コマンドラインツール(CLI)
コードを書かずに、同じ機能をターミナルから使う
MCP サーバー
AI コーディングアシスタントに、SpicyAPI を代わりに使ってもらう
Python SDK
同じワークフローを Python から。実行時の依存パッケージはありません
Go SDK
同じワークフローを Go から。標準ライブラリだけで動きます
PHP SDK
同じワークフローを PHP から。ライブラリの依存はありません
Java SDK
同じワークフローを Java から。sealed なエラー分類付き

