spicyapiDokumentation
SDKs
Hauptinhalt

TypeScript-SDK

Eine Schritt-für-Schritt-Anleitung zum TypeScript-SDK @spicyapi/sdk – installieren, das erste Ergebnis erhalten und jede Methode, jede Option und jeden Fehler in einfacher Sprache nachschlagen. Die SDKs für Python, Go, PHP und Java haben eigene Seiten.

@spicyapi/sdk ist die offizielle Bibliothek für TypeScript und JavaScript zu SpicyAPI. Diese Seite setzt voraus, dass Sie sie noch nie benutzt haben. Sie führt Sie von einem leeren Ordner bis zu einem gespeicherten Bild und beschreibt danach jede Funktion des Pakets: wofür sie gedacht ist, was Sie übergeben, was zurückkommt, ob sie Geld kostet und was schiefgehen kann.

Was das SDK ist

Ein SDK ist eine fertige Codebibliothek, die Sie in Ihr eigenes Programm einbinden. Ohne sie muss Ihr Programm jede HTTP-Anfrage von Hand zusammensetzen: den API-Schlüssel anhängen, die Antwort von JSON in Daten umwandeln, immer wieder nachfragen, ob ein Task fertig ist, nach einem Verbindungsabbruch erneut senden, Dateien in drei getrennten Schritten hochladen und bei jedem Webhook die Signatur prüfen. Das SDK übernimmt all das für Sie, sodass eine komplette Generierung in einen einzigen Aufruf passt:

const task = await client.run({ model: modelId, input: { prompt: "A paper lantern at night" } });

Es passt zu Ihnen, wenn Sie ein Programm in Node.js (JavaScript oder TypeScript) schreiben oder schreiben wollen, das auf einem Server, als Skript auf Ihrem eigenen Computer oder als Hintergrundjob läuft. Wer TypeScript nutzt, bekommt außerdem Autovervollständigung und Typprüfung für jedes Feld.

Ist das das richtige Werkzeug?

Sie möchten …Verwenden Sie
SpicyAPI aus Ihrem eigenen Node.js- oder TypeScript-Code aufrufenDieses SDK
Ohne Code aus einem Terminal heraus generierenDie CLI
Einen KI-Assistenten wie Claude Code oder Cursor SpicyAPI für Sie aufrufen lassenDen MCP-Server und den Agent Skill
Ein Chat- oder Textmodell nutzenDen offiziellen OpenAI-Client, ausgerichtet auf SpicyAPI – siehe Text und Streaming
Python, Go, PHP oder Java verwendenDas offizielle Python-SDK, Go-SDK, PHP-SDK oder Java-SDK
Irgendeine andere Sprache verwendenDirekte HTTP-Aufrufe oder einen aus der OpenAPI und Endpunktübersicht generierten Client

Was es nicht tut

  • Es läuft nicht im Browser und nicht in einer mobilen App. Es braucht Node.js, und Ihr API-Schlüssel darf Ihren Server nie verlassen: Aus einer Webseite oder einer App kann jeder einen Schlüssel auslesen. Lassen Sie Ihre Website Ihr eigenes Backend aufrufen und das Backend SpicyAPI.
  • Es deckt keine Chatmodelle ab. Textgenerierung läuft über OpenAI-kompatible Endpunkte, und das offizielle Paket openai erledigt das bereits gut. Siehe Text und Streaming.
  • Es wählt kein Modell für Sie aus und füllt keine Felder aus. Jedes Modell nimmt seine eigenen Felder an. Das SDK liefert Ihnen die Modellliste und das Schema jedes Modells (die Liste der Felder, die es annimmt); was Sie senden, entscheiden Sie.
  • Es kann keinen Task abbrechen. Hat SpicyAPI einen Task angenommen, läuft er bis zum Ende. Wenn Sie Ihr Programm beenden, endet nur Ihr Warten.
  • Es verwaltet nicht Ihr Konto. Guthaben aufladen, Schlüssel anlegen, Ausgabenlimits und Webhook-Signaturschlüssel stellen Sie in der Konsole ein.

Ihr API-Schlüssel bleibt auf Ihrem Server

Legen Sie SPICY_API_KEY niemals in Browser-JavaScript, eine mobile App, ein öffentliches Repository, einen Screenshot oder eine Chatnachricht. Mit einem geleakten Schlüssel lässt sich Ihr Guthaben ausgeben, bis Sie ihn sperren. Das SDK liest den Schlüssel aus einer Umgebungsvariable, damit er nie in Ihrem Code stehen muss.

Begriffe auf dieser Seite

BegriffBedeutung hier
Modell-IDDer exakte Name eines Modells in der Form publisher/model/task. Kopieren Sie ihn immer aus dem Live-Katalog (listModels()), nie aus dem Gedächtnis
EingabeDie Einstellungen, die Sie mit einem Task senden, etwa prompt oder image_url. Jedes Modell hat seine eigene Liste erlaubter Felder, sein Schema
TaskEin einzelner Generierungsauftrag. Sie erstellen ihn, SpicyAPI bearbeitet ihn im Hintergrund, und Sie holen das Ergebnis ab. Jeder Task hat eine ID, die mit job_ beginnt
ReservierungWird ein Task angenommen, wird sein Höchstpreis in Ihrem Guthaben zurückgelegt. Bei fehlgeschlagenen oder abgelaufenen Tasks wird die Reservierung normalerweise vollständig freigegeben. Einige Inhaltsablehnungen werden zum angenommenen Angebotspreis berechnet, sofern die Modellseite diese Ausnahme ausdrücklich nennt. Prüfen Sie den endgültigen Betrag anhand von settled und cost.
IdempotenzschlüsselEine Kennung, die Sie sich für einen bestimmten Arbeitsauftrag ausdenken, etwa order-8814-image-1. Senden Sie dieselbe Anfrage zweimal mit derselben Kennung, liefert SpicyAPI den ersten Task zurück, statt einen zweiten anzulegen und zu berechnen
WebhookEine Webadresse von Ihnen, die SpicyAPI aufruft, sobald ein Task fertig ist, damit Sie nicht ständig nachfragen müssen

Was Sie tun können

Jede Funktion unten ist eine Methode des Clients (client.listModels()), sofern nichts anderes angegeben ist. „Kostet Geld“ heißt, dass der Aufruf zu einer Belastung Ihres Guthabens führen kann.

Was Sie tun möchtenMethodeKostet Geld?
Prüfen, ob SpicyAPI erreichbar ist (ohne Schlüssel)getStatusNein
Die aktuelle Modellliste, Ihre Preise und die Felder jedes Modells sehenlistModels, getModelNein
Den genauen Preis einer Anfrage erfahren, bevor Geld ausgegeben wirdquoteTaskNein
Eine Generierung startencreateTaskJa
Eine Generierung starten und in einem Aufruf auf das Ergebnis wartenrunJa
Einen Task einmal ansehengetTaskNein
Warten, bis ein Task fertig istwaitForTaskNein
Einen fehlgeschlagenen Task erneut versuchenretryTaskJa (ein neuer Task)
Die mit Ihrem Schlüssel erstellten Tasks auflistenlistTasksNein
Ihr Guthaben abfragengetBalanceNein
Sehen, was Sie ausgegeben haben, nach Tag und nach ModellgetUsageNein
Eine lokale Bild-, Video- oder Audiodatei hochladenuploadFile, uploadBytes, uploadBase64Nein
Einen frischen Download-Link für ein Ergebnis erhaltencreateDownloadUrlNein
Die gespeicherten Dateien und den Prompt eines beendeten Tasks löschenpurgeTaskNein (und keine Erstattung)
Prüfen, ob ein Webhook wirklich von SpicyAPI stammtverifyWebhook (eine einfache Funktion)Nein
Die Dokumentation durchsuchen oder den API-Vertrag offline lesensearchDocumentation, readOpenApiContract (einfache Funktionen)Nein

Bevor Sie beginnen

Sie brauchen etwa zehn Minuten. Zu jedem Schritt steht da, was der Befehl tut, damit nichts blind ausgeführt wird.

Node.js 22.13 oder neuer installieren

Node.js ist das Programm, das JavaScript außerhalb eines Browsers ausführt. Installieren Sie die LTS-Version von nodejs.org; jede Version ab 22.13 funktioniert. Öffnen Sie danach ein Terminal (unter macOS die App Terminal, unter Windows PowerShell) und prüfen Sie:

node -v

Ausgegeben wird die installierte Version, zum Beispiel v22.13.0 oder v24.8.0. Ist die Zahl kleiner als 22.13 oder wird der Befehl nicht gefunden, installieren Sie Node.js erneut und öffnen ein neues Terminalfenster.

Einen Projektordner anlegen und das SDK installieren

Führen Sie diese Befehle einzeln nacheinander aus. Sie funktionieren unter macOS, Linux und Windows PowerShell gleich.

