spicyapiDokumentation
Mit der API entwickeln
Hauptinhalt

Asynchrone Tasks

createTask und recordInfo, die sechs Taskzustände und die Wahl zwischen Callbacks und Polling.

Generierung ist asynchron: Einreichen und Abholen sind zwei getrennte Aufrufe.

Einfach erklärt:

  1. Sie senden den Auftrag und erhalten sofort eine taskId. Noch ist nichts fertig; der Auftragspreis wird aus Ihrem Guthaben zurückgelegt, als Reservierung.
  2. Später fragen Sie: „Ist diese taskId fertig?“ Das ist Polling; alle paar Sekunden reicht. Oder Sie geben uns eine Webadresse, an die wir das fertige Ergebnis senden. Das ist ein Callback, auch Webhook genannt.
  3. Bei state gleich succeeded steht der Dateilink in data.output.assets[0].url. Öffnen oder laden Sie ihn innerhalb etwa 20 Minuten herunter. Ist er abgelaufen, fragen Sie erneut nach einem frischen Link. Ein erfolgreicher Task kostet höchstens seine Reservierung. Fehlgeschlagene und abgelaufene Tasks geben sie normalerweise frei; Modellseiten können bestimmte kostenpflichtige Inhaltsablehnungen als Ausnahme nennen. Die endgültigen Kosten stehen in settled und cost.

Warum nicht ein Aufruf? Bilder benötigen Sekunden, Videos häufig Minuten. Ein synchroner Endpunkt würde Ihren Prozess an einer offenen Verbindung warten lassen. Bei einem Verbindungsproblem hätten Sie weder Ergebnis noch sichere Wiederholungsmöglichkeit, weil unklar wäre, ob der vorherige Versuch bereits läuft. Mit getrennten Aufrufen ist die Task-ID der Anker: Verbinden Sie sich neu und fragen Sie dieselbe ID weiter ab.

1. Task erstellen

POST /api/v1/jobs/createTask

Dieser Aufruf gibt Geld aus

Ein angenommener Task reserviert sofort estimatedCost aus Ihrem Guthaben und ist nicht abbrechbar. Möchten Sie zuerst den Preis sehen, ohne etwas zu erstellen, fragen Sie den Preis an. Ausgabenlimits des Keys begrenzen seine Ausgaben; siehe Abrechnung.

Header

HeaderErforderlichHinweise
AuthorizationJaBearer sk-spicy-…
Content-TypeJaapplication/json
Idempotency-KeyNeinDringend empfohlen: macht Wiederholung nach Netzwerkfehler sicher; siehe Idempotenz
X-Spicy-RetentionNeinKürzere Aufbewahrung von Ausgaben und Prompt dieses Tasks, in Sekunden; siehe Aufbewahrung und Löschung

Anfragebody

Eigenschaft

Typ

callBackUrl muss eine öffentliche HTTPS-Adresse sein, die unsere Server über das Internet erreichen. Abgelehnt werden einfache http://-URLs, ausdrücklich angegebene Ports außer 443 oder 80, URLs mit Benutzername oder Passwort, private oder Loopback-IP-Adressen sowie nicht auflösbare Hostnamen. Der Callbackbody enthält Ihren Prompt und signierte Ergebnislinks. http://localhost funktioniert daher bei lokaler Entwicklung nicht: Nutzen Sie Polling oder einen Tunnel mit öffentlicher HTTPS-Adresse. Ungültige Werte liefern HTTP 400 mit Geschäftscode code: 400, msg: "Invalid callback URL" und request_id. Der Fehler gibt weder Ihre URL noch Hostname, IP oder DNS-Details wieder. Wir prüfen das Ziel bei Verbindungsaufbau erneut; siehe Webhooks.

Schreiben Sie callBackUrl mit großem B

Nur input wird Feld für Feld geprüft. Ein unbekanntes Feld auf oberster Ebene, etwa callback_url oder webhookUrl, wird derzeit ignoriert. Der Task wird angenommen, aber es wird nie ein Callback gesendet. Erwarten Sie einen Callback und erhalten keinen, prüfen Sie zuerst diese Schreibweise.

Antwort

HTTP/1.1 202 Accepted
Location: /api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3
Retry-After: 2

Der Transportstatus lautet 202 Accepted. Der Body verwendet weiterhin die Standardstruktur; der erfolgreiche Geschäftscode code bleibt 200. Location nennt die Pollingadresse. Retry-After: 2 empfiehlt eine Wartezeit von zwei Sekunden vor der ersten Abfrage.

