spicyapiドキュメント
SDK
本文

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 -ypackage.json を作ります。プロジェクトの設定と、使うライブラリを記録するファイルです
npm pkg set type=moduleファイルが新しい import 構文を使うことを Node.js に伝えます。この SDK にはこの設定が必要です
npm install @spicyapi/sdkSDK を 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 キーを作成する

  1. spicyapi.ai にログインします。まだアカウントがない場合は、spicyapi.ai/register で登録してください。新規登録を一時停止している場合は、同じページからウェイトリストに登録できます。
  2. コンソールの API キーページを開き、キーを作成をクリックします。必須なのは名前だけです。
  3. すぐにキーをコピーしてください。キーは 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 という名前のファイルを作り、次の内容を貼り付けて保存します。

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.mjs
SpicyAPI 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 つ作成し、見積額を仮押さえして、画像の生成に成功した場合に課金します。

first-result.mjs
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 --yes
Model: <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>

各ステップで何をしているか

  1. getStatus() は、SpicyAPI のヘルスチェック用エンドポイントに、サービスが稼働しているかを問い合わせます。ネットワークの問題で例外を投げることはなく、代わりに operational: false を返します。
  2. listModels() は、あなたのアカウントの価格が入ったライブのモデルカタログを返します。使うのは、enabled と available がどちらも true の項目だけにしてください。includeExamples: true を付けると、サーバーが検証済みの入力例が追加されます。これがいちばん安全な出発点です。あとで送る値は、その項目の model フィールド、つまりモデル ID です。
  3. 入力。 受け付けるフィールドはモデルごとに異なり、モデルが知らないフィールドを送ると 400 エラーで拒否されます。検証済みの入力例をコピーしてプロンプトだけを変えれば、これを避けられます。モデルが受け付けるすべてのフィールドを見るには、getModel() を呼び出して inputSchema を読んでください。
  4. quoteTask() は、このリクエストそのものの価格を計算します。何も作成せず、何も確保しません。金額は "0.04" のように文字列で書かれた米ドルなので、丸め誤差が入り込むことはありません。
  5. run() はタスクを作成し、終わるまで様子を確認し続けます。quoteId と expectedCost を渡すのは、「表示された価格のときだけ受理してほしい」という意味です。価格が変わっていれば、別の金額で課金されるのではなく、呼び出しが 40901 で失敗します。idempotencyKey があれば、ネットワークエラー後の再試行も安全です。実際のアプリでは、再起動したプログラムが同じキーを使い回せるよう、呼び出す前にキーを保存しておいてください。onAccepted は、タスクができた瞬間にタスク ID を渡してくれます。ここで ID を保存しておけば、プログラムが止まっても、あとからタスクを確認できます。 6.タスクの状態。 run() は、失敗したタスクでも普通に値を返します。state を確認してください。結果があるのは succeeded だけです。failed と expired は errorCode で理由がわかり、課金されません。失敗・期限切れのタスクでは通常、仮押さえ額が全額解放されます。ただし、モデルページに明記された一部のコンテンツ拒否は、受理時の見積額で課金されます。最終請求額は settled と cost で確認してください。
  6. ダウンロード。 結果には、それぞれ一時的な 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(の作成ステップ)、retryTaskidempotencyKey を渡した場合のみです。渡さない場合、ネットワークエラーはそのままあなたに投げられます。送り直すと、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 が入ります
readiness2 つ目のチェックの結果で、形は同じです。公開サービスでは 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 を、そのまま渡してください。

返るフィールド意味
quoteIdcreateTask か 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 を使ってください。メッセージの言い回しは変わることがあります
costsettled が false の間は仮押さえ中の金額、true になると最終的な課金額
settledcost が確定しているかどうか
createdAt、completedAt、deadlineAt作成された時刻、終了した時刻、期限切れになる時刻
input保存されている入力です。プロンプトの保存期間が過ぎると削除されます
sourceTaskIdretryTask で作成されたタスクに付き、元のタスクを指します
contentStatepresent、expired(保存期間のルールで削除された)、purged(purgeTask で自分で削除した)のいずれか
retentionファイル(outputsExpireAt)とプロンプト(promptsExpireAt)が削除される時刻