mkdir spicy-demo
cd spicy-demo
npm init -y
npm pkg set type=module
npm install @spicyapi/sdk
BefehlWas er tut
mkdir spicy-demoLegt einen leeren Ordner für Ihr Projekt an
cd spicy-demoWechselt im Terminal in diesen Ordner; die folgenden Befehle wirken dort
npm init -yLegt package.json an, die Datei mit den Einstellungen und Bibliotheken Ihres Projekts
npm pkg set type=moduleTeilt Node.js mit, dass Ihre Dateien die moderne import-Syntax verwenden, die dieses SDK voraussetzt
npm install @spicyapi/sdkLädt das SDK in einen Ordner node_modules herunter und trägt es in package.json ein

Das SDK wird nur als modernes ES-Modul veröffentlicht: import { SpicyClient } from "@spicyapi/sdk" funktioniert, das ältere require("@spicyapi/sdk") scheitert mit ERR_PACKAGE_PATH_NOT_EXPORTED. Lassen Sie npm pkg set type=module weg, geben Sie Ihren Dateien stattdessen die Endung .mjs.

Einen API-Schlüssel anlegen

  1. Melden Sie sich bei spicyapi.ai an. Noch kein Konto? Registrieren Sie sich unter spicyapi.ai/register; sind Neuanmeldungen gerade pausiert, bietet diese Seite eine Warteliste an.
  2. Öffnen Sie in der Konsole die Seite API-Schlüssel und klicken Sie auf Schlüssel anlegen. Nur ein Name ist Pflicht.
  3. Kopieren Sie den Schlüssel sofort. Er beginnt mit sk-spicy- und wird nur ein einziges Mal angezeigt. Geht er verloren, legen Sie einen neuen an.

Sie können auf derselben Seite ein Tageslimit für Ausgaben setzen; neue Schlüssel haben keines, bis Sie eines hinzufügen. Für kostenpflichtige Tasks brauchen Sie außerdem Guthaben, das Sie unter Abrechnung aufladen.

Den Schlüssel in eine Umgebungsvariable legen

Eine Umgebungsvariable ist ein benannter Wert, den Ihr Terminal an jedes Programm weitergibt, das es startet. Das SDK sucht nach einer Variable namens SPICY_API_KEY, sodass der Schlüssel nie in Ihrem Code stehen muss.

# Für dieses Terminalfenster setzen (Ihren eigenen Schlüssel einfügen)
export SPICY_API_KEY="sk-spicy-paste-your-key-here"

# Prüfen: Das sollte sk-spicy- ausgeben
echo "${SPICY_API_KEY:0:9}"

Der Wert verschwindet, sobald Sie das Fenster schließen. Um ihn auf Ihrem eigenen Computer dauerhaft zu behalten, fügen Sie die export-Zeile in ~/.zshrc (macOS) oder ~/.bashrc (Linux) ein und öffnen ein neues Fenster.

Lieber eine .env-Datei? Legen Sie im Projektordner eine Datei namens .env mit einer einzigen Zeile an, SPICY_API_KEY=sk-spicy-paste-your-key-here, und starten Sie Ihr Programm mit node --env-file=.env yourfile.mjs. Das SDK liest .env-Dateien nicht selbst; das erledigt dieser Schalter. Tragen Sie .env in Ihre Datei .gitignore ein, bevor Sie je git add ausführen, damit der Schlüssel nie committet wird.

Auf einem echten Server setzen Sie SPICY_API_KEY in den Secret- oder Umgebungseinstellungen Ihres Hosting-Anbieters, oder Sie übergeben den Schlüssel aus Ihrem Secret-Manager mit new SpicyClient({ apiKey }).

Ihre erste Datei ausführen

Legen Sie im Projektordner mit einem beliebigen Texteditor eine Datei namens check.mjs an, fügen Sie Folgendes ein und speichern Sie:

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);

Führen Sie sie aus:

node check.mjs
SpicyAPI is up
Checked at 2026-09-17T08:00:00.000Z

Diese Prüfung braucht keinen Schlüssel und kostet nichts. Sie belegt also, dass Node.js und das SDK richtig installiert sind.

Sie nutzen TypeScript? Speichern Sie denselben Code als check.ts. Aktuelle Versionen von Node.js 22 und 24 führen ihn direkt mit node check.ts aus. Beschwert sich Ihre Version über die Endung .ts, führen Sie node --experimental-strip-types check.ts oder npx tsx check.ts aus.

Ihr erstes Ergebnis

Dieses Programm geht den ganzen Weg: Es prüft den Dienst, wählt ein Bildmodell, das Ihr Schlüssel nutzen kann, fragt den genauen Preis ab und hält an. Führen Sie es ein zweites Mal mit --yes aus, erstellt es das Bild, wartet darauf und speichert es neben dem Skript.

Der Lauf mit --yes kostet Geld

Ohne --yes liest das Programm nur: Es wird nichts erstellt und nichts berechnet. Mit --yes erstellt es einen Bild-Task, der den geschätzten Betrag reserviert und ihn berechnet, wenn das Bild gelingt.

first-result.mjs
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { SpicyApiError, SpicyClient } from "@spicyapi/sdk";

// Liest Ihren Schlüssel aus der Umgebungsvariable SPICY_API_KEY.
const client = new SpicyClient();

async function main() {
  // Schritt 1 – Ist SpicyAPI erreichbar? Kostenlos und ohne Schlüssel.
  const status = await client.getStatus();
  if (!status.operational) {
    throw new Error("SpicyAPI is not reachable right now. Try again in a few minutes.");
  }

  // Schritt 2 – Welche Bildmodelle kann dieser Schlüssel aufrufen? Kostenlos.
  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})`);

  // Schritt 3 – Die Eingabe bauen. Mit einem Beispiel beginnen, das der Server
  // bereits geprüft hat, und den eigenen Prompt einsetzen, falls das Modell einen nimmt.
  const input = { ...model.examples?.[0]?.input };
  if ("prompt" in input) input.prompt = "A paper lantern glowing on a rainy street at night";

  // Schritt 4 – Den genauen Preis abfragen. Kostenlos: Nichts wird erstellt oder reserviert.
  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;
  }

  // Schritt 5 – Den Task erstellen und darauf warten. Dieser Schritt kostet Geld.
  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}`),
    },
  );

  // Schritt 6 — Ein fehlgeschlagener Task ist eine normale Antwort. Abrechnung prüfen.
  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;
  }

  // Schritt 7 – Das Ergebnis jetzt herunterladen. Der Link läuft nach etwa 20 Minuten ab.
  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); // hierher nie Ihren API-Schlüssel senden
  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;
});

Führen Sie es zweimal aus:

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>

Was jeder Schritt tut

  1. getStatus() fragt den Health-Endpunkt von SpicyAPI, ob der Dienst läuft. Bei einem Netzwerkproblem wirft er nie einen Fehler, sondern meldet stattdessen operational: false.
  2. listModels() liefert den Live-Modellkatalog mit den Preisen Ihres Kontos. Verwenden Sie nur Einträge, bei denen enabled und available beide true sind. includeExamples: true fügt Beispiel-Eingaben hinzu, die der Server bereits validiert hat – der sicherste Ausgangspunkt. Was Sie später senden, ist das Feld model des Eintrags, die Modell-ID.
  3. Die Eingabe. Jedes Modell nimmt andere Felder an; ein Feld, das das Modell nicht kennt, wird mit dem Fehler 400 abgelehnt. Wer ein validiertes Beispiel kopiert und nur den Prompt ändert, umgeht das. Um alle Felder eines Modells zu sehen, rufen Sie getModel() auf und lesen dessen inputSchema.
  4. quoteTask() berechnet den Preis genau dieser Anfrage. Es wird nichts erstellt und nichts reserviert. Beträge sind US-Dollar als Text, etwa "0.04", damit sich keine Rundungsfehler einschleichen.
  5. run() erstellt den Task und fragt danach so lange nach, bis er fertig ist. Mit quoteId und expectedCost sagen Sie: „Nur zu dem Preis annehmen, der mir angezeigt wurde.“ Hat sich der Preis geändert, scheitert der Aufruf mit 40901, statt einen anderen Betrag zu berechnen. Der idempotencyKey macht eine Wiederholung nach einem Netzwerkfehler sicher; speichern Sie ihn in einer echten Anwendung vor dem Aufruf, damit ein neu gestartetes Programm ihn wiederverwenden kann. onAccepted übergibt Ihnen die Task-ID in dem Moment, in dem der Task existiert – speichern Sie sie, damit Sie den Task später abfragen können, auch wenn Ihr Programm stoppt.
  6. Der Task-Status. run() kehrt auch bei fehlgeschlagenen Tasks normal zurück. Bei fehlgeschlagenen oder abgelaufenen Tasks wird die Reservierung normalerweise vollständig freigegeben. Einige Inhaltsablehnungen werden zum angenommenen Angebotspreis berechnet, sofern die Modellseite diese Ausnahme ausdrücklich nennt. Prüfen Sie den endgültigen Betrag anhand von settled und cost.
  7. Der Download. Jedes Ergebnis kommt mit einer temporären url. Laden Sie es ohne Ihren API-Schlüssel herunter und behalten Sie eine eigene Kopie: Der Link gilt etwa 20 Minuten (rufen Sie getTask() erneut auf, um einen frischen zu erhalten), und SpicyAPI löscht erzeugte Dateien nach höchstens etwa 14 Tagen. Siehe Datenaufbewahrung und Löschung.