{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "state": "queued",
    "estimatedCost": "0.008",
    "deadlineAt": "2026-08-28T09:42:04Z"
  },
  "request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}
FeldHinweise
taskIdTask-ID mit Präfix job_; für alle weiteren Schritte nötig, daher speichern
stateBei neuem Task queued; bei idempotenter Wiederholung mit gleichem Schlüssel aktueller Zustand des ursprünglichen Tasks
estimatedCostBei Annahme reservierter Betrag, etwa "0.008", als USD-Zeichenfolge; zugleich maximale Taskkosten. Rest wird freigegeben, keine darüber hinausgehende Nachbelastung
deadlineAtSpätester Zeitpunkt, bis zu dem der Server am Task arbeitet. Ohne Abschluss endet er als expired und kostet nichts. Nutzen Sie ihn zur Wahl Ihrer Wartezeit

Geldfelder sind Zeichenfolgen ohne Währungssymbol

estimatedCost, cost und die drei Guthabenfelder sind einfache dezimale Zeichenfolgen wie "0.008", ohne $ oder Tausendertrennzeichen. parseFloat / float() können sie direkt lesen.

Sie sind Zeichenfolgen statt JSON-Zahlen, weil eine JSON-Zahl in den meisten Sprachen zu float64 wird und 0.008 keine exakte binäre Darstellung hat. Nach Umwandlung in eine Gleitkommazahl lässt sich der exakte belastete Betrag nicht mehr bestimmen. Für die Buchhaltung verwenden Sie einen Dezimaltyp wie decimal.Decimal oder BigDecimal.

Währung ist immer USD. Siehe Abrechnung.

Wenn die Erstellung scheitert

Diese Fehler treten häufig bei der ersten Integration auf. Wenn createTask einen Fehler liefert, wird nichts reserviert oder berechnet.

HTTPcodeHäufige BedeutungMaßnahme
400400Feld in input fehlt im Modellschema, Wert außerhalb des Bereichs oder ungültiges callBackUrlMit inputSchema vergleichen; msg nennt das Problem
40040004Jedes Feld ist gültig, aber die Kombination kann derzeit nicht bedient werdenKombination ändern, etwa Auflösung oder Dauer
401401API-Key fehlt, ist falsch oder deaktiviertAuthorization prüfen
40240201Guthaben reicht nicht zur Reservierung von estimatedCostIn Konsole aufladen und mit demselben Idempotency-Key erneut senden
40240202Ausgabenlimit des Keys, Teams oder Kontos erreichtLimit erhöhen oder Zurücksetzung eines Tages- oder Monatslimits abwarten
40340301Key darf Modell nicht aufrufenAnderen Key verwenden oder seine Modellbeschränkung ändern
409409 / 40901Idempotenzkonflikt oder nicht mehr passendes PreisangebotIdempotenz und Preisabfragen lesen
413413Anfragebody zu groß, meist durch Inline-MedienDatei hochladen und ihre spicy://-URI senden; siehe Medien
429429Zu viele Anfragen in kurzer ZeitRetry-After abwarten und mit gleichem Schlüssel erneut senden
50350301Modell vorübergehend nicht verfügbarSpäter mit gleichem Schlüssel versuchen

Vollständige Liste: Fehler.

Vor dem Start: Modell und gültige Eingabe wählen

Die folgenden Beispiele brauchen eine echte Modell-ID und vom Modell akzeptiertes input. Der Katalog liefert beides: includeExamples=1 fügt Eingaben hinzu, die das jeweilige Modellschema bereits bestanden haben. Dafür benötigen Sie jq, ein kleines JSON-Kommandozeilenwerkzeug. Ersetzen Sie für andere Modellarten image durch video oder audio.

curl -s "https://api.spicyapi.ai/api/v1/models?modality=image&includeExamples=1" \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  | jq -c '.data.items[] | select(.enabled and .available) | {model, input: .examples[0].input}'

Jede ausgegebene Zeile ist ein fertiger Anfragebody mit model und input. Wählen Sie eine aus. Bei "input": null hat das Modell kein gespeichertes Beispiel. Lesen Sie inputSchema aus GET /api/v1/models/{model} und schreiben Sie die Eingabe selbst. Siehe Modellkatalog und Schemas.

Beispiele

MODEL_ID_FROM_CATALOG ist nur ein Platzhalter. Ersetzen Sie den ganzen Anfragebody durch die ausgewählte Zeile des Livekatalogs; senden Sie den Platzhalter nicht wörtlich.

