PHP SDK
SpicyAPI 公式の PHP SDK(spicyapi/spicyapi)のガイドです。Composer での導入、見積もりと最初の生成、ローカルファイルのアップロード、安全な Webhook 検証、すべてのエラーコードの扱い方までをまとめています。
spicyapi/spicyapi は、SpicyAPI 公式の PHP SDK です。画像・動画・音声の生成を、1 つの SpicyClient の後ろにまとめます。API キーを付ける、応答のエンベロープをほどいてただの連想配列にする、上限付きのバックオフでタスクを待つ、ローカルファイルを 1 回の呼び出しでアップロードする、そして PHP のコードがそこで犯しがちな 3 つの間違いを 1 つも踏まずに Webhook の署名を検証する。こうした作業を、すべて引き受けます。
必要なのは PHP 8.1 以降と、ext-json、ext-hash です。ライブラリの依存はありません。 ext-curl があれば使いますが、なければ https:// のストリームラッパーにフォールバックします。
このツールで合っている?
| やりたいこと | 使うもの |
|---|---|
| 自分の PHP のコードから SpicyAPI を呼び出す | この SDK |
| Node.js や TypeScript から呼び出す | TypeScript SDK |
| Python、Go、Java から呼び出す | Python、Go、Java の各 SDK |
| コードを書かずに、ターミナルから生成する | CLI |
| AI アシスタントに、SpicyAPI を代わりに呼び出してもらう | MCP サーバーと Agent Skill |
| チャットモデルやテキストモデルを使う | OpenAI、Anthropic、Gemini 形式に対応した、定評のある PHP 用クライアント。テキストとストリーミングを参照 |
インストール
composer require spicyapi/spicyapiキーは環境変数に入れてください。そうすれば、コードにもバージョン管理にも残りません。
export SPICY_API_KEY="sk-spicy-..."最初の結果
価格を尋ねて、判断して、タスクを作成し、待って、ファイルを読み取ります。見積もりは無料で、何も確保しません。
use SpicyApi\SpicyClient;
$client = new SpicyClient(); // 環境変数から SPICY_API_KEY を読み取ります
$quote = $client->quote('MODEL_ID_FROM_CATALOG', ['prompt' => 'a lantern in fog']);
printf("at most %s\n", $quote['maxCharge']); // お金を使う前に判断します
$task = $client->createTask(
model: $quote['model'],
input: ['prompt' => 'a lantern in fog'],
idempotencyKey: SpicyClient::idempotencyKey(),
quoteId: $quote['quoteId'],
expectedCost: $quote['estimatedCost'],
);
$final = $client->waitForTerminal($task['taskId']);
foreach (SpicyClient::readyAssets($final) as $asset) {
echo $asset['url'], "\n";
}どの呼び出しも、応答のエンベロープの data をデコードしたものを、ただの連想配列として返します。読むときは $quote->maxCharge ではなく $quote['maxCharge'] です。
MODEL_ID_FROM_CATALOG は仮の値です。実際の ID は、認証付きのカタログ($client->listModels()、つまり GET /api/v1/models)から取得してください。記憶や古い Web ページから写してはいけません。モデルは追加・一時停止・終了されるからです。入力は、そのモデル自身の inputSchema(listModels() が返します)に従って組み立ててください。受け付けるフィールドは、モデルごとに違います。
金額は米ドルの十進文字列で、浮動小数点数ではありません。2 進数の浮動小数点数では、ほとんどの十進の金額を正確に保持できないからです。比較には SpicyClient::compareUsd() を使ってください。この API には、クレジットやポイントの仕組みはどこにもありません。
waitForTerminal は終端状態のどれかに達した時点で戻りますが、終端状態は成功した状態という意味ではありません。出力を読む前に状態を確認してください。また、ファイルではなくテキストで応答するエンドポイントでは、先に SpicyClient::outputText($final) を読んでください。
最初のポーリングを省く
waitSeconds は、タスクが終わるまで接続を保ちます。短い処理なら、ポーリングのループはまったく要りません。ただし、タスクが終わっていることを約束するものではありません。 待ち時間を使い切ると、代わりにいつもの「受理された」応答が返ります。形は同じで、違うのは state だけです。ですから、パラメーターを渡したという事実ではなく、必ず状態で分岐してください。
$submitted = $client->createTask(
model: 'MODEL_ID_FROM_CATALOG',
input: ['prompt' => 'a lantern in fog'],
idempotencyKey: SpicyClient::idempotencyKey(),
waitSeconds: 30,
);
if (!SpicyClient::isTerminal($submitted)) {
$submitted = $client->waitForTerminal($submitted['taskId']);
}ローカルのファイルから始める
画像から動画を作る、被写体を差し替える、画像を編集する。どれも、まず素材を当社側に置く必要があります。uploadFile は、チケットの発行、ストレージへの PUT、確定という 3 つのステップを 1 回の呼び出しで行い、spicy:// の URI を返します。入力に入れるのは、この URI です。
$uploaded = $client->uploadFile('/path/to/reference.png');
$task = $client->createTask(
model: 'MODEL_ID_FROM_CATALOG',
input: ['image' => $uploaded['uri'], 'prompt' => 'slow dolly in'],
idempotencyKey: SpicyClient::idempotencyKey(),
);公開されている HTTPS の URL なら、アップロードは一切必要なく、そのまま入力に入れられます。画像は 10 MiB まで、音声と動画は 90 MiB までです。メディアを参照してください。
Webhook
use SpicyApi\SpicyWebhook;
// 最初の引数はシークレットです。ほかは省略できます。フレームワークがすでに読み取って
// いない限り、$_SERVER と php://input の生のバイト列は、こちらで読み取ります。
$delivery = SpicyWebhook::verifyRequest(getenv('SPICY_WEBHOOK_SECRET') ?: '');
// ボディをバッファリングするフレームワークの中では、正確なバイト列を自分で渡します。
$delivery = SpicyWebhook::verifyRequest(
secret: getenv('SPICY_WEBHOOK_SECRET') ?: '',
server: $_SERVER,
rawBody: $request->getContent(),
);PHP のコードがここで犯しがちな間違いが 3 つあります。このクラスは、そのいずれも避けています。
- 検証の前にボディを組み直してしまう。
json_decodeしてからjson_encodeするとバイト列が変わり、署名は二度と一致しません。検証するのは生のバイト列です。 - 署名を
==や===で比較してしまう。 処理時間の問題に加えて、==は両方が0e…の形をした文字列を数値として比較します。"0e123" == "0e456"はtrueになるのです。このクラスはhash_equalsを使います。 - タイムスタンプの確認を省いてしまう。 正しい署名は、いつまでも正しいままです。有効期間の確認がなければ、盗み取られた配信をあとから再生できてしまいます。
配信は再試行されるので、配信 ID で重複を取り除いてください。配信の形式と再試行のスケジュールは、Webhook にすべて書いてあります。
残高・利用状況・履歴
$balance = $client->getBalance(); // available / held / total、いずれも米ドルの文字列
$usage = $client->getUsage('2026-09-01', '2026-09-08'); // [from, to) の期間に精算された支出
foreach ($client->iterTasks(state: 'succeeded') as $summary) {
echo $summary['taskId'], "\n"; // 全ページ。フィルターは自動で繰り返されます
}held は、まだ精算されていないタスクが仮押さえしている金額です。使われてもいなければ、使えるわけでもありません。日付は UTC の暦日(YYYY-MM-DD)で、半開区間 [from, to) を作ります。長さは最大 92 日です。この 3 つの呼び出しはいずれも、呼び出しに使った API キーの分しか見えません。同じアカウントのほかのキーや、コンソールからの生成は含まれません。
getUsage() は突き合わせのための機能であって、進捗を見るためのものではありません。この呼び出しには、すべてのキーで共有されるアカウント単位のレート予算が付いていて、その制限はフェイルクローズで働きます。つまり、ポーリングすると、アカウント全体が自分の利用状況を読めなくなることがあります。1 件のタスクを追うには、代わりに getTask() を使ってください。
listTasks() は 1 ページ分を {items, hasMore, nextCursor} の形で返します。自分でページを送るなら、すべてのフィルターを変えずに繰り返し、動かすのはカーソルだけにして、hasMore が false になったときまたは nextCursor が空になったときに止めてください。hasMore しか見ないと、サーバー側の一時的な不具合が、課金される呼び出しの終わらないループになります。しかも、見た目はただのハングアップです。iterTasks() は、そのすべてを代わりに行います。
エラー
失敗は SpicyApiError として送出され、$businessCode、$requestId、$retryAfterSeconds を持ちます。分岐は必ずビジネスコードで行い、メッセージでは判断しないでください。メッセージは翻訳されますが、コードは変わりません。
HTTP 503 は、3 つの異なるビジネスコードで共有されています。 そのため、ステータスだけでは、次に何をすべきかは分かりません。
| コード | 意味 | 対処 |
|---|---|---|
40003 | アップロードされたバイト列が、チケットと一致しない | チケットを取り直して、最初からやり直します |
40004 | そのパラメーターの組み合わせに対応できる構成がない | メッセージに書かれているパラメーターを変更します |
40201 | 残高が足りない | チャージします |
409 | 同じ冪等キーが、別のリクエストに使い回された | そのキーでは元のリクエストを送り直すか、変更したほうを新しいキーで送ります |
40901 | タスクが作成される前に価格が変わった | 見積もりを取り直します。冪等キーは同じものを使ってください |
503 | 依存している何かが、短時間だけ利用できない | Retry-After の秒数だけ待ってから再試行します |
50301 | いま使える構成も価格もない | カタログを取り直します |
50302 | 同期的な生成が上流で失敗し、返金された | 新しい冪等キーで再試行します |
$error->guidance() は、実行時にこれと同じ助言を返します。すべてのコードはエラーに一覧があります。
お金を守る 2 つのこと
1 回の送信につき、冪等キーは 1 つに保ち、送り直すときは毎回同じキーを使ってください。応答が失われたことは、タスクが作成されなかったことの証明にはなりません。冪等性を参照してください。
成功したタスクは課金されます。 結果が期待外れでも同じです。quote() は何も確保せず、何も作成しません。
このパッケージが扱わないこと
- テキストモデル。 OpenAI、Anthropic、Gemini のリクエスト形式に対応しています。それらに対応した定評のある PHP 用クライアントが、
https://api.spicyapi.ai/v1に対してそのまま動きます。テキストとストリーミングを参照してください。 - ブラウザー側のコード。 キーが置かれるべき場所は自分のサーバーであって、訪問者に配るものの中ではありません。
- モデルを選んだり、そのフィールドを埋めたりすること。 SDK が渡すのは、カタログと各モデルのスキーマです。何を送るかは、あなたが決めます。
- タスクのキャンセル。 いったん受理されたタスクは、最後まで実行されます。
ソースコード
このパッケージは MIT ライセンスで、github.com/spicyapi-ai/spicy-php で公開のまま開発しています。README は、クライアント全体をいちばん短く見渡せる資料で、examples/ にはそのまま実行できるスクリプトがあります。このページは、その周りにある考え方を補います。