Video funktioniert genauso, kann aber mehrere Minuten dauern; übergeben Sie run() dann einen längeren timeoutMs (standardmäßig wird 10 Minuten gewartet). Modell-IDs in den folgenden Codeausschnitten erscheinen als MODEL_ID_FROM_CATALOG: Das ist ein Platzhalter, kein echtes Modell. Ersetzen Sie ihn durch den Wert model eines Eintrags aus dem Live-Katalog – listModels() im SDK oder der authentifizierte Endpunkt GET /api/v1/models –, bei dem enabled und available beide true sind.

API-Referenz

Jeder Eintrag unten sagt, wofür die Methode gedacht ist, wie Sie sie aufrufen, was zurückkommt, ob sie Geld kosten kann und mit welchen Fehlern Sie rechnen müssen. Fehlerklassen wie SpicyApiError werden unter Fehler behandeln erklärt. Alle Beträge sind US-Dollar als Text ("0.04"), alle Zeitangaben ISO-8601-Zeichenketten in UTC.

Einen Client erstellen

new SpicyClient(options?: SpicyClientOptions)

Die meisten Programme übergeben nichts: new SpicyClient() liest den Schlüssel aus SPICY_API_KEY und spricht mit dem Produktivdienst. Eine ungültige Option wirft sofort einen TypeError, bevor irgendeine Anfrage gesendet wird.

Eigenschaft

Typ

Automatische Wiederholungen

Das SDK wiederholt eine Anfrage nur dann von selbst, wenn Sie dadurch nicht doppelt zahlen können:

AufrufAutomatisch wiederholt?
Lesende Aufrufe: getStatus, listModels, getModel, getTask, listTasks, getBalance, getUsage; außerdem quoteTask, purgeTask, commitUploadedFileJa
createTask, run (dessen Erstellungsschritt), retryTaskNur wenn Sie einen idempotencyKey übergeben. Ohne ihn wird ein Netzwerkfehler direkt an Sie weitergereicht, denn erneutes Senden könnte einen zweiten Task anlegen
createUploadUrl, der eigentliche Byte-Upload, createDownloadUrlNein

Wiederholt wird, wenn gar keine Antwort kommt (Netzwerkfehler oder lokales Zeitlimit) oder die Antwort HTTP 408, 429, 500, 502, 503 oder 504 lautet. Die Pause richtet sich nach dem Header Retry-After des Servers; fehlt er, sind es 250 ms, dann 500 ms, dann 1 s und so weiter, jeweils um bis zu 25 % gestreut und durch maxRetryDelayMs gedeckelt. Scheitern alle Versuche, erhalten Sie den letzten Fehler.

Einen Aufruf mit signal stoppen

Jede Methode nimmt die Option signal an, ein Standard-AbortSignal. Mit AbortSignal.timeout(5_000) setzen Sie eine Frist, mit einem AbortController stoppen Sie auf Befehl. Der Aufruf wirft dann den Abbruchgrund. Ein Stopp beendet nur Ihre Anfrage oder Ihr Warten: Ein Task, den SpicyAPI bereits angenommen hat, läuft weiter und kann trotzdem berechnet werden.

getStatus()

Prüft, ob SpicyAPI erreichbar ist. Praktisch als erste Zeile eines Skripts oder als Health-Check in Ihrem eigenen Monitoring.

getStatus(options?: { signal?: AbortSignal }): Promise<ServiceStatus>
RückgabefeldBedeutung
operationaltrue, wenn der Dienst antwortet. Dieses Feld sollten Sie verwenden
healthDas Rohergebnis der Health-Prüfung: path, ok, HTTP-status, die ersten 512 Zeichen von body und error, wenn keine Antwort ankam
readinessDieselbe Form für eine zweite Prüfung. Beim öffentlichen Dienst antwortet sie mit 404; das ist normal und heißt nicht, dass der Dienst ausgefallen ist
checkedAtWann die Prüfung lief

Kostet Geld: Nein. Braucht einen Schlüssel: Nein. Fehler: Bei Netzwerkproblemen wirft die Methode nichts – Sie erhalten operational: false und den Grund in health.error. Sie wirft nur, wenn Sie sie mit signal abbrechen. Ein Beispiel finden Sie unter Ihre erste Datei ausführen.

listModels(options?)

Listet den Live-Modellkatalog mit den Preisen Ihres Kontos auf. Von hier stammt jede Modell-ID.

listModels(options?: ListModelsOptions): Promise<ModelList>

Eigenschaft

Typ

Zurück kommt { total, items }. Die nützlichsten Felder jedes Eintrags:

FeldBedeutung
modelDie Modell-ID. Senden Sie genau diesen Wert, wenn Sie einen Task erstellen
displayNameEin lesbarer Name
modality, tasksWas das Modell erzeugt und welche Aufgabe es erledigt
provider, familyWer das Modell entwickelt hat und zu welcher Produktlinie es gehört
enabled, availableBeide müssen true sein, bevor Sie das Modell verwenden
pricingEin Eintrag pro Preisstufe: price, unit (per_image, per_second, per_request oder per_1k_tokens) und variant (welche Stufe, oft eine Auflösung). Während eines Rabatts zeigt regularPrice den Preis ohne Rabatt
startingPriceDie günstigste Stufe. Für einen exakten Betrag verwenden Sie quoteTask
quantityFieldDas Eingabefeld, das die abgerechnete Menge bestimmt, etwa eine Dauer. Leer bedeutet eine Einheit pro Aufruf
taskTimeoutSecondsWie lange ein Task laufen darf, bevor er als expired endet
maxOutputDurationSecondsDie längste Ausgabe, die das Modell erlaubt, sofern es eine Dauer hat
inputSchema, examplesNur vorhanden, wenn Sie sie angefordert haben
updatedAt, versionÄndern sich, sobald sich die Angaben zum Modell ändern; daran erkennen Sie, wann ein gespeichertes Schema aufgefrischt werden sollte

Kostet Geld: Nein. Braucht einen Schlüssel: Ja. Fehler: SpicyApiError (zum Beispiel 401 bei einem Problem mit dem Schlüssel, 429 bei zu vielen Anfragen), 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);
}

Katalogabfragen nutzen das Lesekontingent Ihres Kontos unabhängig von der Taskerstellung. Speichern Sie die Liste und frischen Sie sie gelegentlich auf oder nach einem 400 bzw. 50301, statt sie vor jedem Task neu zu lesen.

getModel(modelId)

Liefert ein einzelnes Modell mit seinem vollständigen Schema: der Liste der Eingabefelder, die es annimmt, welche davon Pflicht sind und welche Werte erlaubt sind.

getModel(modelId: string, options?: { signal?: AbortSignal }): Promise<ApiModel>

Das Ergebnis enthält alle unter listModels beschriebenen Felder, und zwar immer mit inputSchema und examples. Das Schema ist Standard-JSON-Schema: properties listet jedes Feld mit seinem type und oft mit enum (erlaubte Werte), minimum / maximum und default; required listet die Felder, die Sie senden müssen.

Kostet Geld: Nein. Braucht einen Schlüssel: Ja. Fehler: SpicyApiError mit 404, wenn kein Modell diese ID hat; TypeError, wenn die ID leer ist.

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)

Ermittelt den genauen Preis einer Anfrage, ohne etwas zu erstellen. Damit zeigen Sie einer Person einen Preis an oder lehnen Arbeit ab, die über einem Budget liegt.

quoteTask(request: { model: string; input: object; callBackUrl?: string }, options?: { signal?: AbortSignal }): Promise<TaskQuote>

Übergeben Sie dieselben Werte für model, input und optional callBackUrl, mit denen Sie den Task erstellen wollen.

RückgabefeldBedeutung
quoteIdÜbergeben Sie sie an createTask oder run, um diesen Preis festzuschreiben
estimatedCostDer Betrag, der reserviert wird, wenn Sie den Task jetzt erstellen
maxChargeDas Höchste, was diese Anfrage kosten kann
currencyImmer USD
quantity, unitDie abgerechnete Menge, etwa "5" und per_second
expiresAtDie Kostenschätzung gilt fünf Minuten

Kostet Geld: Nein. Braucht einen Schlüssel: Ja. Fehler: SpicyApiError, zum Beispiel 400, wenn die Eingabe nicht zum Schema des Modells passt, 50301, wenn das Modell nicht verfügbar ist, 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?)

Startet eine Generierung und kehrt sofort mit einer Task-ID zurück. Auf das Ergebnis wartet die Methode nicht: Holen Sie es danach mit waitForTask oder über einen Webhook ab, oder verwenden Sie run, das beides in einem Aufruf erledigt.