curl
# Paste one line printed by the command above between the single quotes.
TASK='{"model":"MODEL_ID_FROM_CATALOG","input":{"prompt":"a folded paper lantern, hard side light"}}'
# One new key per new task. After a network error, rerun only the curl command
# so the same key is sent again and no second task is created.
IDEMPOTENCY_KEY="$(uuidgen)"

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: $IDEMPOTENCY_KEY" \
  --data "$TASK"

Sie sollten den oben gezeigten 202-Body mit code: 200 und einer taskId erhalten. Bewahren Sie taskId zum Abholen des Ergebnisses auf. Für einen Callback ergänzen Sie "callBackUrl":"https://…" mit Ihrer eigenen HTTPS-Adresse im JSON.

JavaScript
const res = await fetch('https://api.spicyapi.ai/api/v1/jobs/createTask', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SPICY_API_KEY}`,
    'Content-Type': 'application/json',
    // One key per logical request; reuse it across retries
    'Idempotency-Key': 'order-8814-render-1',
  },
  body: JSON.stringify({
    model: 'MODEL_ID_FROM_CATALOG',
    input: { prompt: 'a folded paper lantern, hard side light' },
    // Your own public HTTPS endpoint; delete this line to poll instead
    callBackUrl: 'https://your-app.example.com/hooks/spicy',
  }),
});

const body = await res.json();
if (res.status !== 202 || body.code !== 200) throw new Error(`${res.status} ${body.code} ${body.msg}`);
console.log(body.data.taskId, body.data.deadlineAt);
Python
import os, requests

res = requests.post(
    "https://api.spicyapi.ai/api/v1/jobs/createTask",
    headers={
        "Authorization": f"Bearer {os.environ['SPICY_API_KEY']}",
        "Content-Type": "application/json",
        # One key per logical request; reuse it across retries
        "Idempotency-Key": "order-8814-render-1",
    },
    json={
        "model": "MODEL_ID_FROM_CATALOG",
        "input": {"prompt": "a folded paper lantern, hard side light"},
        # Your own public HTTPS endpoint; delete this line to poll instead
        "callBackUrl": "https://your-app.example.com/hooks/spicy",
    },
)

body = res.json()
if res.status_code != 202 or body["code"] != 200:
    raise RuntimeError(f'{res.status_code} {body["code"]} {body["msg"]}')
print(body["data"]["taskId"], body["data"]["deadlineAt"])

Eine 202-Antwort bedeutet nicht immer queued

Die erste angenommene Anfrage erstellt einen queued-Task. Wiederholt derselbe API-Key dieselbe Anfrage mit demselben Idempotency-Key, liefert 202 den aktuellen Zustand des ursprünglichen Tasks. Er kann bereits running oder abgeschlossen sein; es entsteht kein zweiter Task und keine zweite Reservierung.

2. Zustände

queued
running
succeededfailedexpired
Ein Task befindet sich immer in genau einem Zustand. Abgerechnet wird nur succeeded – bei jedem anderen Endzustand fließt der reservierte Betrag vollständig zurück.

Ein angenommener Task ist nicht rückgängig zu machen: Weder queued noch running bietet eine Abbruchaktion. Die API liest weiterhin historische canceled-Datensätze; keine neue Nutzer- oder Betreiberaktion erzeugt jedoch diesen Zustand.

ZustandBedeutungEndzustandAbrechnung
queuedErstellt, Geld reserviert, wartet auf VerarbeitungNeinReserviert, nicht abgerechnet
runningModell generiert; wartet auf ErgebnisNeinReserviert, nicht abgerechnet
succeededGenerierung erfolgreichJaTatsächliche Nutzung, begrenzt auf Reservierung
failedGenerierung fehlgeschlagenJaNormalerweise vollständige Freigabe; bestimmte ausdrücklich auf der Modellseite genannte content_rejected-Ergebnisse kosten den angenommenen Angebotspreis. settled und cost prüfen
canceledNur historische Kompatibilität; keine Abbruchaktion verfügbarJaVollständige Freigabe
expiredMaximale Modelllaufzeit überschrittenJaVollständige Freigabe

Prüfen Sie die Abrechnung, nicht nur den Taskzustand

Fehlgeschlagene oder abgelaufene Tasks geben normalerweise die gesamte Reservierung frei. Bestimmte Inhaltsablehnungen kosten nur bei ausdrücklich auf der Modellseite genannter Ausnahme den angenommenen Angebotspreis. Prüfen Sie settled und cost für die endgültigen Kosten. Eine berechnete Ablehnung hat weiterhin state: "failed" und errorCode: "content_rejected". Ein Wiederherstellungsprozess schließt offene Abrechnungen oder Freigaben automatisch ab.

Prüfen Sie auf Endzustand mit state nicht in {queued, running}, statt Endwerte einzeln aufzuzählen. So werden historische canceled-Datensätze sicher behandelt und ein zukünftiger Endzustand verursacht kein endloses Polling.

2.5 Pollingschleife mit wait überspringen

Ergänzen Sie ?wait=<seconds> bei createTask. Wir halten die Verbindung bis zum Endzustand offen, sodass Ihr erster Aufruf ganz ohne Pollingschleife auskommt.

curl
curl -X POST "https://api.spicyapi.ai/api/v1/jobs/createTask?wait=30" \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"MODEL_ID_FROM_CATALOG","input":{"prompt":"a lighthouse at dusk"}}'
ErgebnisAntwort
Endzustand innerhalb der Wartezeit200 mit Taskdatensatz in derselben Struktur wie bei recordInfo
Wartezeit abgelaufenGewöhnliches 202, wie ohne wait; ab dann pollen
Sie trennen die VerbindungTask bleibt unverändert, läuft weiter und wird normal berechnet

Ein Endzustand ist nicht zwingend ein Erfolg

200 bedeutet hier abgeschlossen, nicht erfolgreich. Prüfen Sie data.state: failed und expired haben denselben Status wie bei recordInfo.

Die Obergrenze beträgt 60 Sekunden. Größere Werte werden begrenzt, nicht abgelehnt. Alles, was keine positive Ganzzahl ist, wird ignoriert, nicht abgelehnt. Ein Tippfehler in einem Optimierungsparameter soll keine ansonsten erfolgreiche Generierung verhindern.

Die Grenze folgt aus dem 100-Sekunden-Edge-Timeout vor der API. Danach kappt das CDN die Verbindung statt uns. Sie erhielten 524 ohne Task-ID oder fortsetzbaren Zustand, obwohl der Task existiert und Geld bereits reserviert ist. 60 Sekunden lassen Raum für die Anfrage selbst.

Für Bilder nutzen, nicht für Videos

Videomodelle laufen häufig minutenlang. wait läuft dann einfach ab: Die offene Verbindung kostet Latenz und am Ende pollen Sie doch. Es lohnt sich bei Modellen, die meist in Sekunden fertig sind, also den meisten Bild- und Bearbeitungsendpunkten; andernfalls nicht. Es lässt sich bewusst nicht als Standard einstellen.

Produktiv bleiben Webhooks die bessere Wahl: Keine Verbindung bleibt offen und Sie erfahren auch von Tasks, die länger als jedes gewählte Wartebudget laufen.

3. Task abfragen

GET /api/v1/jobs/recordInfo?taskId=job_…

Ein erforderlicher Abfrageparameter: taskId. Ein Task kann nur mit dem erstellenden API-Key gelesen werden. Ein anderer Key, selbst aus Ihrem Konto oder als Ersatz nach Keyrotation, erhält 404, genau wie für einen nicht vorhandenen Task. Fehlerunterschiede verraten daher keine fremden Task-IDs. Dieselbe Regel gilt für download-url und retry. Holen Sie vor einer Rotation alle Ergebnisse mit dem alten Key ab.

curl
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3" \
  -H "Authorization: Bearer $SPICY_API_KEY"

Abfragen sind kostenlos. Ersetzen Sie die Beispiel-ID durch Ihre eigene taskId.

Antwortfelder

FeldTypHinweise
taskIdstringTask-ID
sourceTaskIdstringNur bei Task aus Retry: ursprünglicher Task
modelstringModellkennung
statestringTabelle oben
inputobjectGeprüfte Parameter; Inline-Bilder werden Plattformdatei-URIs. Nach Ablauf der Eingabeaufbewahrung ausgelassen
outputobjectBeschreibung der erzeugten Dateien; nur bei succeeded aussagekräftig
errorCodestringBei state gleich failed oder expired; abgelaufener Task hat timeout. Einer von neun Werten, siehe Taskfehlercodes. Diesen verwenden, nie errorMessage
errorMessagestringLesbarer Fehlergrund auf Englisch, sofern keine andere Sprache gewählt. Nur bei Fehler vorhanden
coststringAbgerechneter Betrag oder bei offener Abrechnung Reservierung; endgültige Kosten überschreiten sie nicht
settledbooleanAbrechnung endgültig? Bei false kann sich cost noch ändern
createdAtstringRFC 3339
deadlineAtstringBei Annahme gespeicherte Ausführungsfrist; ältere Webhook-Ereignisse können sie auslassen
completedAtstringRFC 3339, nur bei Endzuständen
contentStatestringpresent, expired nach Aufbewahrungsablauf oder purged nach Ihrer Löschanforderung; siehe Aufbewahrung und Löschung
contentRemovedBystringuser oder system; ausgelassen, solange Inhalt vorhanden
purgedAtstringRFC 3339, nur nach Ihrer Löschanforderung
retentionobjectoutputsExpireAt für Ausgabelöschung, promptsExpireAt für Promptlöschung und source für maßgebliche Einstellung

Vier unterschiedliche Fristen

  • Ihre eigene Wartezeit wählen Sie selbst. Ende des Wartens bricht den entfernten Task nicht ab. Behalten Sie taskId und fragen Sie denselben Task später zum Fortsetzen ab.
  • deadlineAt ist die serverseitige Ausführungsfrist. Idempotente Wiederholung verlängert sie nicht. Ein ausdrücklicher Retry erstellt einen neuen Task mit eigener Frist. Lesen Sie Endzustand und settled, statt aus einem lokalen Timer eine Rückerstattung abzuleiten.
  • output.assets[].expiresAt lässt den temporären Ergebnislink normalerweise nach 20 Minuten ablaufen. Eine neue Abfrage erneuert ihn ohne neuen Task.
  • Generierte Medien bleiben höchstens etwa 14 Tage nach Abschluss, Plattformuploads höchstens einen Tag. Dies sind Plattformobergrenzen. Unter Aufbewahrung und Löschung können Sie kürzere Fristen festlegen oder einen Taskinhalt gezielt löschen. Erneuern eines Links verlängert nicht die Medienaufbewahrung.

Im JavaScript-SDK reicht client.run({ model, input }, { idempotencyKey, onAccepted, timeoutMs }) einmal ein und pollt mit wachsenden Abständen von 2 bis maximal 10 Sekunden. timeoutMs ist nur lokale Wartezeit; speichern Sie die ID aus onAccepted. Siehe SDK. Bevorzugen Sie Webhooks für Hintergrundaufträge. Nach Prüfung eines vollständigen Ergebnis-Callbacks verwenden Sie das Ergebnis direkt, ohne weitere Taskabfrage.

Verwenden Sie die Ergebnis-URL direkt

Fertige Dateien enthalten url und expiresAt neben key und Medienmetadaten. Laden Sie output.assets[].url direkt ohne API-Zugangsdaten, etwa mit curl -L -o result.png "<url>". Bei Ablauf pollen Sie erneut; /common/download-url bleibt optional für bestehende Integrationen. URLs gelten gewöhnlich 20 Minuten und verlängern nicht die 14-tägige Ergebnisaufbewahrung.

succeeded
{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "model": "MODEL_ID_FROM_CATALOG",
    "state": "succeeded",
    "input": { "prompt": "a folded paper lantern, hard side light" },
    "output": {
      "assets": [
        {
          "key": "tasks/2026/08/28/job_01k3m8x9q2z4v7n5p6r8s0t1w3/9f3c1d0a7b4e2f68c5a1d3e9b0472fa1.png",
          "url": "https://example.r2.cloudflarestorage.com/results/result.png?X-Amz-Signature=SIGNATURE_FROM_RESPONSE",
          "expiresAt": "2026-08-28T09:32:11Z",
          "mime": "image/png",
          "width": 1024,
          "height": 1024,
          "bytes": 1483920
        }
      ]
    },
    "cost": "0.008",
    "settled": true,
    "createdAt": "2026-08-28T09:12:04Z",
    "deadlineAt": "2026-08-28T09:42:04Z",
    "completedAt": "2026-08-28T09:12:11Z",
    "contentState": "present"
  },
  "request_id": "req_01k3m8xa1b2c3d4e5f6g7h8j9k0m"
}
failed
{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "model": "MODEL_ID_FROM_CATALOG",
    "state": "failed",
    "errorCode": "upstream_failed",
    "errorMessage": "Generation failed; the charge has been refunded",
    "cost": "0",
    "settled": true,
    "createdAt": "2026-08-28T09:12:04Z",
    "deadlineAt": "2026-08-28T09:42:04Z",
    "completedAt": "2026-08-28T09:12:39Z",
    "contentState": "present"
  },
  "request_id": "req_01k3m8xb2c3d4e5f6g7h8j9k0m1n"
}

Eine erfolgreiche Abfrage ist kein erfolgreicher Task

Beide Antworten tragen code: 200: Die Abfrage war erfolgreich. Das Taskergebnis steht in data.state. Geschäftscode beschreibt die Auslieferung, state den Task. Vermischen Sie dies nicht.

Ein Webhook-Body verwendet dieselbe Struktur, dieselbe code: 200-Hülle und dieselben data-Felder. Ihr Parser kann einen Callback unverändert lesen: zwei Ausgänge, ein Parser.

Struktur von output

output hat zwei unabhängige Teile. Generierte Bild-, Video- und Audiodateien stehen in output.assets[]. Textergebnisse stehen als einfache Zeichenfolge in output.text.

Die meisten Modelle füllen genau einen Teil; ein Modell kann beide füllen. Dies hängt vom Endpunkt ab, nicht von seiner modality. Ein speech-to-text-Endpunkt gehört im Katalog zu audio, weil er Audio liest, antwortet aber mit Text.

`output.assets` ist nicht immer vorhanden

Ein Endpunkt mit Textergebnis hat überhaupt keinen Schlüssel assets, also auch kein leeres Array. Code mit task.output.assets[0].url wirft bei solchen Tasks einen Fehler, statt einen prüfbaren Wert zu liefern. Lesen Sie zuerst vorhandenes output.text und behandeln Sie assets als optional:

const { text, assets = [] } = task.output ?? {};
if (text) console.log(text);
for (const asset of assets) console.log(asset.url);

Ein Textergebnis hat keinen Download und keinen ablaufenden Link. Es ist dennoch Teil der Taskausgabe und wird nach demselben Plan wie andere Ergebnisse entfernt; contentState wechselt zu expired. Kopieren Sie benötigte Inhalte. Mehrteilige Ergebnisse, etwa Transkriptsegmente, werden zu einer Zeichenfolge mit Zeilenumbrüchen zwischen den Teilen verbunden.

Der Rest dieses Abschnitts beschreibt assets[].

FeldTypHinweise
keystringKompatibilitätskennung für optionale Linkerneuerung; leer bei pending
urlstringDirekte signierte GET-URL; bei ausstehender oder nicht verfügbarer Datei ausgelassen. Keinen API-Key anhängen
expiresAtstringRFC-3339-Ablauf dieser URL; innerhalb der Aufbewahrung erneut pollen zum Erneuern
mimestringInhaltstyp wie image/png, video/mp4
width · heightintegerPixelmaße; bei Audio und Text nicht vorhanden
durationSecondsnumberLänge in Sekunden, nur Video und Audio
bytesintegerGröße in Bytes
pendingbooleanSiehe unten
unavailablebooleanSiehe unten
layerobjectNur bei Ebenenzerlegung: zIndex als Ganzzahl, optionales boundingBox mit [x1, y1, x2, y2] und optionales name; siehe unten

Nicht verfügbare Felder werden ausgelassen, nicht als null oder 0 ausgegeben. Lesen Sie sie als optional.

Zwei Kennzeichen benötigen Behandlung:

  • pending: true: Datei wird noch in unseren Speicher kopiert; key und url sind vorerst leer. Nach einigen Sekunden erneut abfragen. download-url ohne key liefert derzeit 409.
  • unavailable: true: Datei ist abgelaufen oder nach Übertragungsfehler dauerhaft nicht verfügbar. Warten hilft nicht; generieren Sie neu. Konnte keine Datei eines erfolgreichen Tasks gespeichert werden, wird der Task nicht berechnet.

Ebenenzerlegung ergänzt layer. Die erste Datei ist das Basisbild, also die Quelle ohne getrennte Elemente, mit zIndex 0. Alle weiteren sind transparente PNG-Ebenen. Stapeln Sie sie mit aufsteigendem zIndex, um das Bild wiederherzustellen. boundingBox enthält [x1, y1, x2, y2] in Basisbildpixeln. Skalieren Sie die Ebene auf diesen Bereich, um sie richtig zu platzieren. Modelle ohne Positionsangaben lassen dies aus; die Ebenen funktionieren weiter als eigenständige Freisteller. name ist eine Bezeichnung des Modells und erscheint nur bei vorhandener Angabe. Jedes zurückgegebene Bild einschließlich Basisbild zählt bei der Abrechnung.

4. Callbacks bevorzugen, Polling als Ersatz

Beim Polling fragt Ihr Code recordInfo wiederholt bis zum Abschluss ab. Bei einem Callback senden wir den fertigen Task einmal an Ihren Server. Beide liefern dieselben Daten.

CallbackPolling
LatenzBei TaskabschlussIhr Pollingabstand
Anfragen1 je Task, von unsN je Task, von Ihnen
Verbraucht Ihr AnfragekontingentNeinJa, siehe Ratenbegrenzungen
Öffentlicher HTTPS-Endpunkt nötigJaNein

Nutzen Sie produktiv Callbacks. Ein dreiminütiger Videotask mit Sekundenabfragen erzeugt 180 Anfragen mit kaum neuer Information. Die derzeitige Open-API-Sicherung von 1000 Anfragen je 10 Sekunden begrenzt normale kostenpflichtige Generierung nicht. Enges Polling erzeugt aber unnötigen Verkehr; eine unkontrollierte Schleife leert ihr eigenes Lesekontingent, bis recordInfo mit 429 antwortet.

Polling passt in zwei Fällen: lokale Entwicklung und Dienste ohne öffentlichen Eingang.

Abstände beim Polling vergrößern

recordInfo nutzt das Lese- und createTask das Schreibkontingent. Polling kann deshalb Einreichungen nicht blockieren. Zu häufiges Polling lässt jedoch recordInfo selbst mit 429 antworten: Bis sich das Kontingent füllt, verlieren Sie den Blick auf Ihre Tasks.

Ein praktikabler Ablauf:

  • Bei 2–3 Sekunden beginnen. Kürzer ist unnötig; selbst schnelle Bildmodelle brauchen einige Sekunden.
  • Mit 1,5 multiplizieren, begrenzt auf 10–15 Sekunden.
  • An der eigenen Taskfrist stoppen, nicht nach einer festen Minutenzahl. Manche Videomodelle laufen bis zu einer Stunde; ein fester 10-Minuten-Timeout gibt dann normal arbeitende Tasks auf. Nutzen Sie deadlineAt aus der createTask-Antwort plus einige Minuten, damit der Server das Ergebnis speichern kann.

Aufhören zu warten bedeutet keinen Taskfehler

Am Ende Ihrer Wartezeit haben Sie nur das Warten beendet. Der Task läuft weiter, kann erfolgreich sein und Kosten verursachen. Reichen Sie ihn nicht mit neuem Idempotency-Key erneut ein; das erstellt und bezahlt einen zweiten Task. Fragen Sie später dieselbe taskId ab oder finden Sie sie im Taskverlauf wieder.

JavaScript
// created is the data object returned by createTask.
async function waitForTask(created) {
  // Stop a few minutes after the server's own deadline.
  const giveUpAt = Date.parse(created.deadlineAt) + 5 * 60 * 1000;
  let wait = 2000;

  while (Date.now() < giveUpAt) {
    await new Promise((r) => setTimeout(r, wait));
    wait = Math.min(wait * 1.5, 15000);

    const res = await fetch(
      `https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${encodeURIComponent(created.taskId)}`,
      { headers: { Authorization: `Bearer ${process.env.SPICY_API_KEY}` } },
    );
    const body = await res.json();

    // Being rate-limited is not a task failure — honour Retry-After
    if (body.code === 429) {
      wait = Number(res.headers.get('retry-after') ?? 1) * 1000;
      continue;
    }
    if (body.code !== 200) throw new Error(`${body.code} ${body.msg}`);

    // Terminal means "not one of the two in-flight states"
    const { state } = body.data;
    if (state !== 'queued' && state !== 'running') return body.data;
  }
  // Not a failure: query this taskId again later instead of resubmitting.
  throw new Error(`stopped waiting for ${created.taskId}`);
}

// created = body.data from the createTask example above
const task = await waitForTask(created);
if (task.state === 'succeeded') console.log(task.output.assets[0].url);
else console.log(task.state, task.errorCode);
Python
import os, time, requests
from datetime import datetime

BASE = "https://api.spicyapi.ai/api/v1"
AUTH = {"Authorization": f"Bearer {os.environ['SPICY_API_KEY']}"}


def wait_for_task(created: dict) -> dict:
    """created is the data object returned by createTask."""
    # Stop a few minutes after the server's own deadline.
    deadline = datetime.fromisoformat(created["deadlineAt"].replace("Z", "+00:00"))
    give_up_at = deadline.timestamp() + 5 * 60
    wait = 2.0

    while time.time() < give_up_at:
        time.sleep(wait)
        wait = min(wait * 1.5, 15.0)

        res = requests.get(f"{BASE}/jobs/recordInfo", headers=AUTH, params={"taskId": created["taskId"]})
        body = res.json()

        # Being rate-limited is not a task failure — honour Retry-After
        if body["code"] == 429:
            wait = float(res.headers.get("retry-after", 1))
            continue
        if body["code"] != 200:
            raise RuntimeError(f'{body["code"]} {body["msg"]}')

        # Terminal means "not one of the two in-flight states"
        if body["data"]["state"] not in ("queued", "running"):
            return body["data"]

    # Not a failure: query this taskId again later instead of resubmitting.
    raise TimeoutError(f'stopped waiting for {created["taskId"]}')


# created = body["data"] from the createTask example above
task = wait_for_task(created)
if task["state"] == "succeeded":
    print(task["output"]["assets"][0]["url"])
else:
    print(task["state"], task.get("errorCode"))

5. Guthaben prüfen

GET /api/v1/chat/credit liefert available für Mittel zur Finanzierung neuer Tasks, held für laufende Reservierungen und total. available kann bei genehmigter Kreditnutzung negativ sein; Aktionsguthaben unter funding.grants gilt möglicherweise nur für bestimmte Modelle. Details: Guthaben prüfen.

curl
curl https://api.spicyapi.ai/api/v1/chat/credit \
  -H "Authorization: Bearer $SPICY_API_KEY"

Taskverlauf und Wiederherstellung

GET /api/v1/jobs listet sichtbare Tasks des aktuellen API-Keys. So finden Sie Arbeit nach einem Neustart der Anwendung oder verpasstem Callback wieder. Tasks anderer Keys und ausgeblendete Datensätze fehlen. Antworten enthalten Task-IDs, Modell, Zustand, Kosten, Abrechnungszustand, Zeitstempel und, sofern erfasst, requestId des erstellenden Aufrufs. Bei Textaufrufen ist dies der Wert des Headers X-Request-Id. Prompts, Eingaben, Ausgaben und Medienlinks fehlen. Lesen Sie den gewählten Task mit recordInfo, um das Ergebnis abzuholen.

Filtern Sie nach state, exaktem Katalog-model oder Alias sowie UTC-Daten from / to im Format YYYY-MM-DD, Beginn eingeschlossen und Ende ausgeschlossen. Standard sind sieben Tage bis morgen UTC; maximal sind 92 Tage möglich. limit ist standardmäßig 20 und höchstens 100.

Lassen Sie ungenutzte Filter weg: Leere Werte wie model=, wiederholte Parameter oder unbekannte Parameter ergeben 400. Eine Abfrage über fünf Sekunden ergibt 503; versuchen Sie es mit kürzerem Zeitraum erneut.

curl --fail-with-body \
  'https://api.spicyapi.ai/api/v1/jobs?from=2026-09-01&to=2026-09-07&state=failed&limit=20' \
  -H "Authorization: Bearer $SPICY_API_KEY"

Lesen Sie data.items. Ist data.hasMore wahr, senden Sie data.nextCursor als cursor der nächsten Anfrage, mit unveränderten Filtern und ausdrücklich gesetzten Datumswerten. Die Reihenfolge ist absteigendes createdAt, danach Task-ID. Seiten zeigen den aktuellen Taskzustand und keine eingefrorene Momentaufnahme. Konstruieren Sie Cursor nicht selbst. Das Auflisten erstellt oder wiederholt nie einen Task.

Dieselbe Funktion bieten client.listTasks(...), spicyapi tasks list und das schreibgeschützte MCP-Werkzeug spicyapi_tasks_list. Behalten Sie USD-cost als dezimale Zeichenfolge. Bei settled gleich false ist es weiterhin die reservierte Schätzung und keine endgültige Belastung.

Ist die Erstellungsantwort verloren gegangen, wiederholen Sie zuerst die unveränderte Anfrage mit dem ursprünglichen Idempotency-Key. Eine leere Verlaufsseite berechtigt nicht, dieselbe kostenpflichtige Arbeit mit neuem Schlüssel erneut zu erstellen.

Auf dieser Seite