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:
- Sie senden den Auftrag und erhalten sofort eine
taskId. Noch ist nichts fertig; der Auftragspreis wird aus Ihrem Guthaben zurückgelegt, als Reservierung. - Später fragen Sie: „Ist diese
taskIdfertig?“ 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. - Bei
stategleichsucceededsteht der Dateilink indata.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 insettledundcost.
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/createTaskDieser 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
| Header | Erforderlich | Hinweise |
|---|---|---|
Authorization | Ja | Bearer sk-spicy-… |
Content-Type | Ja | application/json |
Idempotency-Key | Nein | Dringend empfohlen: macht Wiederholung nach Netzwerkfehler sicher; siehe Idempotenz |
X-Spicy-Retention | Nein | Kü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: 2Der 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"
}| Feld | Hinweise |
|---|---|
taskId | Task-ID mit Präfix job_; für alle weiteren Schritte nötig, daher speichern |
state | Bei neuem Task queued; bei idempotenter Wiederholung mit gleichem Schlüssel aktueller Zustand des ursprünglichen Tasks |
estimatedCost | Bei Annahme reservierter Betrag, etwa "0.008", als USD-Zeichenfolge; zugleich maximale Taskkosten. Rest wird freigegeben, keine darüber hinausgehende Nachbelastung |
deadlineAt | Spä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.
| HTTP | code | Häufige Bedeutung | Maßnahme |
|---|---|---|---|
| 400 | 400 | Feld in input fehlt im Modellschema, Wert außerhalb des Bereichs oder ungültiges callBackUrl | Mit inputSchema vergleichen; msg nennt das Problem |
| 400 | 40004 | Jedes Feld ist gültig, aber die Kombination kann derzeit nicht bedient werden | Kombination ändern, etwa Auflösung oder Dauer |
| 401 | 401 | API-Key fehlt, ist falsch oder deaktiviert | Authorization prüfen |
| 402 | 40201 | Guthaben reicht nicht zur Reservierung von estimatedCost | In Konsole aufladen und mit demselben Idempotency-Key erneut senden |
| 402 | 40202 | Ausgabenlimit des Keys, Teams oder Kontos erreicht | Limit erhöhen oder Zurücksetzung eines Tages- oder Monatslimits abwarten |
| 403 | 40301 | Key darf Modell nicht aufrufen | Anderen Key verwenden oder seine Modellbeschränkung ändern |
| 409 | 409 / 40901 | Idempotenzkonflikt oder nicht mehr passendes Preisangebot | Idempotenz und Preisabfragen lesen |
| 413 | 413 | Anfragebody zu groß, meist durch Inline-Medien | Datei hochladen und ihre spicy://-URI senden; siehe Medien |
| 429 | 429 | Zu viele Anfragen in kurzer Zeit | Retry-After abwarten und mit gleichem Schlüssel erneut senden |
| 503 | 50301 | Modell vorübergehend nicht verfügbar | Spä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.
# 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.
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);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
queuedrunningsucceededfailedexpiredEin 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.
| Zustand | Bedeutung | Endzustand | Abrechnung |
|---|---|---|---|
queued | Erstellt, Geld reserviert, wartet auf Verarbeitung | Nein | Reserviert, nicht abgerechnet |
running | Modell generiert; wartet auf Ergebnis | Nein | Reserviert, nicht abgerechnet |
succeeded | Generierung erfolgreich | Ja | Tatsächliche Nutzung, begrenzt auf Reservierung |
failed | Generierung fehlgeschlagen | Ja | Normalerweise vollständige Freigabe; bestimmte ausdrücklich auf der Modellseite genannte content_rejected-Ergebnisse kosten den angenommenen Angebotspreis. settled und cost prüfen |
canceled | Nur historische Kompatibilität; keine Abbruchaktion verfügbar | Ja | Vollständige Freigabe |
expired | Maximale Modelllaufzeit überschritten | Ja | Vollstä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 -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"}}'| Ergebnis | Antwort |
|---|---|
| Endzustand innerhalb der Wartezeit | 200 mit Taskdatensatz in derselben Struktur wie bei recordInfo |
| Wartezeit abgelaufen | Gewöhnliches 202, wie ohne wait; ab dann pollen |
| Sie trennen die Verbindung | Task 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 "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
| Feld | Typ | Hinweise |
|---|---|---|
taskId | string | Task-ID |
sourceTaskId | string | Nur bei Task aus Retry: ursprünglicher Task |
model | string | Modellkennung |
state | string | Tabelle oben |
input | object | Geprüfte Parameter; Inline-Bilder werden Plattformdatei-URIs. Nach Ablauf der Eingabeaufbewahrung ausgelassen |
output | object | Beschreibung der erzeugten Dateien; nur bei succeeded aussagekräftig |
errorCode | string | Bei state gleich failed oder expired; abgelaufener Task hat timeout. Einer von neun Werten, siehe Taskfehlercodes. Diesen verwenden, nie errorMessage |
errorMessage | string | Lesbarer Fehlergrund auf Englisch, sofern keine andere Sprache gewählt. Nur bei Fehler vorhanden |
cost | string | Abgerechneter Betrag oder bei offener Abrechnung Reservierung; endgültige Kosten überschreiten sie nicht |
settled | boolean | Abrechnung endgültig? Bei false kann sich cost noch ändern |
createdAt | string | RFC 3339 |
deadlineAt | string | Bei Annahme gespeicherte Ausführungsfrist; ältere Webhook-Ereignisse können sie auslassen |
completedAt | string | RFC 3339, nur bei Endzuständen |
contentState | string | present, expired nach Aufbewahrungsablauf oder purged nach Ihrer Löschanforderung; siehe Aufbewahrung und Löschung |
contentRemovedBy | string | user oder system; ausgelassen, solange Inhalt vorhanden |
purgedAt | string | RFC 3339, nur nach Ihrer Löschanforderung |
retention | object | outputsExpireAt 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
taskIdund fragen Sie denselben Task später zum Fortsetzen ab. deadlineAtist die serverseitige Ausführungsfrist. Idempotente Wiederholung verlängert sie nicht. Ein ausdrücklicher Retry erstellt einen neuen Task mit eigener Frist. Lesen Sie Endzustand undsettled, statt aus einem lokalen Timer eine Rückerstattung abzuleiten.output.assets[].expiresAtlä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.
{
"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"
}{
"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[].
| Feld | Typ | Hinweise |
|---|---|---|
key | string | Kompatibilitätskennung für optionale Linkerneuerung; leer bei pending |
url | string | Direkte signierte GET-URL; bei ausstehender oder nicht verfügbarer Datei ausgelassen. Keinen API-Key anhängen |
expiresAt | string | RFC-3339-Ablauf dieser URL; innerhalb der Aufbewahrung erneut pollen zum Erneuern |
mime | string | Inhaltstyp wie image/png, video/mp4 |
width · height | integer | Pixelmaße; bei Audio und Text nicht vorhanden |
durationSeconds | number | Länge in Sekunden, nur Video und Audio |
bytes | integer | Größe in Bytes |
pending | boolean | Siehe unten |
unavailable | boolean | Siehe unten |
layer | object | Nur 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;keyundurlsind vorerst leer. Nach einigen Sekunden erneut abfragen.download-urlohnekeyliefert derzeit409.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.
| Callback | Polling | |
|---|---|---|
| Latenz | Bei Taskabschluss | Ihr Pollingabstand |
| Anfragen | 1 je Task, von uns | N je Task, von Ihnen |
| Verbraucht Ihr Anfragekontingent | Nein | Ja, siehe Ratenbegrenzungen |
| Öffentlicher HTTPS-Endpunkt nötig | Ja | Nein |
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
deadlineAtaus dercreateTask-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.
// 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);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 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.