createTask(request: CreateTaskInput, options?: CreateTaskOptions): Promise<CreateTaskResult>

Die Anfrage:

Eigenschaft

Typ

Die Optionen:

Eigenschaft

Typ

RückgabefeldBedeutung
taskIdDie ID des neuen Tasks. Speichern Sie sie sofort
statequeued bei einem neuen Task. Haben Sie einen Idempotenzschlüssel wiederholt, der aktuelle Status des ursprünglichen Tasks
estimatedCostDer jetzt reservierte Betrag, zugleich das Höchste, was dieser Task kosten kann
deadlineAtWann SpicyAPI die Arbeit an dem Task einstellt, falls er nicht fertig ist

Kostet Geld: Ja. Der estimatedCost wird in Ihrem Guthaben reserviert. Ist der Task erfolgreich, zahlen Sie den tatsächlichen Verbrauch, nie mehr als die Reservierung, und der Rest wird freigegeben. Bei fehlgeschlagenen oder abgelaufenen Tasks wird die Reservierung normalerweise vollständig freigegeben. Einige Inhaltsablehnungen werden zum angenommenen Angebotspreis berechnet, sofern die Modellseite diese Ausnahme ausdrücklich nennt. Prüfen Sie den endgültigen Betrag anhand von settled und cost.

Fehler:

error.codeBedeutungWas zu tun ist
400Die Eingabe passt nicht zum Schema des Modells, oder die callBackUrl ist nicht erlaubtMit getModel().inputSchema vergleichen und die Anfrage korrigieren
401Der Schlüssel fehlt, ist falsch formatiert oder wurde widerrufenSPICY_API_KEY prüfen
40201Ihr Guthaben reicht für die Reservierung nichtIn der Konsole unter Abrechnung aufladen
40202Ein Tages-, Monats- oder Gesamtlimit ist erreichtDas Limit auf der Seite API-Schlüssel anheben oder warten, bis es neu beginnt
40301Dieser Schlüssel darf dieses Modell nicht verwendenDie Modellliste des Schlüssels in der Konsole ändern
409Dieser Idempotenzschlüssel wurde bereits für eine andere Anfrage verwendetFür neue Arbeit einen neuen Idempotenzschlüssel verwenden
40901Die Kostenschätzung ist abgelaufen, oder Preis oder Anfrage haben sich geändertNeu schätzen lassen und den neuen Preis bestätigen
413Der Anfragerumpf ist zu großMediendateien hochladen, statt sie direkt einzubetten
429Zu viele Anfragenerror.retryAfterSeconds abwarten, dann erneut versuchen
50301Das Modell kann gerade keine Tasks annehmenSpäter erneut versuchen oder ein anderes Modell wählen

Ein SpicyTransportError oder SpicyTimeoutError bedeutet, dass keine Antwort ankam; Sie können also nicht wissen, ob der Task angelegt wurde. Senden Sie dieselbe Anfrage mit demselben Idempotenzschlüssel erneut: Entweder wird der Task jetzt angelegt, oder Sie erhalten den bestehenden zurück. Ein TypeError bedeutet, dass eine Option ungültig war (zum Beispiel ein negativer retentionSeconds) und nichts gesendet wurde.

import { randomUUID } from "node:crypto";

const idempotencyKey = randomUUID(); // in einer echten Anwendung zuerst zusammen mit Ihrer Bestellung speichern
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?)

Der einfachste Weg, etwas zu generieren: Die Methode ruft createTask und danach waitForTask auf und liefert den fertigen Task.

run(request: CreateTaskInput, options?: RunTaskOptions): Promise<TaskRecord>

request ist dasselbe wie bei createTask. Als Optionen nimmt run alles an, was createTask annimmt, und zusätzlich:

Eigenschaft

Typ

Zurück kommt der fertige Task mit den unter getTask beschriebenen Feldern. Bei einem erfolgreichen Task wartet die Methode außerdem, bis die Ergebnisdateien fertig kopiert sind, sodass output.assets[].url zum Herunterladen bereitsteht.

Kostet Geld: Ja, genau wie createTask. Ein fehlgeschlagener Task wird zurückgegeben, nicht geworfen – prüfen Sie task.state.

Fehler: alles, was createTask werfen kann, und zusätzlich:

  • SpicyWaitTimeoutError, wenn timeoutMs abläuft, nachdem der Task angenommen wurde. Der Task läuft bei SpicyAPI weiter. Rufen Sie später waitForTask(error.taskId) auf, um ihn abzuholen.
  • SpicyTimeoutError, wenn die Zeit abläuft, bevor SpicyAPI den Task bestätigt hat. Senden Sie dieselbe Anfrage mit demselben Idempotenzschlüssel, um es herauszufinden.
  • Jeder Fehler, der in Ihrem eigenen onAccepted oder onUpdate geworfen wird, beendet das Warten; der Task selbst läuft weiter.
  • TypeError bei einem ungültigen timeoutMs oder intervalMs. Diese Werte werden geprüft, bevor etwas angelegt wird.

Ein vollständiges Beispiel finden Sie unter Ihr erstes Ergebnis.

getTask(taskId)

Liest einen Task einmal: seinen Status, seine Ergebnislinks und was er gekostet hat.

getTask(taskId: string, options?: { signal?: AbortSignal }): Promise<TaskRecord>
FeldBedeutung
taskId, modelWelcher Task, mit welchem Modell erstellt
statequeued oder running, solange er arbeitet; succeeded, failed oder expired, wenn er beendet ist (canceled kommt nur bei alten Datensätzen vor)
output.assetsDie Ergebnisdateien, beschrieben in der nächsten Tabelle. Nur bei succeeded
output.textTextausgabe, bei Modellen, die Text zurückgeben
errorCode, errorMessageWarum ein Task fehlgeschlagen ist. Entscheiden Sie anhand von errorCode; der Wortlaut der Meldung kann sich ändern
costSolange settled false ist, der reservierte Betrag; sobald es true ist, die endgültige Belastung
settledOb cost endgültig ist
createdAt, completedAt, deadlineAtWann er erstellt wurde, wann er fertig war und wann er ablaufen würde
inputDie Eingabe, wie sie gespeichert ist. Wird entfernt, wenn die Aufbewahrungsfrist für Prompts endet
sourceTaskIdGesetzt bei einem Task, den retryTask erstellt hat; verweist auf das Original
contentStatepresent; expired (durch Aufbewahrungsregeln entfernt); oder purged (Sie haben ihn mit purgeTask gelöscht)
retentionWann die Dateien (outputsExpireAt) und der Prompt (promptsExpireAt) entfernt werden

Jeder Eintrag in output.assets:

FeldBedeutung
urlEin temporärer Download-Link. Laden Sie ohne Ihren API-Schlüssel herunter
expiresAtWann dieser Link nicht mehr funktioniert, normalerweise nach etwa 20 Minuten. Rufen Sie getTask erneut auf, um einen frischen Link zu erhalten
mimeDer Dateityp, etwa image/png oder video/mp4
width, height, durationSeconds, bytesAngaben zur Größe, sofern zutreffend
pendingtrue, solange die Datei noch in den Speicher kopiert wird. Fragen Sie in ein paar Sekunden erneut
unavailabletrue, wenn die Datei endgültig weg ist. Warten hilft nicht; generieren Sie neu
keyEin interner Dateiname, nur für createDownloadUrl nötig

Ein Feld, für das SpicyAPI keinen Wert hat, wird weggelassen statt auf null gesetzt. Behandeln Sie also jedes Feld oben als optional.

Kostet Geld: Nein. Fehler: SpicyApiError mit 404, wenn für Ihren Schlüssel kein Task mit dieser ID sichtbar ist.

const task = await client.getTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3"); // die ID, die Sie gespeichert haben

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?)

Fragt einen Task wiederholt ab, bis er fertig ist, und liefert ihn dann zurück. Verwenden Sie die Methode für einen Task, den Sie früher erstellt haben, zum Beispiel nach einem Neustart Ihres Programms.

waitForTask(taskId: string, options?: WaitForTaskOptions): Promise<TaskRecord>

Optionen: timeoutMs (Standard 600000, bis 86400000), intervalMs, onUpdate und signal, jeweils wie unter run beschrieben.

Die Methode kehrt zurück, sobald state succeeded, failed, expired oder canceled ist, bei einem erfolgreichen Task erst, wenn keine Ergebnisdatei mehr pending ist. Jede Abfrage ist eine getTask-Anfrage und zählt gegen das Rate Limit Ihres Kontos; deshalb wird der Standardabstand mit der Zeit größer.

Kostet Geld: Nein. Fehler: SpicyWaitTimeoutError, wenn timeoutMs abläuft. Er enthält taskId, timeoutMs und lastTask (den zuletzt gesehenen Stand). Der Task wird nicht abgebrochen; Sie können einfach erneut warten. Außerdem SpicyApiError (zum Beispiel 404) und 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?)