output.assets の各項目:

フィールド意味
url一時的なダウンロードリンクです。API キーは付けずにダウンロードします
expiresAtリンクが使えなくなる時刻で、通常は約 20 分後です。新しいリンクが必要なら、getTask をもう一度呼び出します
mimeimage/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実行中のタスクのために仮押さえされているお金
totalavailable と 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>
  1. createUploadUrl は、fileId、uploadUrl、method(PUT)、headers、maxBytes、expiresAt を返します。
  2. uploadUrl にファイルを PUT で送ります。ヘッダーは列挙された headers とまったく同じにし、Authorization ヘッダーは付けません。申告したサイズとファイルのサイズが一致している必要があります。
  3. 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/webhooksverifyWebhook、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 で起動してください。

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 を設定した別のターミナルで、次を実行します。

send-test.mjs
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 種類あり、それぞれ扱い方が違います。

  1. 呼び出しが失敗する。 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(元になったエラー)
SpicyTimeoutError1 回のリクエストが timeoutMs(または uploadTimeoutMs)より長くかかった。SpicyTransportError の一種ですtimeoutMs
SpicyWaitTimeoutErrorrun や waitForTask が時間切れになった。タスクはまだ実行中ですtaskId、timeoutMs、lastTask
SpicyUploadErrorアップロードしたファイルをストレージが受け付けなかった、またはファイルが大きすぎたstatus
SpicyWebhookVerificationErrorWebhook の検証に失敗したreason
TypeError呼び出しそのものが不正だった(キーが設定されていない、オプションが範囲外、ID が空など)。何も送られていませんmessage

どう対応するかは、message ではなく必ず code で決めてください。メッセージは人が読むためのもので、言い回しは変わることがあり、アカウントのエラー言語の設定にも従います。サポートに問い合わせるときは、必ず requestId を添えてください。それがあれば、こちらで該当するリクエストを正確に特定できます。

handle-errors.mjs
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 がすでに自動で再試行しています
500SpicyAPI 側で何かが壊れたあります。間をおいてから
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 の値に置き換えてください。置き換え忘れた場合は、どのレシピもわかりやすいメッセージを出して止まります。

画像から動画:まず手元の画像をアップロードする

image-to-video.mjs
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 つになるように実行します。

batch.mjs
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 を保存しておき、結果が届いたら受信側に処理させましょう。

webhook-create.mjs
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 ページにあります。

失敗したタスクを再試行する

retry.mjs
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 で公開のまま開発しています。このページで説明したメソッドの実装を読む、書かれている動作と食い違う点を報告する、変更を提案する——いずれもそこからどうぞ。

次のステップ

このページの内容

SDK とはこのツールで合っている?SDK がしないことこのページで使う言葉できること始める前にNode.js 22.13 以降をインストールするプロジェクトフォルダーを作って SDK をインストールするAPI キーを作成するキーを環境変数に入れる最初のファイルを実行する最初の結果を得る各ステップで何をしているかAPI リファレンスクライアントを作る自動再試行signal で呼び出しを止めるgetStatus()listModels(options?)getModel(modelId)quoteTask(request)createTask(request, options?)run(request, options?)getTask(taskId)waitForTask(taskId, options?)retryTask(taskId, options?)listTasks(options?)getBalance()getUsage(options?)ファイルのアップロードuploadFile(path, options?)uploadBytes(data, options)uploadBase64(value, options?)createUploadUrl と commitUploadedFilecreateDownloadUrl(taskId, key?)purgeTask(taskId)Webhook 用の関数オフラインで使えるヘルパーと型WebhookWebhook を必ず検証しなければならない理由設定する署名キーを取得する公開 HTTPS アドレスで受信側を動かすタスクと一緒に callBackUrl を送る最小限の受信側本番前に受信側をテストするverifyWebhook(options)エラーの扱い方エラークラスよくあるエラーコード結果を手元に残すよく使うレシピ画像から動画:まず手元の画像をアップロードする同時実行数を絞ってバッチを実行するポーリングの代わりに Webhook で結果を受け取る失敗したタスクを再試行するトラブルシューティングとよくある質問ソースコード次のステップ