Schnellstart
Key erstellen, Task starten und Ergebnis abholen – der vollständige Ablauf von Anfang bis Ende.
SpicyAPI bündelt Bild- und Videogenerierung hinter einem Authentifizierungs-, Task- und Ergebnisprotokoll und bietet Textmodelle in bekannten OpenAI-, Anthropic- und Gemini-Formaten. Beim Modellwechsel behalten Sie denselben Ablauf und bauen input aus dessen inputSchema auf, der Liste akzeptierter Felder, Typen und Werte.
Dieser Leitfaden verwendet die native Task-API unter https://api.spicyapi.ai/api/v1. Text- und Videokompatibilität nutzt https://api.spicyapi.ai/v1. Beide akzeptieren denselben API-Key, aber unterschiedliche Anfrage- und Antwortformate.
Diese Seite führt Sie von der Modellwahl bis zum Abrufen des Ergebnisses. Wie Sie vor der Bezahlung den exakten Preis erfahren, erklärt Preisabfragen und Kompatibilität; Informationen zu Chat finden Sie unter Text und Streaming. Arbeiten Sie vor Bereitstellung Produktive Integration durch und verwenden Sie bei Fehlern Fehlerbehebung. Das Glossar erklärt unbekannte Begriffe.
Ergebnis sehen, bevor Sie Code schreiben?
Öffnen Sie ein Modell unter spicyapi.ai/explore und verwenden Sie den Playground-Tab. Der API-Tab zeigt dieselbe Anfrage als kopierbaren Befehl. Ohne eigenen Code arbeiten Sie im Terminal mit der CLI oder lassen einen KI-Assistenten über den MCP-Server handeln.
Offizielle Werkzeuge
Direkte HTTP-API-Aufrufe werden vollständig unterstützt und im Rest der Seite gezeigt. Möchten Sie diese nicht selbst schreiben, gibt es vier MIT-lizenzierte npm-Pakete, jeweils separat installierbar und dokumentiert:
@spicyapi/sdk
TypeScript-SDK: OpenAPI-Typen, adaptives Warten, Uploads und Webhook-Prüfung
@spicyapi/cli
spicyapi-Befehl: Katalog durchsuchen, Preise abfragen, Tasks erstellen und im Terminal verfolgen
@spicyapi/mcp
MCP-Server für Katalog, Preisabfragen und Tasks eines Coding-Agenten mit Bestätigung jedes kostenpflichtigen Schritts
@spicyapi/skill
Portabler Agent Skill mit Workflow und Ausgabengrenzen, in Ihren Agenten installierbar
Vier weitere offizielle MIT-lizenzierte SDKs unterstützen Sprachen außerhalb Node.js mit demselben Workflow. Aktuell ist nur das Go-Modul mit üblichem Befehl aus seinem Repository installierbar; die anderen drei Seiten zeigen die Installation bis zur Paketveröffentlichung:
Python-SDK
spicyapi für Python 3.11+, ohne Laufzeitabhängigkeiten
Go-SDK
github.com/spicyapi-ai/spicy-go für Go 1.23+, nur Standardbibliothek
PHP-SDK
spicyapi/spicyapi für PHP 8.1+, ohne Bibliotheksabhängigkeiten
Java-SDK
ai.spicyapi:spicyapi-java für Java 17+, mit geschlossener Fehlerhierarchie
Arbeiten Sie in ComfyUI? Die ComfyUI-Nodes bringen jedes Bild-, Video- und Audiomodell als Node auf Ihre Zeichenfläche, mit Preis vor Ausführung.
Vor dem Start
Sie benötigen vier Dinge. Ein ausgelassener Punkt scheitert später mit einem Fehler, der nicht unbedingt den fehlenden Punkt nennt.
- Konto mit bestätigter E-Mail. Registrieren Sie sich unter spicyapi.ai und klicken Sie auf den Bestätigungslink. Zuvor liefert jeder Key-Aufruf
403. - Guthaben. Generierungen werden aus US-Dollar-Guthaben bezahlt, nicht mit Credits. Neue Konten beginnen bei null. Laden Sie unter Konsole → Abrechnung auf. Ohne ausreichendes Guthaben scheitert die Taskerstellung mit Geschäftscode
40201. Kontoeinrichtung erklärt Registrierung, Aufladen und Key-Erstellung. - Terminal. macOS: Terminal-App. Linux: beliebige Shell. Windows: PowerShell. Abweichende Befehle haben einen Windows-PowerShell-Tab. macOS-/Linux-Befehle funktionieren auch unverändert in WSL oder Git Bash unter Windows.
- Einige Cent. Schritt 3 erstellt einen echten Bildtask, der bei Erfolg kostet. Schritt 2 zeigt Modellpreise und einen optionalen kostenlosen Befehl für den exakten Betrag.
Zuerst bewerten: Modelle und Preise ohne Konto
Für verfügbare Modelle und Preise benötigen Sie weder Key noch Konto. Der öffentliche Katalog hinter der Marketingwebsite ist anonym lesbar; dieser Befehl benötigt keinerlei Ersetzung:
curl "https://api.spicyapi.ai/console/v1/catalog/models?locale=en"Die Antwortstruktur wird unten erklärt. Unter data.items enthält jeder Eintrag:
model: exakter später alsmodelgesendeter Wert;slugenthält denselben;callable: ob das Modell jetzt laufen kann. Wählen Sietrue;statusist unsere Betriebsabsicht undlivekann dennoch vorübergehend nicht aufrufbar sein;modalityundtasks: Ausgabeart;price: günstigste Stufe,amountin USD jeunit, alsoper_image,per_second,per_requestoderper_1k_tokens, plustiersfür Stufenzahl. Bei Text lesen Sieprice.perMillionTokensmit getrennten Eingabe-, Cacheeingabe- und Ausgabepreisen je Million Tokens;amountallein ist nur Ausgabepreis.
Hängen Sie eine Modell-ID für den vollständigen Eintrag mit inputSchema, paramNotes und examples an:
curl "https://api.spicyapi.ai/console/v1/catalog/models/MODEL_ID_FROM_CATALOG?locale=en"MODEL_ID_FROM_CATALOG ist überall ein Platzhalter, kein reales Modell. Kopieren Sie immer eine echte Livekatalog-ID.
locale ist standardmäßig en; mit ja, ko, de, fr, es, pt-BR, ru, it oder pl erhalten Sie übersetzte Namen, Zusammenfassungen und Parameternotizen. Andere Werte liefern 400. strict=1 entfernt Einträge ohne veröffentlichte Texte in der Sprache statt Englischrückfall; normalerweise unnötig. Dieser Bereich wird nach Quelladresse statt Konto begrenzt und hat ein eigenes Kontingent, siehe Ratenbegrenzungen. Er ist auch gecacht, weshalb Preisänderungen verzögert erscheinen können.
Bei tiers größer als 1 zeigt price die günstigste Stufe, nicht Ihre Einstellungen. price.variant nennt deren Auswahl, etwa 720p oder kombinierte Felder wie generate_audio=false;resolution=480p. Das Einzelmodell nennt, sofern vorhanden, alle stufenwählenden Felder unter inputSchema["x-pricing"].variantFields. pricingNote erinnert allgemein an die maßgebliche Liveschätzung, listet keine Felder. Für exakten Betrag verwenden Sie mit vorhandenem Key POST /api/v1/jobs/quote.
Öffentlicher Katalog zur Bewertung, authentifizierter zum Aufruf
Der öffentliche Katalog zeigt angebotene Modelle und Listenpreise. Er zeigt nicht, was Ihr Key aufrufen darf oder Ihr Konto bezahlt; beides ist kontospezifisch. Wechseln Sie mit Key zu GET /api/v1/models in Schritt 2 als einziger Quelle aufrufbarer IDs. Erinnerte IDs, Veröffentlichungsschnappschüsse und Marketingseiten sind kein Ersatz.
Fünf Schritte
Key erstellen
Ein API-Key ist das Passwort, das Ihr Code bei jeder Anfrage sendet. Er identifiziert das Konto und dessen Abrechnung.
- Öffnen Sie Konsole → API-Keys und klicken Sie auf Key erstellen.
- Vergeben Sie etwa
my-first-testals Namen. Lassen Sie Erweitert unverändert. - Kopieren Sie den mit
sk-spicy-beginnenden Key oder klicken Sie auf Als Datei speichern. Er wird genau einmal gezeigt; bei Verlust erstellen Sie einen neuen. - Speichern Sie ihn im für die weiteren Schritte verwendeten Terminal als Umgebungsvariable, einen benannten Wert für dessen Befehle. So muss er nicht in einen Befehl geschrieben werden:
export SPICY_API_KEY="sk-spicy-paste-your-key-here"
# Check: this should print sk-spicy-
echo "${SPICY_API_KEY:0:9}"Erwartete Anzeige: sk-spicy-. Der Wert bleibt bis zum Schließen des Terminalfensters; führen Sie weitere Schritte dort aus.
Fehlt Key erstellen, erlaubt Ihre aktuelle Teamrolle die Erstellung nicht. Fragen Sie den Inhaber oder verwenden Sie Eigenes Team starten auf derselben Seite. Neue Keys haben ohne Ihre Einstellung kein Ausgabenlimit; ein Tageslimit begrenzt Fehlkonfiguration oder Offenlegung. Siehe Authentifizierung.
Aufrufbares Modell wählen
Der authentifizierte Katalog ist die einzige Quelle aufrufbarer IDs, aktueller Schemas und Beispiele. Verwenden Sie zwei Aufrufe: Die leichte Liste zeigt was aufrufbar ist, dann liefert ein Eintrag das Schema des gewählten Modells. Kataloglesen ist kostenlos.
curl "https://api.spicyapi.ai/api/v1/models?modality=image&task=text-to-image" \
-H "Authorization: Bearer $SPICY_API_KEY"Die Antwort ist lang; der benötigte Teil sieht gekürzt so aus:
{
"code": 200,
"msg": "success",
"data": {
"total": 1,
"items": [
{
"model": "MODEL_ID_FROM_CATALOG",
"displayName": "…",
"modality": "image",
"tasks": ["text-to-image"],
"enabled": true,
"available": true,
"startingPrice": { "variant": "", "unit": "per_image", "price": "0.008", "currency": "USD" }
}
]
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}Wählen Sie einen Eintrag mit enabled und available jeweils true, prüfen Sie den Preis und speichern Sie seinen model-Wert für den Rest der Seite. Mit jq zeigt curl … | jq -r '.data.items[] | select(.enabled and .available) | .model' nur aufrufbare IDs.
# Replace MODEL_ID_FROM_CATALOG with the model value you copied
export SPICY_MODEL="MODEL_ID_FROM_CATALOG"curl "https://api.spicyapi.ai/api/v1/models/${SPICY_MODEL}" \
-H "Authorization: Bearer $SPICY_API_KEY"Schrägstriche der ID bleiben im Pfad unverändert; prozentkodieren Sie nur bei Clients, die einen einzelnen Segmentparameter verlangen. inputSchema ist maßgeblich für input: required nennt Pflichtfelder, properties beschreibt sie. examples enthält serverseitig validierte Eingaben. Weitere Felder einschließlich pricing erklärt Modellkatalog und Schemas.
Fordern Sie Schemas nur bei Bedarf an. includeSchema=1&includeExamples=1 am Listenaufruf liefert alle aktivierten Modelle samt vollständigem JSON Schema und deutlich größerer Antwort. Katalogabfragen nutzen das Lesekontingent des Kontos unabhängig von der Taskerstellung. Lesen Sie den Katalog auch nicht vor jedem Task erneut; siehe Katalog zwischenspeichern.
Ist kein Eintrag geeignet, halten Sie an, bevor Sie einen kostenpflichtigen Task erstellen. Schnappschuss, Marketingseite oder erinnerte ID ersetzen keinen leeren Livekatalog. Für programmgesteuerte Auswahl siehe Modell aus Code wählen.
Optional: Exakten Preis vorher sehen. Preisabfragen sind kostenlos, erstellen nichts und reservieren nichts. data.estimatedCost ist der maximale USD-Betrag dieser exakten Anfrage:
curl -X POST https://api.spicyapi.ai/api/v1/jobs/quote \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
--data "{\"model\":\"${SPICY_MODEL}\",\"input\":{\"prompt\":\"A cinematic night portrait\"}}"Task erstellen
Ein Task ist ein Hintergrund-Generierungsauftrag. Sie erhalten sofort seine ID, später das Ergebnis. Dieser Schritt kostet Geld.
Jeder neue Task erhält einen neuen Idempotenzschlüssel, eine zufällige Kennung, die bei zweimaligem Senden derselben Anfrage, etwa nach Netzwerkfehler, doppelte Tasks verhindert. Erzeugen Sie ihn in einem eigenen Befehl und senden Sie danach den Task:
SPICY_IDEMPOTENCY_KEY="$(uuidgen)"
# Linux without uuidgen: SPICY_IDEMPOTENCY_KEY="$(cat /proc/sys/kernel/random/uuid)"curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
--data "{\"model\":\"${SPICY_MODEL}\",\"input\":{\"prompt\":\"A cinematic night portrait\"}}"Bei Timeout oder Verbindungsabbruch in 3b wiederholen Sie 3b ohne 3a. Derselbe Schlüssel liefert den gestarteten Task statt doppelter Bezahlung. Für ein anderes Bild führen Sie zuerst 3a aus. 409 bedeutet Schlüsselwiederverwendung mit anderem Prompt oder Modell. Mehr unter Idempotenz.
Die Anfrage sendet ein Feld. Verlangt das Schema aus Schritt 2 weitere, ergänzen Sie diese. Das jeweilige Schema bestimmt input; ein unbekanntes Feld verursacht 400.
Die Verbindung wird nie offengehalten. Die Task-ID kommt sofort:
HTTP/1.1 202 Accepted{
"code": 200,
"msg": "success",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"state": "queued",
"estimatedCost": "0.008",
"deadlineAt": "2026-08-28T09:22:04Z"
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}Kopieren und speichern Sie auf macOS / Linux die taskId; PowerShell hat das bereits übernommen:
export SPICY_TASK_ID="paste-the-taskId-here"202 bedeutet angenommen, nicht abgeschlossen
Ein neuer Task hat state: "queued"; sichere Wiederholung mit demselben Idempotency-Key liefert den ursprünglichen Task im aktuellen Zustand. HTTP 202 bestätigt Annahme, nicht abgeschlossene Generierung. Bei der ersten Annahme werden Gelder reserviert und noch nicht berechnet: estimatedCost wird vom Guthaben zurückgelegt. Diese Reservierung ist zugleich Kostenobergrenze. Ungenutzte Beträge werden freigegeben; darüber hinaus wird kein weiterer Betrag eingezogen. Fehlgeschlagene Tasks geben die Reservierung normalerweise frei; ausdrücklich auf der Modellseite genannte Inhaltsablehnungen werden zum angenommenen Angebotspreis berechnet. Prüfen Sie settled und cost. deadlineAt ist die späteste Abschlusszeit vor expired. Der Geschäftscode code einer erfolgreichen Antwort bleibt 200. Ein angenommener Task ist nicht abbrechbar.
Ergebnis abholen
Enthält Ihre produktive Anfrage callBackUrl, rufen wir Sie zurück, wenn der Task einen Endzustand erreicht. Ein Callback oder Webhook ist eine HTTPS-Adresse Ihres Servers, die wir nach Abschluss aufrufen; ständiges Nachfragen entfällt. Die Minimalanfrage lässt sie für Betrieb ohne öffentlichen Endpunkt weg. Callbacks benötigen Signaturprüfung, siehe Webhooks.
Ohne Callback fragen Sie den Task ab:
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${SPICY_TASK_ID}" \
-H "Authorization: Bearer $SPICY_API_KEY"Verwenden Sie den API-Key, mit dem Sie den Task erstellt haben; ein anderer Key erhält 404. Prüfen Sie data.state:
data.state | Bedeutung | Maßnahme |
|---|---|---|
queued oder running | Generierung läuft; Gelder reserviert, nicht ausgegeben | Einige Sekunden warten, gleichen Befehl erneut ausführen. Bilder meist Sekunden, Videos Minuten. deadlineAt ist Obergrenze |
succeeded | Fertig | Zum nächsten Schritt |
failed oder expired | Nicht gelungen. Reservierung normalerweise freigegeben; ausdrücklich modellseitig genannte Inhaltsablehnungen berechnet. settled und cost prüfen | Stabiles data.errorCode und lesbares data.errorMessage lesen, Eingabe korrigieren, bei 3a neu starten |
Der code der Antwort bleibt auch dann 200, wenn der Task fehlgeschlagen ist: code bestätigt, dass das Lesen des Tasks erfolgreich war. Das Ergebnis des Tasks steht immer in data.state. So sehen die Antworten in den einzelnen Phasen aus, gekürzt dargestellt:
{
"code": 200,
"msg": "success",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"model": "MODEL_ID_FROM_CATALOG",
"state": "running",
"cost": "0.008",
"settled": false,
"createdAt": "2026-08-28T09:12:04Z",
"deadlineAt": "2026-08-28T09:22:04Z"
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w4"
}Solange settled gleich false ist, zeigt cost die Reservierung, nicht endgültige Belastung.
Ergebnisdatei herunterladen
Verwenden Sie data.output.assets[0].url aus dem Erfolgsergebnis direkt ohne API-Key. Der Link gilt ungefähr 20 Minuten, siehe expiresAt. Bei Ablauf wiederholen Sie Schritt 4 für einen frischen Link. Erzeugte Dateien werden spätestens nach 14 Tagen gelöscht; bewahren Sie benötigte eigene Kopien.
# Paste data.output.assets[0].url. Keep the double quotes: the URL contains & characters.
RESULT_URL="paste-the-url-here"
curl -L "$RESULT_URL" -o result.png # use result.mp4 when mime is video/mp4Mit jq entfällt Kopieren und Einfügen:
RESULT_URL="$(curl -s "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${SPICY_TASK_ID}" \
-H "Authorization: Bearer $SPICY_API_KEY" | jq -r '.data.output.assets[0].url')"Erwartetes Ergebnis: result.png im aktuellen Terminalordner. Prüfen Sie Kosten unter Konsole → Logs oder cost bei settled gleich true.
Textmodelle: Eine Anfrage, Antwort direkt zurück
Textmodelle antworten in derselben HTTP-Antwort, ohne Polling oder Download. X-Spicy-Task-Id bezeichnet den abrechnenden Task zur späteren Prüfung. Listen Sie in Schritt 2 stattdessen modality=text, exportieren Sie eines als SPICY_MODEL und führen Sie Folgendes aus:
curl https://api.spicyapi.ai/v1/chat/completions \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
--data "{\"model\":\"${SPICY_MODEL}\",\"messages\":[{\"role\":\"user\",\"content\":\"Say hello in one sentence.\"}]}"Die Antwort steht in choices[0].message.content; berechnet werden verwendete Tokens. Der Endpunkt nutzt OpenAI-Antwortformat statt {code, msg, data}. Siehe Text und Streaming.
Modell aus Code wählen
Die fünf Schritte oben halten einen Menschen beteiligt. Eine bereitgestellte Integration wählt selbst unmittelbar vor Einreichung: Letzte Woche aufrufbare Modelle können heute pausiert sein, Preise sind kontospezifisch. Fordern Sie Schema und servervalidiertes Beispiel zusammen an und prüfen Sie, dass enabled und available zutreffen:
CATALOG_JSON="$(curl --fail-with-body \
"https://api.spicyapi.ai/api/v1/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1" \
-H "Authorization: Bearer $SPICY_API_KEY")"
MODEL_ID="$(printf '%s' "$CATALOG_JSON" | jq -er \
'[.data.items[] | select(.enabled == true and .available == true and (.examples | length > 0))][0].model')"
MODEL_INPUT="$(printf '%s' "$CATALOG_JSON" | jq -cer \
'[.data.items[] | select(.enabled == true and .available == true and (.examples | length > 0))][0].examples[0].input')"
printf 'Selected %s\n' "$MODEL_ID"SPICY_IDEMPOTENCY_KEY="$(uuidgen)" # new task, new key; reuse it only to resend this same request
TASK_PAYLOAD="$(jq -cn \
--arg model "$MODEL_ID" \
--argjson input "$MODEL_INPUT" \
'{model: $model, input: $input}')"
curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
--data "$TASK_PAYLOAD"Bei leerer Auswahl stoppen Sie, statt eine erinnerte ID zu verwenden. Die drei Programme unten treffen dieselbe Auswahl ohne jq.
Alles zusammenführen
Die drei Programme lesen den Katalog, erstellen einen Task, fragen mit wachsenden Abständen ab und geben eine Download-URL aus. Verwenden Sie produktiv stattdessen Webhooks. Polling ist Ersatz, nicht der geplante Hauptweg.
Diese Programme erstellen einen echten kostenpflichtigen Task
Jedes wählt das erste aufrufbare Text-to-Image-Modell Ihres Katalogs, nicht das günstigste, und reicht ein validiertes Beispiel ein. Prüfen Sie dessen Preis vorher oder wählen Sie ausdrücklich Ihr gewünschtes Modell.
| Sprache | Dateiname | Vorher installieren | Ausführen |
|---|---|---|---|
| Node.js 20 oder neuer | generate.mjs | Nichts | node generate.mjs |
| Python 3.9 oder neuer | generate.py | pip install requests | python3 generate.py, Windows: py generate.py |
| Go 1.21 oder neuer | main.go in leerem Ordner | go mod init example | go run . |
Alle lesen SPICY_API_KEY aus Schritt 1 und zeigen nach Abschluss die Ergebnis-URL. Sie warten bis zehn Minuten, passend für Bildmodelle; bei Video warten Sie stattdessen bis deadlineAt.
const BASE = 'https://api.spicyapi.ai/api/v1';
const KEY = process.env.SPICY_API_KEY;
const headers = {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
};
async function call(path, init) {
const res = await fetch(`${BASE}${path}`, {
...init,
headers: { ...headers, ...init?.headers },
signal: AbortSignal.timeout(30_000),
});
const body = await res.json();
// Branch on `code`, not on res.ok — the business code says far more
if (body.code !== 200) {
throw new Error(`${body.code} ${body.msg} (request_id=${body.request_id})`);
}
return body.data;
}
async function generate() {
const catalog = await call(
'/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1',
);
const selected = catalog.items.find(
(item) => item.enabled && item.available && item.examples?.length,
);
if (!selected) throw new Error('No callable text-to-image model with a validated example');
const idempotencyKey = crypto.randomUUID();
const { taskId } = await call('/jobs/createTask', {
method: 'POST',
headers: { 'Idempotency-Key': idempotencyKey },
body: JSON.stringify({
model: selected.model,
input: selected.examples[0].input,
}),
});
// Start at 2s, multiply by 1.5, cap at 15s. The first check is almost
// certainly still `queued`, so a shorter start just wastes a round trip.
const deadline = Date.now() + 10 * 60_000;
let wait = 2000;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, wait));
wait = Math.min(wait * 1.5, 15000);
const task = await call(`/jobs/recordInfo?taskId=${taskId}`);
if (task.state === 'succeeded') {
const asset = task.output?.assets?.find((item) => item.url);
if (asset) return asset.url;
if (task.output?.assets?.some((item) => item.pending)) continue;
throw new Error('Result is unavailable');
}
// Anything other than queued / running is terminal. This also handles
// historical canceled records and future terminal states safely.
if (task.state !== 'queued' && task.state !== 'running') {
throw new Error(`${task.state}: ${task.errorMessage ?? 'no detail'}`);
}
}
throw new Error(`task ${taskId} timed out`);
}
generate().then(console.log);import os, time, uuid, requests
BASE = "https://api.spicyapi.ai/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['SPICY_API_KEY']}",
"Content-Type": "application/json",
}
def call(method: str, path: str, headers=None, **kwargs) -> dict:
res = requests.request(
method, f"{BASE}{path}", headers={**HEADERS, **(headers or {})}, timeout=30, **kwargs
)
body = res.json()
# Branch on `code`, not on status_code
if body["code"] != 200:
raise RuntimeError(f'{body["code"]} {body["msg"]} (request_id={body.get("request_id")})')
return body["data"]
def generate() -> str:
catalog = call(
"GET",
"/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1",
)
selected = next(
(
item
for item in catalog["items"]
if item["enabled"] and item["available"] and item.get("examples")
),
None,
)
if selected is None:
raise RuntimeError("No callable text-to-image model with a validated example")
idempotency_key = str(uuid.uuid4())
task_id = call("POST", "/jobs/createTask", headers={"Idempotency-Key": idempotency_key}, json={
"model": selected["model"],
"input": selected["examples"][0]["input"],
})["taskId"]
# Start at 2s, multiply by 1.5, cap at 15s
deadline = time.monotonic() + 600
wait = 2.0
while time.monotonic() < deadline:
time.sleep(wait)
wait = min(wait * 1.5, 15.0)
task = call("GET", "/jobs/recordInfo", params={"taskId": task_id})
if task["state"] == "succeeded":
assets = task.get("output", {}).get("assets", [])
ready = next((asset for asset in assets if asset.get("url")), None)
if ready:
return ready["url"]
if any(asset.get("pending") for asset in assets):
continue
raise RuntimeError("Result is unavailable")
# Anything other than queued / running is terminal.
if task["state"] not in {"queued", "running"}:
raise RuntimeError(f'{task["state"]}: {task.get("errorMessage", "no detail")}')
raise TimeoutError(f"task {task_id} timed out")
print(generate())package main
import (
"bytes"
"crypto/rand"
"encoding/json"
"encoding/hex"
"fmt"
"math"
"net/http"
"os"
"time"
)
const base = "https://api.spicyapi.ai/api/v1"
// envelope is the shape every endpoint returns. Data stays raw so each
// call site can decode it into whatever it actually expects.
type envelope struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data json.RawMessage `json:"data"`
RequestID string `json:"request_id"`
}
var client = &http.Client{Timeout: 30 * time.Second}
func newIdempotencyKey() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return "job-" + hex.EncodeToString(b)
}
func call(method, path string, body any, idempotencyKey string, out any) error {
var rdr *bytes.Reader
if body != nil {
raw, err := json.Marshal(body)
if err != nil {
return err
}
rdr = bytes.NewReader(raw)
} else {
rdr = bytes.NewReader(nil)
}
req, err := http.NewRequest(method, base+path, rdr)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("SPICY_API_KEY"))
req.Header.Set("Content-Type", "application/json")
if idempotencyKey != "" {
req.Header.Set("Idempotency-Key", idempotencyKey)
}
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return err
}
// Branch on Code, not on res.StatusCode
if env.Code != 200 {
return fmt.Errorf("%d %s (request_id=%s)", env.Code, env.Msg, env.RequestID)
}
return json.Unmarshal(env.Data, out)
}
func generate() (string, error) {
var catalog struct {
Items []struct {
Model string `json:"model"`
Enabled bool `json:"enabled"`
Available bool `json:"available"`
Examples []struct {
Input map[string]any `json:"input"`
} `json:"examples"`
} `json:"items"`
}
if err := call(http.MethodGet,
"/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1",
nil, "", &catalog); err != nil {
return "", err
}
var modelID string
var modelInput map[string]any
for _, item := range catalog.Items {
if item.Enabled && item.Available && len(item.Examples) > 0 {
modelID, modelInput = item.Model, item.Examples[0].Input
break
}
}
if modelID == "" {
return "", fmt.Errorf("no callable text-to-image model with a validated example")
}
var created struct {
TaskID string `json:"taskId"`
}
err := call(http.MethodPost, "/jobs/createTask", map[string]any{
"model": modelID,
"input": modelInput,
}, newIdempotencyKey(), &created)
if err != nil {
return "", err
}
// Start at 2s, multiply by 1.5, cap at 15s
wait := 2 * time.Second
deadline := time.Now().Add(10 * time.Minute)
for time.Now().Before(deadline) {
time.Sleep(wait)
wait = time.Duration(math.Min(float64(wait)*1.5, float64(15*time.Second)))
var task struct {
State string `json:"state"`
ErrorMsg string `json:"errorMessage"`
Output struct { Assets []struct { URL string `json:"url"`; Pending bool `json:"pending"` } `json:"assets"` } `json:"output"`
}
if err := call(http.MethodGet, "/jobs/recordInfo?taskId="+created.TaskID, nil, "", &task); err != nil {
return "", err
}
if task.State == "succeeded" {
pending := false
for _, asset := range task.Output.Assets {
if asset.URL != "" { return asset.URL, nil }
pending = pending || asset.Pending
}
if pending { continue }
return "", fmt.Errorf("result is unavailable")
}
// Terminal means "not one of the two in-flight states" — do not
// enumerate the terminal states, that breaks when a new one appears
if task.State != "queued" && task.State != "running" {
return "", fmt.Errorf("%s: %s", task.State, task.ErrorMsg)
}
}
return "", fmt.Errorf("task %s timed out", created.TaskID)
}
func main() {
url, err := generate()
if err != nil {
panic(err)
}
fmt.Println(url)
}Die Antwortstruktur
Native Task-API-JSON-Antworten nutzen diese Struktur; /v1-Kompatibilität und SSE eigene Protokollformate:
{ "code": 200, "msg": "success", "data": { }, "request_id": "req_…" }codeist ein Geschäftscode, kein HTTP-Status.200bedeutet Erfolg. Auch der HTTP-Status wird korrekt gesetzt, sodass Sie Ihre Fehlerbehandlung an einem der beiden Werte ausrichten können; wählen Sie einen. Bei spezifischen Codes unterscheiden sich die Werte, etwa bei unzureichendem Guthaben mitcode: 40201und HTTP402. Wenn Sie beide Ansätze vermischen, übersehen Sie irgendwann einen Fall.- Bei Fehler fehlt
data, stattnullzu sein. request_iderscheint bei Erfolg und Fehler und im AntwortheaderX-Request-Id. Nennen Sie es bei Problemmeldungen.
Nächste Schritte
Authentifizierung
Key-Format, Limits je Key und Maßnahmen nach Offenlegung
Modellkatalog und Schemas
Livekatalog, Preise und Parametertabellen aus inputSchema
Asynchrone Tasks
Zustände, Ergebnisabholung und Polling mit wachsenden Abständen
Webhooks
Nutzdatenstruktur, Signaturprüfung und Wiederholungen
Medien
Direkte Referenzuploads und Ergebnisdownloads
Idempotenz
Sicher wiederholen ohne doppelte Generierung oder Belastung
Fehler
Alle Geschäftscodes und nutzlose Wiederholungen
Konto einrichten
In rund fünfzehn Minuten vom fehlenden Konto zum funktionierenden API-Schlüssel. Registrieren, E-Mail bestätigen, Guthaben aufladen, Schlüssel anlegen und einen kostenlosen Testaufruf machen, mit Erklärung zu jedem Bildschirm.
Authentifizierung
Key-Format, Einschränkungen je Key, sichtbare Tasks und sofortige Maßnahmen bei einem offengelegten API-Key.