Führt einen fehlgeschlagenen Task erneut aus, als ganz neuen Task mit demselben Modell und derselben Eingabe. Der ursprüngliche Task bleibt genau so, wie er war, und der neue verweist über sourceTaskId auf ihn.

retryTask(taskId: string, options?: { idempotencyKey?: string; signal?: AbortSignal }): Promise<RetryTaskResult>

Das funktioniert bei Tasks mit failed und expired (und bei alten mit canceled). Ein erfolgreicher oder noch laufender Task wird mit 409 abgelehnt. Preis, Guthaben und die Berechtigungen des Schlüssels werden erneut geprüft, und der neue Task übernimmt die Aufbewahrungseinstellung des ursprünglichen.

Weil die Eingabe unverändert wiederverwendet wird, hilft eine Wiederholung nur, wenn der Fehler vorübergehend war. Lesen Sie zuerst den errorCode des Ausgangs-Tasks: Bei rate_limited, upstream_unavailable, timeout und upstream_failed lohnt nach einer Pause eine Wiederholung; generation_failed und invalid_asset brauchen eine geänderte Anfrage über createTask; content_rejected, invalid_request und unsupported_combination sollten nicht unverändert wiederholt werden. Siehe Häufige Fehlercodes und Fehler.

Zurück kommen taskId (der neue Task), sourceTaskId (das Original), state, estimatedCost und deadlineAt.

Kostet Geld: Ja. Der neue Task hat seine eigene Reservierung und wird bei Erfolg gesondert berechnet. Übergeben Sie einen idempotencyKey, damit Ihr Wiederholungscode, zweimal ausgeführt, nur eine Wiederholung anlegt.

Fehler: 409 (der Task kann nicht wiederholt werden) sowie die Fehler zu Guthaben, Limits und Verfügbarkeit, die unter createTask aufgeführt sind. Hat die ursprüngliche Eingabe eine hochgeladene spicy://-Datei verwendet, ist diese nur einen Tag nutzbar; laden Sie sie erneut hoch und verwenden Sie stattdessen createTask.

retryTask ist nicht für Netzwerkfehler gedacht

Ist createTask selbst an einem Netzwerkfehler gescheitert, haben Sie keinen Task, den Sie wiederholen könnten. Senden Sie createTask mit demselben Idempotenzschlüssel erneut. Verwenden Sie retryTask nur für einen Task, dessen state Sie als failed oder expired gelesen haben.

Ein vollständiges Beispiel steht unter Einen fehlgeschlagenen Task wiederholen.

listTasks(options?)

Listet die mit diesem API-Schlüssel erstellten Tasks auf, die neuesten zuerst, Seite für Seite. Nützlich, um nach einem Neustart oder einem verpassten Webhook Arbeit wiederzufinden.

listTasks(options?: ListTasksOptions): Promise<TaskList>

Eigenschaft

Typ

Zurück kommen items, hasMore und, falls es weitere Seiten gibt, nextCursor. Jeder Eintrag hat taskId, model, state, cost, settled, createdAt, deadlineAt und completedAt. Eingaben, Ergebnisse und Links sind nicht enthalten; rufen Sie für einen benötigten Task getTask auf.

Kostet Geld: Nein. Fehler: SpicyApiError (zum Beispiel 400 bei einem ungültigen Zeitraum), SpicyTransportError.

