キーを作り、Bearerトークンとして送る
キーはマイページの「APIキー」で作成します。完全なキーが表示されるのは作成時のレスポンスだけで、以降キー一覧を読んでも返るのはID、名前、プレフィックスのみ、秘密の値は二度と出ません。すぐに保存してください。プレフィックスは、キー自体を含めずにログ上でキーを特定するためにあります。
すべてのリクエストで、AuthorizationヘッダーにBearerという語とキーを続けて入れます。クエリ文字列で渡す形式はありません。RFC 6750は推奨事項の中で「Bearer tokens SHOULD NOT be passed in page URLs (for example, as query string parameters)」と述べています。トークンを含むURLは、ブラウザーの履歴、リファラー、サーバーログに残るからです。
キーなし、または失効したキーで送ると、401とコードauthentication_requiredが返ります。エラーはすべて同じ本文で、message、type、param、codeを持つerrorオブジェクトです。分岐はcodeで行いましょう。messageは人が読むための文で、言い回しは変わります。
カタログはハードコードせずに読む
GET /v1/modelsはimageとvideoの二つのリストを返します。画像の項目には受け付けるサイズ、それぞれのクレジット料金、デフォルトのサイズがあり、動画の項目には秒あたり料金付きの解像度、長さの下限と上限、受け付ける比率があります。サイズの一覧もデフォルトもカタログ全体ではなく一つのモデルに属するので、どこにでも当てはめられる値はありません。モデル一覧と料金のページも同じエンドポイントを読んでいます。各モデルページには送るIDも表示されていて、たとえば動画ならSeedance 2.5、画像ならQwen 3です。リクエストの形はNSFW画像APIとNSFW動画APIのページにまとめています。
kindフィールドはモデルが画像と動画のどちらを作るかを示し、各項目にはテキストモードと参照モードがあります。どちらでも同じモデルIDを送り、参照画像を添付すると参照モードのサイズと料金が適用されます。未知のモデル、無効なサイズ、範囲外の長さは、paramに項目名が入った400で返ります。範囲外の長さは収まるよう切り詰められるのではなく、拒否されます。
実行を始め、記録をポーリングする
実行を始めるには、/v1/generate/imageまたは/v1/generate/videoに、モデルID、プロンプト、モデルごとのオプションをPOSTします。応答は202で、ID、状態processing、差し引かれたクレジット数が含まれます。クレジットは実行の終わりではなく始まりに引かれます。そのあとGET /v1/generations/{id}を読み続け、statusがqueuedでもprocessingでもなくなれば実行は終わりです。完了した記録のoutput_urlはVellriaのファイルエンドポイントを指し、失敗した記録には固定の集合から取ったerror_codeと、ログ向けの短い要約errorが入ります。
ポーリングの間隔には遅延を入れ、遅延にはランダムさを加えます。AWS Architecture BlogでのMarc Brookerのバックオフ実験は、一つの行を取り合うクライアントをシミュレーションし、ジッターを加えると「in the case with 100 contending clients, we've reduced our call count by more than half」(競合する100クライアントの場合、呼び出し回数が半分以下になった)と報告しています。ポーリングは結果を知る手段であって、結果を生む手段ではありません。実行は見ている人がいてもいなくてもサーバー側で照合されるので、接続が切れて失うのは結果への手がかりであり、結果そのものではありません。
一巡を一つのファイルで
これを.mjsファイルとして保存し、VELLRIA_API_KEYを設定して、新しいバージョンのNode.jsで実行してください。モデルID、サイズ、料金はどれもハードコードしていません。
```js import { writeFile } from "node:fs/promises"; const BASE = "https://vellria.com"; const AUTH = { Authorization: `Bearer ${process.env.VELLRIA_API_KEY}` }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function call(path, init) { const res = await fetch(BASE + path, { ...init, headers: { ...AUTH, ...init?.headers } }); if (res.status === 429) { await sleep(Number(res.headers.get("retry-after") ?? 5) * 1000); return call(path, init); } if (!res.ok) throw new Error((await res.json().catch(() => null))?.error?.code ?? res.status); return res.json(); } const catalog = await call("/v1/models"); const model = catalog.image[0]; let rec = await call("/v1/generate/image", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: model.id, prompt: "a lighthouse in fog", size: model.text.default_size }), }); while (rec.status === "queued" || rec.status === "processing") { await sleep(2000 + Math.random() * 2000); rec = await call(`/v1/generations/${rec.id}`); } if (rec.status !== "completed") throw new Error(rec.error_code ?? "generation_failed"); const file = await fetch(rec.output_url, { headers: AUTH }); const ext = file.headers.get("content-type") === "image/png" ? "png" : "jpg"; await writeFile(`${rec.id}.${ext}`, Buffer.from(await file.arrayBuffer())); ```
画像を受け取るモデルの場合は、先にバイトと一致するContent-Typeヘッダーを付けてファイルを/v1/uploadsにPOSTし、返ってきたURLをimage_urlsに渡します。受け付ける形式と拒否の一覧は静止画から動画へにあります。画像が必要かどうかはモデルの性質で、形の合わない本文は黙って調整されずに拒否されます。この点はキャラクターの一貫性を保つで扱っています。動画の本文はsizeではなくdurationとresolutionを持ち、こちらは解像度と長さの選び方で扱っています。動画モデルのIDは、種類と秒あたり料金とともにNSFW動画APIのページの表にまとめています。
失敗、流量制御、有効期限
開始後に失敗した実行のクレジットは、記録がfailedになった時点で自動的に残高へ戻ります。failedは終了かつ精算済みとして扱い、理由はerror_codeで分岐し、残高を自分の側で照合しないようにします。返還は一度しか成立しない条件付きの書き込みで記録されるので、生成サービスの通知が再送されても、二重に返還されたり、一度も返還されなかったりすることはありません。
レート制限を超えると、コードrate_limit_exceededとRetry-Afterヘッダー付きの429が返ります。RFC 6585はこのステータスを「the 429 status code indicates that the user has sent too many requests in a given amount of time」と定義し、RFC 9110の10.2.3節はこのヘッダーを「indicates how long the user agent ought to wait before making a follow-up request」ものとして定義しています。推測せずに値を読み取りましょう。まとめて実行するときの意味は絵コンテとプリビズにあります。残高不足は402で、記録は作られません。詳細はAPIリファレンスにあります。




