Zadania asynchroniczne
Kontrakty createTask i recordInfo, znaczenie sześciu stanów oraz wybór między callbackami a cyklicznym odpytywaniem.
Generowanie jest asynchroniczne: wysłanie zadania i odebranie wyniku to dwa oddzielne wywołania.
W praktyce:
- Wysyłasz zadanie i od razu otrzymujesz
taskId. Nic nie jest jeszcze gotowe; cena zadania zostaje odłożona z salda jako rezerwacja środków. - Później pytasz „czy to
taskIdjuż się zakończyło?” — to odpytywanie; wystarczy robić to co kilka sekund. Możesz też podać adres internetowy, na który wyślemy wynik po zakończeniu — to callback (webhook). - Gdy
statewynosisucceeded, link do pliku znajduje się wdata.output.assets[0].url. Otwórz go lub pobierz plik w ciągu około 20 minut; jeżeli link wygasł, zapytaj ponownie o nowy. Opłata za udane zadanie nie przekracza rezerwacji; zadania zakończone niepowodzeniem lub wygaśnięciem zwykle ją zwalniają, z opisanymi na stronie modelu wyjątkami dla określonych płatnych odmów dotyczących treści. Ostateczną opłatę sprawdzisz wsettledicost.
Dlaczego nie jedno wywołanie? Generowanie obrazu trwa sekundy, a filmu często minuty. Synchroniczny endpoint utrzymywałby proces na otwartym połączeniu, a każde zakłócenie mogłoby pozbawić Cię wyniku i bezpiecznej możliwości ponowienia — nie byłoby wiadomo, czy poprzednia próba już działa. Rozdzielenie wywołań pozwala oprzeć wszystko na ID zadania: połącz się ponownie i pytaj o to samo ID.
1. Utwórz zadanie
POST /api/v1/jobs/createTaskTo wywołanie wykorzystuje środki
Przyjęte zadanie od razu rezerwuje estimatedCost z salda i nie można go anulować. Aby najpierw poznać cenę bez tworzenia zadania, uzyskaj wycenę żądania. Limity wydatków klucza określają, ile może wydać; zobacz Rozliczenia.
Nagłówki
| Nagłówek | Wymagany | Uwagi |
|---|---|---|
Authorization | Tak | Bearer sk-spicy-… |
Content-Type | Tak | application/json |
Idempotency-Key | Nie | Zdecydowanie zalecany: zapewnia bezpieczne ponowienie po błędzie sieci. Zobacz Idempotencja |
X-Spicy-Retention | Nie | Krótszy okres przechowywania wyników i promptu tego zadania, w sekundach. Zobacz Przechowywanie i usuwanie danych |
Treść żądania
Właściwość
Typ
callBackUrl musi być publicznym adresem HTTPS, dostępnym dla naszych serwerów przez internet. Odrzucamy zwykłe adresy http://, jawny port inny niż 443 lub 80, adresy z nazwą użytkownika lub hasłem, prywatne adresy IP i adresy pętli zwrotnej oraz nazwy hostów, których nie można rozwiązać. Treść callbacku zawiera bowiem prompt i podpisane linki wyników. Dlatego http://localhost nie działa podczas pracy lokalnej: korzystaj z odpytywania lub udostępnij komputer przez tunel z publicznym adresem HTTPS. Nieprawidłowe wartości zwracają HTTP 400 z biznesowym code: 400, msg: "Invalid callback URL" i request_id. Błąd nie powtarza adresu URL, nazwy hosta, IP ani szczegółów DNS. Sprawdzamy cel ponownie przy nawiązywaniu połączenia; zobacz Webhooki.
Użyj nazwy callBackUrl z wielką literą B
Tylko input jest sprawdzane pole po polu. Nieznane pole na najwyższym poziomie, takie jak callback_url lub webhookUrl, jest obecnie ignorowane, więc zadanie zostaje przyjęte, lecz callback nigdy nie jest wysyłany. Jeżeli oczekujesz callbacku, który nie nadchodzi, najpierw sprawdź tę pisownię.
Odpowiedź
HTTP/1.1 202 Accepted
Location: /api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3
Retry-After: 2Status transportowy to 202 Accepted. Treść nadal używa standardowej struktury, dlatego biznesowy code powodzenia pozostaje 200. Location podaje adres do odpytywania, a Retry-After: 2 sugeruje odczekanie dwóch sekund przed pierwszym zapytaniem.
{
"code": 200,
"msg": "success",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"state": "queued",
"estimatedCost": "0.008",
"deadlineAt": "2026-08-28T09:42:04Z"
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}| Pole | Uwagi |
|---|---|
taskId | ID zadania z prefiksem job_. Będzie potrzebne w kolejnych operacjach, więc je zapisz |
state | queued dla nowego zadania; bieżący stan pierwotnego zadania przy idempotentnym powtórzeniu z tym samym kluczem |
estimatedCost | Kwota zarezerwowana przy przyjęciu, np. "0.008" (dolary amerykańskie jako ciąg znaków), i maksymalna opłata za to zadanie. Niewykorzystane środki są zwalniane; później nie pobieramy kwoty ponad rezerwację |
deadlineAt | Najpóźniejszy moment, do którego serwer pracuje nad zadaniem. Jeżeli nie zakończy się do tego czasu, otrzymuje expired i nie ma opłaty. Na tej podstawie ustal czas oczekiwania |
Pola kwot są ciągami znaków, ale bez symbolu waluty
estimatedCost, cost i trzy pola salda to zwykłe dziesiętne ciągi znaków, takie jak "0.008": bez $ i separatorów tysięcy. parseFloat / float() działają na nich bezpośrednio.
Stosujemy ciągi znaków zamiast liczb JSON, ponieważ liczba JSON w większości języków trafia do float64, a 0.008 nie ma dokładnej reprezentacji binarnej. Po konwersji ceny do liczby zmiennoprzecinkowej nie da się już dokładnie określić naliczonej kwoty — jeżeli trafia do ksiąg, odczytaj ją przy użyciu typu dziesiętnego (decimal.Decimal, BigDecimal).
Walutą zawsze jest USD. Zobacz Rozliczenia.
Niepowodzenie tworzenia
To błędy, z którymi najczęściej spotyka się pierwsza integracja. Gdy createTask zwraca błąd, nie rezerwujemy ani nie pobieramy środków.
| HTTP | code | Typowe znaczenie | Co zrobić |
|---|---|---|---|
| 400 | 400 | Pole w input nie należy do schematu modelu, wartość jest poza zakresem lub callBackUrl jest nieprawidłowe | Porównaj z inputSchema modelu; msg wskazuje problem |
| 400 | 40004 | Każde pole jest prawidłowe, lecz ten zestaw obecnie nie może być obsłużony | Zmień zestaw, na przykład rozdzielczość lub czas |
| 401 | 401 | Brakuje klucza API, jest błędny lub wyłączony | Sprawdź nagłówek Authorization |
| 402 | 40201 | Saldo nie wystarcza na rezerwację estimatedCost | Zasil konto w konsoli i wyślij ponownie z tym samym Idempotency-Key |
| 402 | 40202 | Osiągnięto limit wydatków klucza, zespołu lub konta | Podnieś limit w konsoli lub poczekaj na odnowienie dziennego albo miesięcznego limitu |
| 403 | 40301 | Klucz nie ma uprawnienia do wywołania modelu | Użyj innego klucza lub zmień ograniczenia modeli klucza |
| 409 | 409 / 40901 | Konflikt idempotencji lub wycena, która już nie pasuje | Zobacz Idempotencja i Wyceny |
| 413 | 413 | Treść żądania jest za duża, zwykle z powodu mediów inline | Prześlij plik i podaj jego URI spicy://; zobacz Pliki multimedialne |
| 429 | 429 | Zbyt wiele żądań w krótkim czasie | Odczekaj według Retry-After, a następnie wyślij z tym samym kluczem |
| 503 | 50301 | Model jest chwilowo niedostępny | Spróbuj później z tym samym kluczem |
Pełna lista: Błędy.
Zanim zaczniesz: wybierz model i prawidłowe dane wejściowe
Poniższe przykłady wymagają rzeczywistego ID modelu i input, które model przyjmuje. Katalog może dostarczyć oba: includeExamples=1 dodaje dane wejściowe, które już przeszły weryfikację schematu danego modelu. Potrzebujesz jq, niewielkiego narzędzia wiersza poleceń do JSON; dla innych rodzajów modeli zmień image na video lub 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}'Każdy wypisany wiersz jest gotową treścią żądania z dwoma polami, model i input. Wybierz jeden. Jeżeli wiersz zawiera "input": null, model nie ma zapisanego przykładu: odczytaj jego inputSchema z GET /api/v1/models/{model} i przygotuj dane samodzielnie. Zobacz Katalog modeli i schematy.
Przykłady
Poniższe MODEL_ID_FROM_CATALOG jest tylko symbolem zastępczym: zastąp całą treść żądania wierszem wybranym z aktualnego katalogu. Nie wysyłaj tego symbolu dosłownie.
# 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"Powinna pojawić się pokazana wyżej odpowiedź 202, zawierająca code: 200 i taskId. Zapisz taskId: pozwala odebrać wynik. Aby otrzymać callback, dodaj do JSON "callBackUrl":"https://…" z własnym adresem HTTPS.
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"])Odpowiedź 202 nie zawsze oznacza queued
Pierwsze przyjęte żądanie tworzy zadanie queued. Jeżeli ten sam klucz API powtarza to samo żądanie z tym samym Idempotency-Key, odpowiedź 202 zwraca bieżący stan pierwotnego zadania. Może już mieć running lub stan końcowy, lecz nie powstaje drugie zadanie ani rezerwacja.
2. Stany
queuedrunningsucceededfailedexpiredPrzyjęcie zadania jest nieodwracalne: ani queued, ani running nie ma operacji anulowania. API nadal może odczytywać historyczne rekordy canceled, lecz żadne nowe działanie użytkownika ani operatora nie tworzy tego stanu.
| Stan | Znaczenie | Końcowy | Rozliczenie |
|---|---|---|---|
queued | Utworzone, środki zarezerwowane, oczekuje na podjęcie | Nie | Zarezerwowane, nierozliczone |
running | Model generuje; trwa oczekiwanie na wynik | Nie | Zarezerwowane, nierozliczone |
succeeded | Generowanie zakończyło się powodzeniem | Tak | Rozliczenie rzeczywistego wykorzystania do wysokości rezerwacji |
failed | Generowanie nie powiodło się | Tak | Zwykle cała rezerwacja jest zwalniana; określone wyniki content_rejected, wyraźnie opisane na stronie modelu, są rozliczane według przyjętej wyceny. Sprawdź settled i cost |
canceled | Wyłącznie zgodność z historią; brak dostępnej operacji anulowania | Tak | Cała rezerwacja zwolniona |
expired | Przekroczono maksymalny czas wykonania modelu | Tak | Cała rezerwacja zwolniona |
Sprawdzaj rozliczenie, nie tylko stan zadania
Zadania zakończone niepowodzeniem lub wygaśnięciem zwykle zwalniają całą rezerwację. Niektóre odmowy dotyczące treści są rozliczane według przyjętej wyceny, ale tylko wtedy, gdy strona modelu wyraźnie podaje taki wyjątek. Ostateczną opłatę sprawdzisz w settled i cost. Płatna odmowa nadal ma state: "failed" i errorCode: "content_rejected". Proces odzyskiwania automatycznie kończy ewentualne zaległe rozliczenie lub zwolnienie środków.
Sprawdzaj stan końcowy jako state spoza {queued, running}, zamiast wyliczać wartości końcowe. To bezpiecznie obsługuje historyczne rekordy canceled i zapobiega nieskończonemu odpytywaniu, gdy w przyszłości pojawi się nowy stan końcowy.
2.5 Pomiń pętlę odpytywania dzięki wait
Dodaj ?wait=<seconds> do createTask, a utrzymamy połączenie do osiągnięcia stanu końcowego, dzięki czemu pierwsze wywołanie może w ogóle nie wymagać pętli odpytywania.
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"}}'| Wynik | Odpowiedź |
|---|---|
| Stan końcowy w dostępnym czasie | 200 z rekordem zadania — ta sama struktura co w recordInfo |
| Czas oczekiwania się kończy | Zwykłe 202, jak przy pominięciu wait. Następnie odpytuj |
| Rozłączasz się | Nic nie zmienia się dla zadania. Dalej działa i jest rozliczane zwyczajnie |
Stan końcowy nie musi oznaczać powodzenia
200 oznacza tutaj, że zadanie się zakończyło, nie że się powiodło. Sprawdź data.state — failed i expired mają ten sam status, dokładnie jak w recordInfo.
Maksymalny czas to 60 sekund. Większe wartości są ograniczane, a nie odrzucane; wartości inne niż dodatnia liczba całkowita są ignorowane, nie odrzucane — literówka w parametrze optymalizacji nie powinna uniemożliwiać generowania, które poza tym mogłoby się powieść.
Ten limit wynika ze 100-sekundowego limitu czasu warstwy brzegowej przed API. Po tym czasie połączenie zrywa CDN, a nie nasz serwer, i otrzymasz 524 bez ID zadania ani stanu pozwalającego wznowić obsługę — mimo że zadanie istnieje i środki są już zarezerwowane. 60 sekund zostawia czas na samo żądanie.
Używaj tego dla obrazów, nie dla filmów
Modele wideo często pracują przez minuty. wait po prostu przekroczy czas, otwarte połączenie zwiększy opóźnienie, a odpytywanie nadal będzie potrzebne. Opcja ma sens przy modelach zwykle kończących pracę w kilka sekund — większości endpointów obrazów i edycji — a w pozostałych przypadkach jest zbędna. Celowo nie można ustawić jej jako domyślnej.
W integracjach produkcyjnych webhooki pozostają lepszym rozwiązaniem: nie utrzymują żadnego połączenia i informują o zadaniach trwających dłużej niż dowolnie wybrany czas oczekiwania.
3. Odpytaj zadanie
GET /api/v1/jobs/recordInfo?taskId=job_…Wymagany jest jeden parametr zapytania: taskId. Zadanie można odczytać wyłącznie tym samym kluczem API, który je utworzył. Inny klucz — nawet z tego samego konta lub zastępujący stary po rotacji — zwraca 404, dokładnie jak nieistniejące zadanie, aby różnice błędów nie pozwalały wyszukiwać cudzych ID. Ta sama zasada dotyczy download-url i retry. Przed rotacją klucza zakończ odbieranie wyników zadań utworzonych starym kluczem.
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3" \
-H "Authorization: Bearer $SPICY_API_KEY"Odpytywanie jest bezpłatne. Zastąp przykładowe ID własnym taskId.
Pola odpowiedzi
| Pole | Typ | Uwagi |
|---|---|---|
taskId | string | ID zadania |
sourceTaskId | string | Tylko w zadaniu utworzonym przez ponowienie: ID zadania źródłowego |
model | string | Identyfikator modelu |
state | string | Zobacz tabelę wyżej |
input | object | Zweryfikowane parametry; obrazy inline są zastępowane URI plików platformy. Pole pomijane po wygaśnięciu przechowywania danych wejściowych |
output | object | Opis wyniku. Ma znaczenie tylko przy succeeded |
errorCode | string | Obecne przy state równym failed lub expired (wygasłe zadanie ma timeout). Jedna z dziewięciu wartości — zobacz Kody niepowodzenia zadania. Kieruj logikę według tego pola, nigdy errorMessage |
errorMessage | string | Czytelny powód niepowodzenia, po angielsku, chyba że wybrano inny język. Obecne tylko przy niepowodzeniu |
cost | string | Kwota rozliczona albo rezerwacja, jeżeli rozliczenie się nie zakończyło; opłata końcowa nie może przekroczyć tej rezerwacji |
settled | boolean | Czy rozliczenie jest ostateczne. Przy false wartość cost może się jeszcze zmienić |
createdAt | string | RFC 3339 |
deadlineAt | string | Termin wykonania zapisany przy przyjęciu; starsze zdarzenia webhooków mogą go pomijać |
completedAt | string | RFC 3339. Tylko stany końcowe |
contentState | string | present, expired (usunięcie po okresie przechowywania) lub purged (usunięcie przez Ciebie). Zobacz Przechowywanie i usuwanie danych |
contentRemovedBy | string | user lub system; pomijane, dopóki treść istnieje |
purgedAt | string | RFC 3339. Tylko po usunięciu treści na Twoje żądanie |
retention | object | outputsExpireAt (termin usunięcia wyników), promptsExpireAt (termin usunięcia promptu) i source (ustawienie, które rozstrzygnęło) |
Cztery odrębne terminy
- Własny czas oczekiwania wybierasz samodzielnie. Zakończenie oczekiwania nie anuluje zdalnego zadania: zachowaj
taskIdi później odpytaj to samo zadanie, aby wznowić obsługę. deadlineAtjest terminem wykonania po stronie serwera. Idempotentne powtórzenie go nie wydłuża; jawne ponowienie tworzy nowe zadanie z własnym terminem. Odczytuj stan końcowy isettled, zamiast wnioskować o zwrocie z lokalnego zegara.output.assets[].expiresAtokreśla wygaśnięcie tymczasowego linku wyniku, zwykle po 20 minutach. Kolejne zapytanie odświeża link bez tworzenia nowego zadania.- Wygenerowane pliki są przechowywane maksymalnie około 14 dni od zakończenia; dane wejściowe przesłane na platformę — maksymalnie jedną dobę. To limity platformy: możesz ustawić krótsze okresy w sekcji Przechowywanie i usuwanie danych lub usunąć treść jednego zadania na żądanie. Odświeżenie linku nie wydłuża przechowywania samego pliku.
W JavaScript SDK client.run({ model, input }, { idempotencyKey, onAccepted, timeoutMs }) wysyła zadanie raz i odpytuje z rosnącym odstępem od 2 do maksymalnie 10 sekund; jego timeoutMs określa tylko lokalne oczekiwanie, więc zapisz ID z onAccepted. Zobacz SDK. Dla zadań w tle wybieraj webhooki. Po zweryfikowaniu kompletnego callbacku wyniku użyj jego wyniku bezpośrednio, bez kolejnego zapytania o zadanie.
Korzystaj bezpośrednio z adresu wyniku
Gotowe pliki zawierają url i expiresAt wraz z key i metadanymi mediów. Pobieraj output.assets[].url bezpośrednio, bez danych uwierzytelniających API — na przykład curl -L -o result.png "<url>". Odpytaj ponownie po wygaśnięciu adresu; /common/download-url pozostaje opcjonalne dla istniejących integracji. Adresy zwykle działają 20 minut i nie wydłużają 14-dniowego okresu przechowywania wyników.
{
"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"
}Udane zapytanie nie oznacza udanego zadania
Obie odpowiedzi zawierają code: 200 — powiodło się zapytanie. Wynik zadania określa data.state. Kod biznesowy opisuje odpowiedź, a state zadanie. Nie łącz tych znaczeń.
Treść webhooku ma tę samą strukturę: tę samą otoczkę code: 200 i te same pola data. Kod odczytujący odpowiedź z tej sekcji odczyta callback bez zmian — dwa sposoby dostarczenia, jeden parser.
Struktura output
output ma dwie niezależne części. Wygenerowane pliki — obrazy, filmy, dźwięk — trafiają do output.assets[]. Wyniki tekstowe trafiają do output.text jako zwykły ciąg znaków.
Większość modeli wypełnia dokładnie jedną z części, ale model może wypełnić obie. Rodzaj otrzymanego wyniku jest właściwością endpointu, a nie jego modality: endpoint speech-to-text jest w katalogu grupowany jako audio, ponieważ taki materiał odczytuje, ale odpowiada tekstem.
`output.assets` nie zawsze istnieje
Endpoint z wynikiem tekstowym nie ma w ogóle klucza assets — nie jest to pusta tablica. Kod otwierający wynik przez task.output.assets[0].url zgłosi błąd dla takich zadań, zamiast zwrócić wartość do sprawdzenia. Najpierw odczytaj output.text, jeżeli występuje, a assets traktuj jako opcjonalne:
const { text, assets = [] } = task.output ?? {};
if (text) console.log(text);
for (const asset of assets) console.log(asset.url);Wynik tekstowy nie ma pliku do pobrania ani linku, który może wygasnąć, ale nadal jest częścią wyniku zadania: jest usuwany według tego samego harmonogramu co inne wyniki, a jego contentState zmienia się na expired. Skopiuj wszystko, co chcesz zachować. Wynik złożony z kilku części — na przykład segmentów transkrypcji — jest łączony w jeden ciąg znaków z podziałami wierszy między częściami.
Pozostała część tej sekcji opisuje assets[].
| Pole | Typ | Uwagi |
|---|---|---|
key | string | Identyfikator zgodności do opcjonalnego odświeżania linku pobierania. Pusty przy pending |
url | string | Bezpośredni podpisany adres GET. Pomijany przy oczekującym lub niedostępnym pliku; nie dołączaj klucza API |
expiresAt | string | Termin wygaśnięcia adresu w RFC 3339; odpytaj ponownie, aby odświeżyć go w okresie przechowywania |
mime | string | Typ treści, np. image/png, video/mp4 |
width · height | integer | Wymiary w pikselach. Brak dla dźwięku i tekstu |
durationSeconds | number | Długość w sekundach. Tylko film i dźwięk |
bytes | integer | Rozmiar w bajtach |
pending | boolean | Zobacz niżej |
unavailable | boolean | Zobacz niżej |
layer | object | Tylko przy rozdzielaniu na warstwy: zIndex (integer), boundingBox ([x1, y1, x2, y2], opcjonalne) i name (opcjonalne). Zobacz niżej |
Pola, dla których nie mamy wartości, są pomijane — nie mają wartości null ani 0. Odczytuj je jako opcjonalne.
Obsłuż dwie flagi:
pending: true— plik nadal jest kopiowany do naszego magazynu, akeyiurlsą na razie puste. Odpytaj ponownie za kilka sekund; wywołanie terazdownload-urlbezkeyzwraca409.unavailable: true— plik wygasł lub jest trwale niedostępny po niepowodzeniu przenoszenia. Nie da się go odzyskać przez czekanie; trzeba wygenerować ponownie. Jeżeli nie udało się zapisać żadnego pliku udanego zadania, nie naliczamy za nie opłaty.
Wyniki rozdzielania na warstwy dodają jeszcze pole layer. Pierwszy plik jest obrazem bazowym (źródłem z usuniętymi wydzielonymi elementami, zIndex 0); każdy kolejny jest przezroczystą warstwą PNG. Ułóż je według rosnącego zIndex, aby odtworzyć obraz. boundingBox to [x1, y1, x2, y2] w pikselach obrazu bazowego: przeskaluj warstwę tak, aby wypełniała ten prostokąt, a wróci na swoje miejsce. Modele niepodające pozycji pomijają to pole; warstwy nadal działają jako samodzielne wycięte elementy. name jest własną etykietą modelu i występuje tylko wtedy, gdy model ją poda. Rozliczenie obejmuje każdy zwrócony obraz, również bazowy.
4. Najpierw callbacki, odpytywanie jako opcja zapasowa
Odpytywanie oznacza, że kod wielokrotnie pyta recordInfo, aż zadanie się zakończy. Callback oznacza, że raz wysyłamy zakończone zadanie na Twój serwer. Obie opcje dostarczają te same dane.
| Callback | Odpytywanie | |
|---|---|---|
| Opóźnienie | W chwili zakończenia zadania | Zależy od odstępu między zapytaniami |
| Żądania | 1 na zadanie, wysłane przez nas | N na zadanie, wysłane przez Ciebie |
| Zużywa Twój limit częstotliwości | Nie | Tak — zobacz Limity częstotliwości |
| Wymaga publicznego endpointu HTTPS | Tak | Nie |
W produkcji korzystaj z callbacków. Trzyminutowe zadanie wideo odpytywane co sekundę oznacza 180 żądań, z których niemal żadne nie wnosi nowych informacji. Aktualne ogólne zabezpieczenie API — 1000 żądań na 10 sekund — nie ogranicza zwykłego płatnego generowania, ale częste odpytywanie tworzy zbędny ruch, a niekontrolowana pętla wyczerpuje własną pulę odczytów, aż recordInfo zaczyna zwracać 429.
Odpytywanie ma sens w dwóch sytuacjach: podczas pracy lokalnej oraz w usługach bez publicznego dostępu z internetu.
Zwiększaj odstępy podczas odpytywania
recordInfo korzysta z puli odczytów, a createTask z puli zapisów, więc odpytywanie nie może wyczerpać limitu wysyłania zadań. Zbyt częste zapytania sprawią jednak, że samo recordInfo zacznie zwracać 429: stracisz podgląd zadań do czasu odnowienia puli.
Sprawdzony schemat:
- Zacznij od 2–3 sekund. Krótszy odstęp jest zbędny; nawet najszybsze modele obrazowe potrzebują kilku sekund.
- Mnożnik 1,5, z maksimum 10–15 sekund.
- Kończ według terminu samego zadania, nie stałej liczby minut. Niektóre modele wideo mogą działać do godziny, więc stały limit 10 minut porzuca zadania nadal działające prawidłowo. Korzystaj z
deadlineAtz odpowiedzicreateTask, dodając kilka minut na zapisanie wyniku przez serwer.
Koniec oczekiwania nie oznacza niepowodzenia
Gdy kończy się Twój czas oczekiwania, jedynie przestajesz czekać. Zadanie nadal działa, może się powieść i zostać rozliczone. Nie wysyłaj go ponownie z nowym Idempotency-Key — to tworzy drugie płatne zadanie. Odpytaj później to samo taskId lub znajdź je w historii zadań.
// 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. Sprawdź saldo
GET /api/v1/chat/credit zwraca available (środki, które mogą finansować nowe zadania), held (zablokowane przez zadania w toku) i total. available może być ujemne przy korzystaniu z zatwierdzonego kredytu, a środki promocyjne w funding.grants mogą obejmować tylko część modeli. Pełne wyjaśnienie: Sprawdzanie salda.
curl https://api.spicyapi.ai/api/v1/chat/credit \
-H "Authorization: Bearer $SPICY_API_KEY"Historia zadań i odzyskiwanie
GET /api/v1/jobs wyświetla widoczne zadania utworzone bieżącym kluczem API. Użyj go, aby odnaleźć pracę po ponownym uruchomieniu aplikacji lub pominiętym callbacku. Zadania innych kluczy i ukryte rekordy są wyłączone. Odpowiedzi zawierają ID zadań, model, stan, koszt, stan rozliczenia, znaczniki czasu oraz, jeżeli zapisano, requestId wywołania tworzącego zadanie (dla wywołania tekstowego tę samą wartość co w nagłówku X-Request-Id); nie zawierają promptów, danych wejściowych, wyników ani linków do plików. Odczytaj wybrane zadanie przez recordInfo, aby uzyskać wynik.
Filtruj według state, dokładnego katalogowego model lub aliasu oraz dat UTC from / to (YYYY-MM-DD, początek włącznie / koniec wyłącznie). Domyślnie zakres obejmuje siedem dni do jutrzejszej daty UTC; maksymalnie 92 dni. limit domyślnie wynosi 20 i nie może przekroczyć 100.
Pomijaj nieużywane filtry: pusta wartość, taka jak model=, powtórzony parametr lub nieznany parametr zwracają 400. Zapytanie trwające ponad pięć sekund zwraca 503; spróbuj ponownie z krótszym zakresem dat.
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"Odczytaj data.items. Jeżeli data.hasMore jest prawdziwe, przekaż data.nextCursor jako cursor kolejnego żądania, zachowując wszystkie filtry i jawnie podane daty. Kolejność jest malejąca według createdAt, a następnie ID zadania. Strony odzwierciedlają aktualny stan zadań, nie niezmienny obraz danych. Nie konstruuj kursorów samodzielnie. Wyświetlanie historii nigdy nie tworzy ani nie ponawia zadania.
Ta sama funkcja jest dostępna przez client.listTasks(...), spicyapi tasks list i narzędzie MCP tylko do odczytu spicyapi_tasks_list. Zachowuj cost w USD jako dziesiętny ciąg znaków; gdy settled jest fałszywe, to nadal zarezerwowana wycena, nie ostateczna opłata.
Jeżeli odpowiedź tworzenia zaginęła, najpierw ponów niezmienione żądanie z jego pierwotnym Idempotency-Key. Pusta strona historii nie uprawnia do utworzenia tej samej płatnej pracy ponownie z nowym kluczem.
Katalog modeli i schematy
Korzystaj z uwierzytelnionego katalogu na żywo, aby wybierać dostępne modele, odczytywać ich ceny i budować żądania według aktualnego JSON Schema.
Media
Bezpośrednie przesyłanie mediów referencyjnych przez podpisane adresy oraz krótkotrwałe linki do pobierania plików wynikowych.