let cursor;
do {
  const page = await client.listTasks({
    from: "2026-09-01", // auf jeder Seite dieselben Daten verwenden
    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()

Zeigt, wie viel Geld auf Ihrem Konto ist.

getBalance(options?: { signal?: AbortSignal }): Promise<Balance>
FeldBedeutung
availableGeld, mit dem neue Tasks bezahlt werden können. Kann negativ sein, wenn Ihr Konto eine bewilligte Kreditlinie nutzt
heldGeld, das für noch laufende Tasks zurückgelegt ist
totalavailable plus held
fundingOptionale Aufschlüsselung: vorausbezahltes Guthaben, Bonusguthaben aus Aktionen (mit den Modellen, für die es gilt, und seinem Ablaufdatum) und eine eventuelle Kreditlinie. Ältere Antworten lassen sie womöglich weg

Kostet Geld: Nein. Sie müssen das Guthaben nicht vor jedem Task prüfen: SpicyAPI prüft es beim Erstellen und antwortet mit 40201, wenn es nicht reicht. Alle Felder zur Finanzierung erklärt die Seite Abrechnung.

const balance = await client.getBalance();
console.log(`Available: $${balance.available} (on hold: $${balance.held})`);

getUsage(options?)

Zeigt, was dieser API-Schlüssel ausgegeben hat, gruppiert nach Tag und nach Modell.

getUsage(options?: { from?: string; to?: string; signal?: AbortSignal }): Promise<UsageReport>

from und to sind UTC-Datumsangaben (YYYY-MM-DD); from ist eingeschlossen, to nicht. Lassen Sie beide weg, erhalten Sie die letzten sieben Tage einschließlich heute. Der Zeitraum darf höchstens 92 Tage umfassen.

FeldBedeutung
totalCallsAnzahl der Tasks im Zeitraum
totalSpendNur endgültige Belastungen; noch reserviertes Geld zählt nicht mit
daysEin Eintrag pro Tag mit Aufrufen: day, calls, succeeded, failed, spend
modelsEin Eintrag pro Modell: model, calls, succeeded, failed, spend

Kostet Geld: Nein. Dieser Bericht ist aufwendiger als andere Abfragen und hat deshalb ein zusätzliches Limit von 30 Anfragen pro Minute; nutzen Sie ihn für Berichte, nicht in einer Schleife. Fehler: SpicyApiError (429 oberhalb dieses Limits), 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);

Dateien hochladen

Braucht ein Modell ein Bild, ein Video oder einen Ton als Eingabe, können Sie einen öffentlichen HTTPS-Link zur Datei direkt in das Eingabefeld schreiben. Eine Datei auf Ihrem Computer laden Sie zuerst hoch. Sie erhalten eine spicy://-Adresse zurück und schreiben stattdessen diese in das Eingabefeld. Das Hochladen ist kostenlos.

  • Bilder (JPEG, PNG, WebP, GIF): bis 10 MiB.
  • Video (MP4, WebM) und Audio (MP3, WAV): bis 90 MiB und 600 Sekunden Länge.
  • Ein Modell kann in seinem Schema eine niedrigere Grenze festlegen.
  • Eine hochgeladene Datei ist einen Tag lang nutzbar, und zwar nur von Ihrem Konto.

uploadFile(path, options?)

Lädt eine Datei von der Festplatte hoch.

uploadFile(path: string, options?: { contentType?: string; signal?: AbortSignal }): Promise<UploadedFile>

Der Dateityp wird an der Endung erkannt: .jpg, .jpeg, .png, .webp, .gif, .mp4, .webm, .mp3 oder .wav. Bei jedem anderen Namen übergeben Sie contentType: image/jpeg, image/png, image/webp, image/gif, video/mp4, video/webm, audio/mpeg oder audio/wav.

RückgabefeldBedeutung
uriDie spicy://-Adresse für die Eingabe eines Tasks
expiresAtWann die Adresse nicht mehr funktioniert (nach einem Tag)
fileId, status, bytes, contentType, sha256Angaben zur gespeicherten Datei; status ist ready
width, height, durationSecondsGemessene Abmessungen oder Länge, sofern zutreffend

Kostet Geld: Nein. Fehler: TypeError, wenn sich der Typ nicht erkennen lässt – die Meldung listet jede erkannte Endung auf, contentType is required unless the file extension is one of: gif, jpeg, jpg, png, webp, mp4, webm, mp3, wav; ein Node.js-Fehler ENOENT, wenn der Pfad falsch ist; SpicyUploadError (mit status), wenn der Speicher die Bytes abgelehnt hat oder die Datei größer als erlaubt ist (413); SpicyApiError, wenn SpicyAPI den Upload abgelehnt hat; SpicyTimeoutError, wenn das Senden länger als uploadTimeoutMs gedauert hat.

const file = await client.uploadFile("./photo.jpg");
console.log(file.uri, "can be used until", file.expiresAt);

uploadBytes(data, options)

Lädt Bytes hoch, die Sie bereits im Speicher haben, etwa eine Datei, die ein Nutzer an Ihren Server geschickt hat. Ein Node.js-Buffer funktioniert.

uploadBytes(data: Uint8Array, options: { contentType: string; signal?: AbortSignal }): Promise<UploadedFile>

contentType ist Pflicht. Ergebnis und Fehler sind dieselben wie bei 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?)

Lädt eine Datei hoch, die Sie als Base64-Text vorliegen haben: entweder als Data-URI wie data:image/png;base64,iVBOR… oder als reines Base64 zusammen mit contentType.

uploadBase64(value: string, options?: { contentType?: string; signal?: AbortSignal }): Promise<UploadedFile>

Kodierung und Größe werden zuerst auf Ihrem Rechner geprüft; ungültige oder zu große Daten werfen einen TypeError, ohne dass etwas hochgeladen wird. Das Ergebnis ist dasselbe wie bei uploadFile.

createUploadUrl und commitUploadedFile

Die drei Hilfsmethoden oben bauen auf diesen beiden Aufrufen einer tieferen Ebene auf. Sie brauchen sie nur, wenn die Bytes von woanders als aus Ihrem Node.js-Prozess gesendet werden.

createUploadUrl(contentType: string, bytes: number, options?: { signal?: AbortSignal }): Promise<UploadTicket>
commitUploadedFile(fileId: string, options?: { signal?: AbortSignal }): Promise<UploadedFile>
  1. createUploadUrl liefert fileId, uploadUrl, method (PUT), headers, maxBytes und expiresAt.
  2. Senden Sie die Datei per PUT an uploadUrl, mit genau den aufgeführten headers und ohne Header Authorization. Die angegebene Größe muss zur Datei passen.
  3. commitUploadedFile(fileId) prüft die gespeicherte Datei und liefert die spicy://-uri. Eine hochgeladene, aber nie bestätigte Datei lässt sich nicht verwenden.

Den gesamten Ablauf, einschließlich Uploads aus dem Browser, beschreibt die Seite Medien.

createDownloadUrl(taskId, key?)

Erstellt einen frischen temporären Download-Link für eine Ergebnisdatei. Das brauchen Sie selten: getTask, waitForTask, run und Webhooks enthalten für jedes Ergebnis bereits eine fertige url.

createDownloadUrl(taskId: string, key?: string, options?: { signal?: AbortSignal }): Promise<DownloadTicket>

Übergeben Sie den key des Assets, um eine Datei auszuwählen, oder lassen Sie ihn für die erste weg. Zurück kommen key, url und expiresAt (etwa 20 Minuten).

Kostet Geld: Nein. Fehler: SpicyApiError mit 409, solange die Datei noch kopiert wird (warten Sie ein paar Sekunden), oder 404, wenn der Task nicht erfolgreich war oder der key nicht zu ihm gehört.

purgeTask(taskId)

Löscht die erzeugten Medien, das Ergebnis (die gespeicherten Ergebnisdaten), den Prompt und andere Eingabetexte eines beendeten Tasks endgültig, früher, als es die Aufbewahrungsfrist täte. Verwenden Sie es, wenn ein Ergebnis nicht aufbewahrt werden darf.

purgeTask(taskId: string, options?: { signal?: AbortSignal }): Promise<PurgeTaskResult>
RückgabefeldBedeutung
contentStatepurged, wenn die Inhalte gelöscht sind. present heißt, dass diesmal nichts entfernt wurde; versuchen Sie es später erneut
purgedAtWann die Inhalte erstmals gelöscht wurden
contentRemovedByuser, wenn Sie sie gelöscht haben
billingRetainedImmer true: Der Datensatz darüber, was der Task gekostet hat, bleibt erhalten
mediaDeletionPendingtrue, solange gespeicherte Dateien noch im Hintergrund entfernt werden, normalerweise weniger als eine Minute

Kostet Geld: Nein – und es ist keine Erstattung. Buchungseintrag, berechneter Betrag, Modell, Status, Zeitstempel und Request-ID bleiben gespeichert. Das Löschen lässt sich nicht rückgängig machen. Ein erneuter Aufruf für denselben Task ist unbedenklich. Fehler: SpicyApiError mit 400, wenn der Task noch queued oder running ist, 404, wenn er nicht existiert. Ein angenommener Task kann nicht abgebrochen werden, und es gibt keine Schnittstelle zum Abbrechen; warten Sie also mit waitForTask, bis er fertig ist, und löschen Sie ihn dann. Siehe Datenaufbewahrung und Löschung.

const result = await client.purgeTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3");
console.log(result.contentState, "billing kept:", result.billingRetained);

Webhook-Funktionen

verifyWebhook und computeWebhookSignature sind einfache Funktionen, keine Methoden des Clients. Beschrieben werden sie mit einem lauffähigen Beispiel unter Webhooks.

Offline-Hilfen und Typen

Diese funktionieren ohne Netzwerkverbindung und ohne Schlüssel.

ExportWas er Ihnen liefert
searchDocumentation(query, limit)Durchsucht einen kleinen eingebauten Index dieser Dokumentation. Liefert bis zu limit Einträge (1 bis 25, Standard 10), jeweils mit slug, title, summary, keywords und url, wobei url die Form https://docs.spicyapi.ai/docs/<slug> hat. Ohne Treffer kommt ein leeres Array zurück; eine leere query liefert die ersten Einträge
DOCUMENTATIONDen gesamten eingebauten Index als Array, ein Eintrag pro Dokumentationsseite
readOpenApiContract()Den OpenAPI-3.1-Vertrag, der dieser SDK-Version beiliegt, als YAML-Text
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_BYTESDie auf dieser Seite beschriebenen Standardwerte. API-Antworten, die größer als MAX_API_RESPONSE_BYTES (4 MiB) sind, werden abgelehnt

TypeScript-Typen für jede Anfrage und jede Antwort werden ebenfalls exportiert, zum Beispiel import type { TaskRecord, ApiModel, Balance } from "@spicyapi/sdk".

ImportpfadEnthält
@spicyapi/sdkAlles auf dieser Seite
@spicyapi/sdk/webhooksNur verifyWebhook, computeWebhookSignature und SpicyWebhookVerificationError – ein schlanker Import für einen Webhook-Empfänger
@spicyapi/sdk/openapiNur die rohen OpenAPI-Typen paths, components und operations

Webhooks

Mit einem Webhook meldet SpicyAPI Ihnen, dass ein Task fertig ist, statt dass Ihr Programm alle paar Sekunden nachfragt. Sie geben jedem Task eine callBackUrl mit, eine Adresse auf Ihrem eigenen Server, und SpicyAPI sendet den fertigen Task-Datensatz dorthin.

Warum Sie jeden Webhook prüfen müssen

Ihre Webhook-Adresse ist eine öffentliche URL: Wer sie errät, kann ihr eine gefälschte Nachricht schicken, etwa „Task erfolgreich, hier ist die Datei“. Glaubt Ihr Code dieser Nachricht, liefert er womöglich einem Kunden die falsche Datei aus oder markiert eine Bestellung als erledigt.

Jede echte Zustellung ist mit einem geheimen Signaturschlüssel signiert, den nur Sie und SpicyAPI kennen. verifyWebhook berechnet diese Signatur aus genau den Bytes, die Sie empfangen haben, neu und vergleicht beide. Außerdem prüft die Funktion den Zustellungszeitpunkt, damit ein Angreifer keine echte Nachricht abfangen und Ihnen Stunden später erneut schicken kann. Handeln Sie erst, wenn eine Nachricht diese Prüfung bestanden hat.

Einrichten

Ihren Signaturschlüssel holen

Bei der Registrierung wird automatisch ein Signaturschlüssel angelegt, sein Wert wird aber nie angezeigt; Sie müssen also einen erzeugen, den Sie kopieren können. Öffnen Sie in der Konsole Webhooks und klicken Sie auf Erzeugen (oder auf Erneuern, wenn die Karte Eingerichtet anzeigt). Der neue Schlüssel wird nur ein einziges Mal angezeigt: Kopieren Sie ihn und legen Sie ihn auf Ihrem Server als SPICY_WEBHOOK_SECRET ab. Geht er verloren, erneuern Sie ihn noch einmal.

Das Erneuern hat keine Übergangszeit: Der alte Schlüssel signiert ab sofort nicht mehr, und auch Zustellungen, die gerade noch wiederholt werden, werden mit dem neuen signiert. Aktualisieren Sie direkt nach dem Erneuern jeden laufenden Empfänger. Details stehen unter Webhooks.

Einen Empfänger unter einer öffentlichen HTTPS-Adresse betreiben

SpicyAPI kann nur eine aus dem Internet erreichbare Adresse aufrufen, die mit https:// beginnt. Reines http://, localhost und Adressen aus privaten Netzen werden abgelehnt. Während der Entwicklung machen Sie Ihren lokalen Server über einen Tunneldienst erreichbar, der ihm eine öffentliche HTTPS-Adresse gibt, oder Sie nutzen Polling, bis Sie deployen. Alle Regeln zur Callback-URL finden Sie unter Webhooks.

callBackUrl mit Ihren Tasks senden

Ergänzen Sie die Anfrage, die Sie an createTask übergeben, um callBackUrl: "https://your-app.example.com/hooks/spicy". Siehe Ergebnisse per Webhook erhalten.

Ein minimaler Empfänger

Dieser Empfänger nutzt nur das eingebaute Modul http von Node.js; es muss also nichts weiter installiert werden. Speichern Sie ihn als receiver.mjs und starten Sie ihn mit 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");

// Zustellungs-IDs, die Sie bereits verarbeitet haben. Im Produktivbetrieb eine
// Datenbanktabelle verwenden, damit die Liste einen Neustart übersteht.
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. Den Rumpf als rohe Bytes lesen. Vor der Prüfung nicht parsen.
  const chunks = [];
  for await (const chunk of request) chunks.push(chunk);
  const rawBody = Buffer.concat(chunks);

  // 2. Signatur und Zeitstempel prüfen.
  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. Schnell antworten. SpicyAPI wartet höchstens 15 Sekunden und wiederholt dann.
  response.writeHead(204).end();

  // 4. Dieselbe Zustellung kann mehrfach ankommen. Jede nur einmal verarbeiten.
  if (!event.deliveryId || handled.has(event.deliveryId)) return;
  handled.add(event.deliveryId);

  if (!("data" in event.payload)) return; // Rümpfe der Version 2 enthalten den Task unter data
  const task = event.payload.data; // dieselben Felder, die getTask() liefert
  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"));

Worauf es dabei ankommt:

  • Rohe Bytes. Die Signatur deckt genau die Bytes ab, die SpicyAPI gesendet hat. Parst ein Framework das JSON zuerst und Sie wandeln es zurück in Text, ändern sich die Bytes, und jede Prüfung schlägt fehl. In Express verwenden Sie auf dieser Route express.raw({ type: "application/json" }).
  • Schnell antworten. Jede 2xx-Antwort bedeutet „empfangen“. Kommt innerhalb von 15 Sekunden keine Antwort oder ein anderer Status, wiederholt SpicyAPI die Zustellung mehrmals über die folgenden Stunden. Erledigen Sie langsame Arbeit erst nach der Antwort.
  • Duplikate kommen vor. Wiederholungen und Netzwerkstörungen können dieselbe Nachricht zweimal zustellen. event.deliveryId bleibt über Wiederholungen hinweg gleich; merken Sie sich also die IDs, die Sie verarbeitet haben.
  • Ob es geklappt hat, steht in state. Eine Zustellung für einen fehlgeschlagenen Task ist trotzdem eine gültige Zustellung. Lesen Sie task.state, nie das code des Envelopes.
  • Im Produktivbetrieb speichern Sie das geprüfte Ereignis zuerst in Ihrer Datenbank, antworten erst nach erfolgreichem Speichern mit 2xx und ignorieren Task-IDs, die Ihre Anwendung nicht erstellt hat. Die Seite Webhooks enthält eine Fassung mit Datenbank.

Den Empfänger vor dem Livegang testen

computeWebhookSignature erzeugt eine echte Signatur, sodass Sie sich selbst eine signierte Testzustellung schicken können. Lassen Sie receiver.mjs in einem Terminal laufen, setzen Sie in einem zweiten dasselbe SPICY_WEBHOOK_SECRET und führen Sie dort Folgendes aus:

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); // erwartet: 204

