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 aufrufen | Dieses SDK |
| Ohne Code aus einem Terminal heraus generieren | Die CLI |
| Einen KI-Assistenten wie Claude Code oder Cursor SpicyAPI für Sie aufrufen lassen | Den MCP-Server und den Agent Skill |
| Ein Chat- oder Textmodell nutzen | Den offiziellen OpenAI-Client, ausgerichtet auf SpicyAPI – siehe Text und Streaming |
| Python, Go, PHP oder Java verwenden | Das offizielle Python-SDK, Go-SDK, PHP-SDK oder Java-SDK |
| Irgendeine andere Sprache verwenden | Direkte 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
openaierledigt 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
| Begriff | Bedeutung hier |
|---|---|
| Modell-ID | Der exakte Name eines Modells in der Form publisher/model/task. Kopieren Sie ihn immer aus dem Live-Katalog (listModels()), nie aus dem Gedächtnis |
| Eingabe | Die Einstellungen, die Sie mit einem Task senden, etwa prompt oder image_url. Jedes Modell hat seine eigene Liste erlaubter Felder, sein Schema |
| Task | Ein 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 |
| Reservierung | Wird 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üssel | Eine 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 |
| Webhook | Eine 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öchten | Methode | Kostet Geld? |
|---|---|---|
| Prüfen, ob SpicyAPI erreichbar ist (ohne Schlüssel) | getStatus | Nein |
| Die aktuelle Modellliste, Ihre Preise und die Felder jedes Modells sehen | listModels, getModel | Nein |
| Den genauen Preis einer Anfrage erfahren, bevor Geld ausgegeben wird | quoteTask | Nein |
| Eine Generierung starten | createTask | Ja |
| Eine Generierung starten und in einem Aufruf auf das Ergebnis warten | run | Ja |
| Einen Task einmal ansehen | getTask | Nein |
| Warten, bis ein Task fertig ist | waitForTask | Nein |
| Einen fehlgeschlagenen Task erneut versuchen | retryTask | Ja (ein neuer Task) |
| Die mit Ihrem Schlüssel erstellten Tasks auflisten | listTasks | Nein |
| Ihr Guthaben abfragen | getBalance | Nein |
| Sehen, was Sie ausgegeben haben, nach Tag und nach Modell | getUsage | Nein |
| Eine lokale Bild-, Video- oder Audiodatei hochladen | uploadFile, uploadBytes, uploadBase64 | Nein |
| Einen frischen Download-Link für ein Ergebnis erhalten | createDownloadUrl | Nein |
| Die gespeicherten Dateien und den Prompt eines beendeten Tasks löschen | purgeTask | Nein (und keine Erstattung) |
| Prüfen, ob ein Webhook wirklich von SpicyAPI stammt | verifyWebhook (eine einfache Funktion) | Nein |
| Die Dokumentation durchsuchen oder den API-Vertrag offline lesen | searchDocumentation, 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 -vAusgegeben 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| Befehl | Was er tut |
|---|---|
mkdir spicy-demo | Legt einen leeren Ordner für Ihr Projekt an |
cd spicy-demo | Wechselt im Terminal in diesen Ordner; die folgenden Befehle wirken dort |
npm init -y | Legt package.json an, die Datei mit den Einstellungen und Bibliotheken Ihres Projekts |
npm pkg set type=module | Teilt Node.js mit, dass Ihre Dateien die moderne import-Syntax verwenden, die dieses SDK voraussetzt |
npm install @spicyapi/sdk | Lä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
- 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.
- Öffnen Sie in der Konsole die Seite API-Schlüssel und klicken Sie auf Schlüssel anlegen. Nur ein Name ist Pflicht.
- 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:
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.mjsSpicyAPI is up
Checked at 2026-09-17T08:00:00.000ZDiese 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.
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 --yesModel: <display name> (<model ID>)
Price: $<amount> USD
Nothing was charged. Run again with --yes to generate the image.
Model: <display name> (<model ID>)
Price: $<amount> USD
Task accepted: job_…
State: running
State: succeeded
Saved result.png. Charged: $<amount>Was jeder Schritt tut
getStatus()fragt den Health-Endpunkt von SpicyAPI, ob der Dienst läuft. Bei einem Netzwerkproblem wirft er nie einen Fehler, sondern meldet stattdessenoperational: false.listModels()liefert den Live-Modellkatalog mit den Preisen Ihres Kontos. Verwenden Sie nur Einträge, bei denenenabledundavailablebeidetruesind.includeExamples: truefügt Beispiel-Eingaben hinzu, die der Server bereits validiert hat – der sicherste Ausgangspunkt. Was Sie später senden, ist das Feldmodeldes Eintrags, die Modell-ID.- Die Eingabe. Jedes Modell nimmt andere Felder an; ein Feld, das das Modell nicht kennt, wird mit dem Fehler
400abgelehnt. Wer ein validiertes Beispiel kopiert und nur den Prompt ändert, umgeht das. Um alle Felder eines Modells zu sehen, rufen SiegetModel()auf und lesen desseninputSchema. 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.run()erstellt den Task und fragt danach so lange nach, bis er fertig ist. MitquoteIdundexpectedCostsagen Sie: „Nur zu dem Preis annehmen, der mir angezeigt wurde.“ Hat sich der Preis geändert, scheitert der Aufruf mit40901, statt einen anderen Betrag zu berechnen. DeridempotencyKeymacht 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.- 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 vonsettledundcost. - 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 SiegetTask()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:
| Aufruf | Automatisch wiederholt? |
|---|---|
Lesende Aufrufe: getStatus, listModels, getModel, getTask, listTasks, getBalance, getUsage; außerdem quoteTask, purgeTask, commitUploadedFile | Ja |
createTask, run (dessen Erstellungsschritt), retryTask | Nur 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, createDownloadUrl | Nein |
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ückgabefeld | Bedeutung |
|---|---|
operational | true, wenn der Dienst antwortet. Dieses Feld sollten Sie verwenden |
health | Das Rohergebnis der Health-Prüfung: path, ok, HTTP-status, die ersten 512 Zeichen von body und error, wenn keine Antwort ankam |
readiness | Dieselbe 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 |
checkedAt | Wann 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:
| Feld | Bedeutung |
|---|---|
model | Die Modell-ID. Senden Sie genau diesen Wert, wenn Sie einen Task erstellen |
displayName | Ein lesbarer Name |
modality, tasks | Was das Modell erzeugt und welche Aufgabe es erledigt |
provider, family | Wer das Modell entwickelt hat und zu welcher Produktlinie es gehört |
enabled, available | Beide müssen true sein, bevor Sie das Modell verwenden |
pricing | Ein 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 |
startingPrice | Die günstigste Stufe. Für einen exakten Betrag verwenden Sie quoteTask |
quantityField | Das Eingabefeld, das die abgerechnete Menge bestimmt, etwa eine Dauer. Leer bedeutet eine Einheit pro Aufruf |
taskTimeoutSeconds | Wie lange ein Task laufen darf, bevor er als expired endet |
maxOutputDurationSeconds | Die längste Ausgabe, die das Modell erlaubt, sofern es eine Dauer hat |
inputSchema, examples | Nur 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ückgabefeld | Bedeutung |
|---|---|
quoteId | Übergeben Sie sie an createTask oder run, um diesen Preis festzuschreiben |
estimatedCost | Der Betrag, der reserviert wird, wenn Sie den Task jetzt erstellen |
maxCharge | Das Höchste, was diese Anfrage kosten kann |
currency | Immer USD |
quantity, unit | Die abgerechnete Menge, etwa "5" und per_second |
expiresAt | Die 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ückgabefeld | Bedeutung |
|---|---|
taskId | Die ID des neuen Tasks. Speichern Sie sie sofort |
state | queued bei einem neuen Task. Haben Sie einen Idempotenzschlüssel wiederholt, der aktuelle Status des ursprünglichen Tasks |
estimatedCost | Der jetzt reservierte Betrag, zugleich das Höchste, was dieser Task kosten kann |
deadlineAt | Wann 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.code | Bedeutung | Was zu tun ist |
|---|---|---|
400 | Die Eingabe passt nicht zum Schema des Modells, oder die callBackUrl ist nicht erlaubt | Mit getModel().inputSchema vergleichen und die Anfrage korrigieren |
401 | Der Schlüssel fehlt, ist falsch formatiert oder wurde widerrufen | SPICY_API_KEY prüfen |
40201 | Ihr Guthaben reicht für die Reservierung nicht | In der Konsole unter Abrechnung aufladen |
40202 | Ein Tages-, Monats- oder Gesamtlimit ist erreicht | Das Limit auf der Seite API-Schlüssel anheben oder warten, bis es neu beginnt |
40301 | Dieser Schlüssel darf dieses Modell nicht verwenden | Die Modellliste des Schlüssels in der Konsole ändern |
409 | Dieser Idempotenzschlüssel wurde bereits für eine andere Anfrage verwendet | Für neue Arbeit einen neuen Idempotenzschlüssel verwenden |
40901 | Die Kostenschätzung ist abgelaufen, oder Preis oder Anfrage haben sich geändert | Neu schätzen lassen und den neuen Preis bestätigen |
413 | Der Anfragerumpf ist zu groß | Mediendateien hochladen, statt sie direkt einzubetten |
429 | Zu viele Anfragen | error.retryAfterSeconds abwarten, dann erneut versuchen |
50301 | Das Modell kann gerade keine Tasks annehmen | Spä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, wenntimeoutMsabläuft, nachdem der Task angenommen wurde. Der Task läuft bei SpicyAPI weiter. Rufen Sie späterwaitForTask(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
onAcceptedoderonUpdategeworfen wird, beendet das Warten; der Task selbst läuft weiter. TypeErrorbei einem ungültigentimeoutMsoderintervalMs. 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>| Feld | Bedeutung |
|---|---|
taskId, model | Welcher Task, mit welchem Modell erstellt |
state | queued oder running, solange er arbeitet; succeeded, failed oder expired, wenn er beendet ist (canceled kommt nur bei alten Datensätzen vor) |
output.assets | Die Ergebnisdateien, beschrieben in der nächsten Tabelle. Nur bei succeeded |
output.text | Textausgabe, bei Modellen, die Text zurückgeben |
errorCode, errorMessage | Warum ein Task fehlgeschlagen ist. Entscheiden Sie anhand von errorCode; der Wortlaut der Meldung kann sich ändern |
cost | Solange settled false ist, der reservierte Betrag; sobald es true ist, die endgültige Belastung |
settled | Ob cost endgültig ist |
createdAt, completedAt, deadlineAt | Wann er erstellt wurde, wann er fertig war und wann er ablaufen würde |
input | Die Eingabe, wie sie gespeichert ist. Wird entfernt, wenn die Aufbewahrungsfrist für Prompts endet |
sourceTaskId | Gesetzt bei einem Task, den retryTask erstellt hat; verweist auf das Original |
contentState | present; expired (durch Aufbewahrungsregeln entfernt); oder purged (Sie haben ihn mit purgeTask gelöscht) |
retention | Wann die Dateien (outputsExpireAt) und der Prompt (promptsExpireAt) entfernt werden |
Jeder Eintrag in output.assets:
| Feld | Bedeutung |
|---|---|
url | Ein temporärer Download-Link. Laden Sie ohne Ihren API-Schlüssel herunter |
expiresAt | Wann dieser Link nicht mehr funktioniert, normalerweise nach etwa 20 Minuten. Rufen Sie getTask erneut auf, um einen frischen Link zu erhalten |
mime | Der Dateityp, etwa image/png oder video/mp4 |
width, height, durationSeconds, bytes | Angaben zur Größe, sofern zutreffend |
pending | true, solange die Datei noch in den Speicher kopiert wird. Fragen Sie in ein paar Sekunden erneut |
unavailable | true, wenn die Datei endgültig weg ist. Warten hilft nicht; generieren Sie neu |
key | Ein 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>| Feld | Bedeutung |
|---|---|
available | Geld, mit dem neue Tasks bezahlt werden können. Kann negativ sein, wenn Ihr Konto eine bewilligte Kreditlinie nutzt |
held | Geld, das für noch laufende Tasks zurückgelegt ist |
total | available plus held |
funding | Optionale 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.
| Feld | Bedeutung |
|---|---|
totalCalls | Anzahl der Tasks im Zeitraum |
totalSpend | Nur endgültige Belastungen; noch reserviertes Geld zählt nicht mit |
days | Ein Eintrag pro Tag mit Aufrufen: day, calls, succeeded, failed, spend |
models | Ein 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ückgabefeld | Bedeutung |
|---|---|
uri | Die spicy://-Adresse für die Eingabe eines Tasks |
expiresAt | Wann die Adresse nicht mehr funktioniert (nach einem Tag) |
fileId, status, bytes, contentType, sha256 | Angaben zur gespeicherten Datei; status ist ready |
width, height, durationSeconds | Gemessene 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>createUploadUrlliefertfileId,uploadUrl,method(PUT),headers,maxBytesundexpiresAt.- Senden Sie die Datei per
PUTanuploadUrl, mit genau den aufgeführtenheadersund ohne HeaderAuthorization. Die angegebene Größe muss zur Datei passen. commitUploadedFile(fileId)prüft die gespeicherte Datei und liefert diespicy://-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ückgabefeld | Bedeutung |
|---|---|
contentState | purged, wenn die Inhalte gelöscht sind. present heißt, dass diesmal nichts entfernt wurde; versuchen Sie es später erneut |
purgedAt | Wann die Inhalte erstmals gelöscht wurden |
contentRemovedBy | user, wenn Sie sie gelöscht haben |
billingRetained | Immer true: Der Datensatz darüber, was der Task gekostet hat, bleibt erhalten |
mediaDeletionPending | true, 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.
| Export | Was 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 |
DOCUMENTATION | Den 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_BYTES | Die 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".
| Importpfad | Enthält |
|---|---|
@spicyapi/sdk | Alles auf dieser Seite |
@spicyapi/sdk/webhooks | Nur verifyWebhook, computeWebhookSignature und SpicyWebhookVerificationError – ein schlanker Import für einen Webhook-Empfänger |
@spicyapi/sdk/openapi | Nur 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.
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.deliveryIdbleibt ü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 Sietask.state, nie dascodedes Envelopes. - Im Produktivbetrieb speichern Sie das geprüfte Ereignis zuerst in Ihrer Datenbank, antworten erst nach erfolgreichem Speichern mit
2xxund 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:
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: 204Fü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): VerifiedWebhookEigenschaft
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:
- 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/catchab. - Ein Task scheitert. Der Aufruf hat funktioniert, die Generierung nicht. Es wird nichts geworfen: Der zurückgegebene Task hat
state: "failed"und einenerrorCode. 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 vonsettledundcost.
Die Fehlerklassen
| Klasse | Wann sie auftritt | Nützliche Felder |
|---|---|---|
SpicyApiError | SpicyAPI hat mit einem Fehler geantwortet, etwa wegen eines abgelehnten Schlüssels oder zu niedrigen Guthabens | code (der Business-Code, nach dem Sie handeln, etwa 40201), status (HTTP-Status), message, requestId, retryAfterSeconds, rateLimitLimit, rateLimitRemaining |
SpicyTransportError | Es kam keine verwertbare Antwort an: Netzwerkausfall, DNS-Problem oder eine Antwort größer als 4 MiB | message, cause (der zugrunde liegende Fehler) |
SpicyTimeoutError | Eine einzelne Anfrage hat länger als timeoutMs (bzw. uploadTimeoutMs) gedauert. Eine Unterart von SpicyTransportError | timeoutMs |
SpicyWaitTimeoutError | run oder waitForTask ist die Zeit ausgegangen. Der Task läuft noch | taskId, timeoutMs, lastTask |
SpicyUploadError | Der Speicher hat eine hochgeladene Datei abgelehnt, oder sie war zu groß | status |
SpicyWebhookVerificationError | Ein Webhook hat die Prüfung nicht bestanden | reason |
TypeError | Der Aufruf selbst war ungültig – kein Schlüssel gesetzt, eine Option außerhalb des Bereichs, eine leere ID. Es wurde nichts gesendet | message |
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.
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.code | Bedeutung | Lohnt ein neuer Versuch? |
|---|---|---|
400 | Ungültige Anfrage, meist ein Eingabefeld, das das Modell nicht annimmt | Nein. Anfrage korrigieren |
401 | Schlüssel fehlt, ist falsch formatiert, widerrufen oder abgelaufen | Nein. Schlüssel prüfen |
40201 | Guthaben reicht für die Reservierung nicht | Nein. Zuerst aufladen |
40202 | Ein Ausgabenlimit ist erreicht | Nein. Limit anheben oder warten, bis das Tages- oder Monatslimit neu beginnt |
40301 | Dieser Schlüssel darf dieses Modell nicht verwenden | Nein. Modellliste des Schlüssels ändern |
404 | Kein solches Modell bzw. kein solcher Task für diesen Schlüssel | Nein |
409 | Konflikt mit dem aktuellen Zustand, etwa ein Idempotenzschlüssel, der für andere Arbeit wiederverwendet wurde | Kommt darauf an; siehe Fehler |
40901 | Kostenschätzung abgelaufen oder Preis geändert | Neu schätzen lassen, dann bestätigen |
429 | Zu viele Anfragen | Ja, nach retryAfterSeconds. Lesende Aufrufe wiederholt das SDK bereits für Sie |
500 | Bei SpicyAPI ist etwas kaputtgegangen | Ja, mit einer Pause |
50301 | Das Modell ist gerade nicht verfügbar | Ja, 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.errorCode | Was zu tun ist |
|---|---|
rate_limited, upstream_unavailable, timeout, upstream_failed | Warten, dann einmal retryTask. Wiederholt sich ein timeout, versuchen Sie mit createTask eine kürzere Dauer oder eine niedrigere Auflösung |
generation_failed | Prompt oder Bild ändern und mit createTask einen neuen Task senden; dieselbe Eingabe scheitert meist auf dieselbe Weise |
invalid_asset | Die 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_combination | Nicht unverändert wiederholen. Einstellungen ändern, dann createTask verwenden |
content_rejected | Nicht 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-
urlfunktioniert etwa 20 Minuten. Rufen SiegetTaskerneut auf, um eine frische zu erhalten. - Erzeugte Dateien werden nach höchstens etwa 14 Tagen gelöscht, früher, wenn Ihr Konto oder
retentionSecondses 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
urlselbst 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
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.
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
409abgelehnt. CONCURRENCYwä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.
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
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
Modellkatalog und Schemas
Wie Sie das inputSchema eines Modells lesen und gültige Eingaben wählen
Asynchrone Tasks
Task-Status, Polling und was jedes Feld bedeutet
Webhooks
Zustellformat, Wiederholungsplan und Prüfung in anderen Sprachen
Medien
Uploads, Base64-Grenzen und Ergebnis-Downloads
Fehler
Jeder Fehlercode und welche sich zu wiederholen lohnen
Abrechnung
Reservierung, Abrechnung, Ausgabenlimits und Ihr Guthaben
Idempotenz
Sicher wiederholen, ohne doppelt zu zahlen
Kommandozeilen-Tool
Dieselben Funktionen im Terminal, ohne Code zu schreiben
MCP-Server
Einen KI-Coding-Assistenten SpicyAPI für Sie nutzen lassen
Python-SDK
Derselbe Ablauf in Python, ganz ohne Laufzeitabhängigkeiten
Go-SDK
Derselbe Ablauf in Go, allein mit der Standardbibliothek
PHP-SDK
Derselbe Ablauf in PHP, ganz ohne Bibliotheksabhängigkeiten
Java-SDK
Derselbe Ablauf in Java, mit einer versiegelten Fehlertaxonomie
Integration für den Produktivbetrieb
Eine Checkliste vor dem Start einer Integration, die sich von Fehlern erholt – das richtige Modell, geschützte Schlüssel und geschütztes Guthaben, bestätigte Kosten, dauerhaft verfolgte Tasks, zuverlässige Callbacks und die Tests vor dem Livegang.
Python-SDK
Das offizielle Python-SDK für SpicyAPI (spicyapi) – installieren, die erste Generierung schätzen und ausführen, eine lokale Datei hochladen, Webhooks prüfen und jeden Fehlercode nachschlagen.