Führen Sie es zweimal aus: Der Empfänger gibt den Task nur einmal aus, weil die zweite Zustellung dieselbe request_id hat. Ändern Sie nach dem Signieren ein Zeichen von body, antwortet der Empfänger mit 401.

verifyWebhook(options)

verifyWebhook(options: VerifyWebhookOptions): VerifiedWebhook

Eigenschaft

Typ

Zurück kommen payload (der geparste Rumpf; bei Version 2 steht der Task in payload.data), payloadVersion, taskId, deliveryId (die request_id der Version 2, über Wiederholungen hinweg identisch) und timestamp. Die Funktion ist synchron: Kein await nötig.

Scheitert die Prüfung, wirft sie SpicyWebhookVerificationError, dessen reason einer dieser Werte ist: invalid_signature, stale_timestamp, invalid_timestamp, invalid_json, invalid_payload, body_too_large oder invalid_secret. Antworten Sie mit 401 und handeln Sie nicht auf Grundlage der Nachricht.

computeWebhookSignature(taskId, timestamp, rawBody, secret) liefert die Base64-Signatur für einen Rumpf. Sie brauchen die Funktion nur für Tests wie den oben. Den vollständigen Signaturalgorithmus für andere Sprachen finden Sie unter Webhooks.

TypeScript-Tipp: payload ist als eines der beiden Formate typisiert. Grenzen Sie es mit "data" in event.payload ein, wie der Empfänger es tut, oder übergeben Sie den erwarteten Typ: verifyWebhook<components["schemas"]["TaskRecordEnvelope"]>(…) mit import type { components } from "@spicyapi/sdk".

Fehler behandeln

Es kann auf zwei verschiedene Arten schiefgehen, und beide brauchen eine andere Behandlung:

  1. Ein Aufruf scheitert. Das SDK wirft einen Fehler: Ihr Schlüssel war falsch, Ihr Guthaben zu niedrig, die Verbindung ist abgebrochen. Fangen Sie ihn mit try / catch ab.
  2. Ein Task scheitert. Der Aufruf hat funktioniert, die Generierung nicht. Es wird nichts geworfen: Der zurückgegebene Task hat state: "failed" und einen errorCode. Bei fehlgeschlagenen oder abgelaufenen Tasks wird die Reservierung normalerweise vollständig freigegeben. Einige Inhaltsablehnungen werden zum angenommenen Angebotspreis berechnet, sofern die Modellseite diese Ausnahme ausdrücklich nennt. Prüfen Sie den endgültigen Betrag anhand von settled und cost.

Die Fehlerklassen

KlasseWann sie auftrittNützliche Felder
SpicyApiErrorSpicyAPI hat mit einem Fehler geantwortet, etwa wegen eines abgelehnten Schlüssels oder zu niedrigen Guthabenscode (der Business-Code, nach dem Sie handeln, etwa 40201), status (HTTP-Status), message, requestId, retryAfterSeconds, rateLimitLimit, rateLimitRemaining
SpicyTransportErrorEs kam keine verwertbare Antwort an: Netzwerkausfall, DNS-Problem oder eine Antwort größer als 4 MiBmessage, cause (der zugrunde liegende Fehler)
SpicyTimeoutErrorEine einzelne Anfrage hat länger als timeoutMs (bzw. uploadTimeoutMs) gedauert. Eine Unterart von SpicyTransportErrortimeoutMs
SpicyWaitTimeoutErrorrun oder waitForTask ist die Zeit ausgegangen. Der Task läuft nochtaskId, timeoutMs, lastTask
SpicyUploadErrorDer Speicher hat eine hochgeladene Datei abgelehnt, oder sie war zu großstatus
SpicyWebhookVerificationErrorEin Webhook hat die Prüfung nicht bestandenreason
TypeErrorDer Aufruf selbst war ungültig – kein Schlüssel gesetzt, eine Option außerhalb des Bereichs, eine leere ID. Es wurde nichts gesendetmessage

Entscheiden Sie anhand von code, nie anhand von message: Meldungen sind für Menschen gedacht, ihr Wortlaut kann sich ändern, und sie folgen der Einstellung für die Fehlersprache Ihres Kontos. Geben Sie requestId immer an, wenn Sie den Support kontaktieren; damit finden wir genau diese Anfrage.

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"; // eine echte ID aus listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

const idempotencyKey = randomUUID(); // aufbewahren: Für einen neuen Versuch brauchen Sie denselben

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) {
    // Ihr Warten ist zu Ende, der Task nicht. Später mit derselben ID nachsehen.
    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) {
    // Netzwerkausfall oder lokales Zeitlimit: Der Task existiert vielleicht, vielleicht nicht.
    // Dieselbe Anfrage mit DEMSELBEN idempotencyKey erneut senden, um es gefahrlos herauszufinden.
    console.log(`Network problem (${error.message}). Retry with key ${idempotencyKey}.`);
  } else {
    throw error; // ein TypeError hier ist ein Fehler im Aufruf selbst, etwa ein fehlender Schlüssel
  }
}

Häufige Fehlercodes

error.codeBedeutungLohnt ein neuer Versuch?
400Ungültige Anfrage, meist ein Eingabefeld, das das Modell nicht annimmtNein. Anfrage korrigieren
401Schlüssel fehlt, ist falsch formatiert, widerrufen oder abgelaufenNein. Schlüssel prüfen
40201Guthaben reicht für die Reservierung nichtNein. Zuerst aufladen
40202Ein Ausgabenlimit ist erreichtNein. Limit anheben oder warten, bis das Tages- oder Monatslimit neu beginnt
40301Dieser Schlüssel darf dieses Modell nicht verwendenNein. Modellliste des Schlüssels ändern
404Kein solches Modell bzw. kein solcher Task für diesen SchlüsselNein
409Konflikt mit dem aktuellen Zustand, etwa ein Idempotenzschlüssel, der für andere Arbeit wiederverwendet wurdeKommt darauf an; siehe Fehler
40901Kostenschätzung abgelaufen oder Preis geändertNeu schätzen lassen, dann bestätigen
429Zu viele AnfragenJa, nach retryAfterSeconds. Lesende Aufrufe wiederholt das SDK bereits für Sie
500Bei SpicyAPI ist etwas kaputtgegangenJa, mit einer Pause
50301Das Modell ist gerade nicht verfügbarJa, später – oder ein anderes Modell wählen

Scheitert ein Task, ist task.errorCode einer dieser Werte: invalid_request, unsupported_combination, content_rejected, rate_limited, upstream_unavailable, generation_failed, timeout, invalid_asset oder upstream_failed. retryTask sendet die ursprüngliche Eingabe unverändert erneut; wählen Sie den nächsten Schritt deshalb nach dem Code:

task.errorCodeWas zu tun ist
rate_limited, upstream_unavailable, timeout, upstream_failedWarten, dann einmal retryTask. Wiederholt sich ein timeout, versuchen Sie mit createTask eine kürzere Dauer oder eine niedrigere Auflösung
generation_failedPrompt oder Bild ändern und mit createTask einen neuen Task senden; dieselbe Eingabe scheitert meist auf dieselbe Weise
invalid_assetDie Datei erneut hochladen und mit createTask einen neuen Task mit der neuen uri senden; eine Wiederholung würde die alte Datei verwenden
invalid_request, unsupported_combinationNicht unverändert wiederholen. Einstellungen ändern, dann createTask verwenden
content_rejectedNicht unverändert wiederholen. Das gewählte Modell hat die Anfrage nach seinen eigenen Richtlinien abgelehnt; ändern Sie den Prompt oder wählen Sie ein anderes Modell

Die vollständige Tabelle finden Sie unter Fehler.

Ergebnisse sichern

  • Eine Ergebnis-url funktioniert etwa 20 Minuten. Rufen Sie getTask erneut auf, um eine frische zu erhalten.
  • Erzeugte Dateien werden nach höchstens etwa 14 Tagen gelöscht, früher, wenn Ihr Konto oder retentionSeconds es so festlegt. Hochgeladene Eingabedateien halten einen Tag, gespeicherte Prompts 30 Tage.
  • Laden Sie alles, was Sie behalten wollen, gleich nach dem Erfolg eines Tasks in Ihren eigenen Speicher. Speichern Sie nicht die temporäre url selbst in Ihrer Datenbank. Details: Datenaufbewahrung und Löschung.

Häufige Rezepte

Die Rezepte verwenden MODEL_ID_FROM_CATALOG als Platzhalter. Bevor Sie eines ausführen, ersetzen Sie ihn durch den Wert model eines passenden Eintrags aus dem Live-Katalog (listModels() oder GET /api/v1/models), bei dem enabled und available beide true sind. Vergessen Sie das, bricht jedes Rezept mit einer klaren Meldung ab.

Bild zu Video: zuerst ein lokales Bild hochladen

image-to-video.mjs
import { randomUUID } from "node:crypto";
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();

// Durch eine Image-to-Video-Modell-ID aus dem Live-Katalog ersetzen:
// 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. Das lokale Bild hochladen. Kostenlos. Die spicy://-Adresse funktioniert einen Tag.
const file = await client.uploadFile("./photo.jpg");
console.log("Uploaded:", file.uri);

// 2. Das Schema des Modells ausgeben: welches Feld das Bild nimmt, welche Felder Pflicht sind.
const { inputSchema } = await client.getModel(MODEL_ID);
console.log(JSON.stringify(inputSchema, null, 2));

// 3. Das Video erstellen und darauf warten. Video dauert oft mehrere Minuten.
const task = await client.run(
  {
    model: MODEL_ID,
    input: {
      prompt: "Slow push-in, light rain, neon reflections on wet stone",
      image_url: file.uri, // den Feldnamen verwenden, den das Schema oben zeigt
    },
  },
  {
    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);

Maßgeblich für Feldnamen und Pflichtfelder ist das ausgegebene Schema; ein Videomodell kann außerdem eine Dauer oder eine Auflösung verlangen, und der Preis hängt oft davon ab. Liegt Ihr Bild bereits unter einer öffentlichen HTTPS-Adresse, überspringen Sie den Upload und schreiben diese Adresse direkt in das Bildfeld. Möchten Sie den Preis sehen, führen Sie vorher quoteTask mit derselben Anfrage aus.

Einen Stapel mit begrenzter Parallelität ausführen

Hunderte Tasks auf einmal zu starten, kann Ihre Ausgabenlimits und Ihr Rate Limit sprengen. Dieses Skript arbeitet eine Liste von Prompts ab, mit höchstens zwei gleichzeitig laufenden Tasks.

batch.mjs
import { SpicyApiError, SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();

const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // eine echte ID aus listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

// Den Stapelnamen jedes Mal ändern, wenn Sie die Prompts ändern.
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; // wie viele Tasks gleichzeitig laufen dürfen

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] } },
        {
          // Gleicher Stapelname + gleiche Position = gleicher Schlüssel. Stürzt das Skript ab
          // und Sie starten es innerhalb von 24 Stunden erneut, erhalten Sie die bestehenden
          // Tasks zurück, statt sie doppelt zu bezahlen.
          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);
      // Guthaben aufgebraucht (40201) oder Ausgabenlimit erreicht (40202): keine neuen Tasks mehr starten.
      if (error instanceof SpicyApiError && (error.code === 40201 || error.code === 40202)) {
        nextIndex = prompts.length;
      }
    }
  }
}

await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
console.table(results);
  • Warum die Schlüssel aus dem Stapelnamen gebildet werden. Ein Idempotenzschlüssel, der an „dieser Stapel, Eintrag 3“ gebunden ist, sorgt dafür, dass ein neuer Lauf nach einem Absturz die bereits angelegten Tasks wiederverwendet. Ein zufälliger Schlüssel bei jedem Lauf würde dasselbe Bild erneut bezahlen.
  • Warum sich der Stapelname mit den Prompts ändern muss. Wird ein Idempotenzschlüssel für eine andere Anfrage wiederverwendet, wird sie mit 409 abgelehnt.
  • CONCURRENCY wählen. Jeder laufende Task reserviert einen Teil Ihres Guthabens, und jede Statusabfrage zählt gegen das Rate Limit Ihres Kontos. Fangen Sie klein an und erhöhen Sie den Wert, sobald der Stapel sauber durchläuft.

Ergebnisse per Webhook statt Polling erhalten

In einer Web-App oder einem Hintergrund-Worker ist es Verschwendung, innerhalb einer Anfrage zu warten. Erstellen Sie den Task mit einer callBackUrl, speichern Sie die Task-ID und überlassen Sie das Ergebnis dem Empfänger, sobald es eintrifft.

webhook-create.mjs
import { randomUUID } from "node:crypto";
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // eine echte ID aus listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

// In einer echten Anwendung diesen Schlüssel vor dem Aufruf mit der Kundenbestellung speichern.
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 jetzt speichern. Ist der Task fertig, ruft SpicyAPI Ihren Empfänger auf.

Halten Sie ein Sicherheitsnetz für Zustellungen bereit, die nie ankommen, etwa weil Ihr Server während aller Wiederholungen nicht erreichbar war: Rufen Sie gelegentlich getTask für gespeicherte Tasks auf, deren deadlineAt längst überschritten ist und zu denen kein Webhook kam, oder blättern Sie durch listTasks. Den Verlauf der Webhook-Zustellungen samt Schaltfläche zum erneuten Senden finden Sie in der Konsole auf der Seite Webhooks.

Einen fehlgeschlagenen Task wiederholen

retry.mjs
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();

const taskId = process.argv[2]; // ausführen als: node retry.mjs job_...
if (!taskId) throw new Error("Usage: node retry.mjs <taskId>");

// Eine Wiederholung sendet die ursprüngliche Eingabe unverändert, sie lohnt also nur bei vorübergehenden Fehlern.
// Bei allen anderen Codes die Anfrage ändern und stattdessen createTask verwenden.
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 {
  // Ein fester Schlüssel pro Wiederholungsversuch: Zweimal ausgeführt, entsteht eine Wiederholung, nicht zwei.
  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);
}

Das Skript wiederholt nur Fehler, die beim nächsten Mal anders ausgehen können – ein ausgelasteter oder vorübergehend nicht verfügbarer Dienst, eine Zeitüberschreitung oder ein unbekannter Fehler –, und zwar nur einmal, mit einem festen Idempotenzschlüssel. Bei invalid_asset laden Sie die Datei erneut hoch und erstellen einen neuen Task, denn die Wiederholung würde den abgelaufenen Upload verwenden. Bei generation_failed ändern Sie Prompt oder Bild und erstellen einen neuen Task. Bei content_rejected, invalid_request und unsupported_combination ändern Sie die Anfrage, statt sie zu wiederholen. Scheitert auch die eine Wiederholung, hören Sie auf und sehen sich den Fehler an; bei einem wiederholten timeout versuchen Sie mit createTask eine kürzere Dauer oder eine niedrigere Auflösung. Siehe Fehler.

Fehlerbehebung und FAQ

Quellcode

Das Paket steht unter MIT-Lizenz und wird offen unter github.com/spicyapi-ai/spicy-sdk entwickelt. Dort lesen Sie den Code hinter jeder Methode dieser Seite, melden Verhalten, das von der Beschreibung abweicht, oder schlagen eine Änderung vor.

Nächste Schritte

Auf dieser Seite