spicyapiDokumentacja
Budowanie z API
Treść główna

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:

  1. 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.
  2. Później pytasz „czy to taskId już 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).
  3. Gdy state wynosi succeeded, link do pliku znajduje się w data.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 w settled i cost.

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/createTask

To 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łówekWymaganyUwagi
AuthorizationTakBearer sk-spicy-…
Content-TypeTakapplication/json
Idempotency-KeyNieZdecydowanie zalecany: zapewnia bezpieczne ponowienie po błędzie sieci. Zobacz Idempotencja
X-Spicy-RetentionNieKró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: 2

Status 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"
}
PoleUwagi
taskIdID zadania z prefiksem job_. Będzie potrzebne w kolejnych operacjach, więc je zapisz
statequeued dla nowego zadania; bieżący stan pierwotnego zadania przy idempotentnym powtórzeniu z tym samym kluczem
estimatedCostKwota 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ę
deadlineAtNajpóź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.

HTTPcodeTypowe znaczenieCo zrobić
400400Pole w input nie należy do schematu modelu, wartość jest poza zakresem lub callBackUrl jest nieprawidłowePorównaj z inputSchema modelu; msg wskazuje problem
40040004Każde pole jest prawidłowe, lecz ten zestaw obecnie nie może być obsłużonyZmień zestaw, na przykład rozdzielczość lub czas
401401Brakuje klucza API, jest błędny lub wyłączonySprawdź nagłówek Authorization
40240201Saldo nie wystarcza na rezerwację estimatedCostZasil konto w konsoli i wyślij ponownie z tym samym Idempotency-Key
40240202Osiągnięto limit wydatków klucza, zespołu lub kontaPodnieś limit w konsoli lub poczekaj na odnowienie dziennego albo miesięcznego limitu
40340301Klucz nie ma uprawnienia do wywołania modeluUżyj innego klucza lub zmień ograniczenia modeli klucza
409409 / 40901Konflikt idempotencji lub wycena, która już nie pasujeZobacz Idempotencja i Wyceny
413413Treść żądania jest za duża, zwykle z powodu mediów inlinePrześlij plik i podaj jego URI spicy://; zobacz Pliki multimedialne
429429Zbyt wiele żądań w krótkim czasieOdczekaj według Retry-After, a następnie wyślij z tym samym kluczem
50350301Model jest chwilowo niedostępnySpró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.

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"

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.

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"])

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

queued
running
succeededfailedexpired
Zadanie jest zawsze w dokładnie jednym stanie. Opłata naliczana jest tylko przy succeeded; każdy inny stan końcowy zwraca zablokowaną kwotę w całości.

Przyję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.

StanZnaczenieKońcowyRozliczenie
queuedUtworzone, środki zarezerwowane, oczekuje na podjęcieNieZarezerwowane, nierozliczone
runningModel generuje; trwa oczekiwanie na wynikNieZarezerwowane, nierozliczone
succeededGenerowanie zakończyło się powodzeniemTakRozliczenie rzeczywistego wykorzystania do wysokości rezerwacji
failedGenerowanie nie powiodło sięTakZwykle 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
canceledWyłącznie zgodność z historią; brak dostępnej operacji anulowaniaTakCała rezerwacja zwolniona
expiredPrzekroczono maksymalny czas wykonania modeluTakCał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
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"}}'
WynikOdpowiedź
Stan końcowy w dostępnym czasie200 z rekordem zadania — ta sama struktura co w recordInfo
Czas oczekiwania się kończyZwykł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
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

PoleTypUwagi
taskIdstringID zadania
sourceTaskIdstringTylko w zadaniu utworzonym przez ponowienie: ID zadania źródłowego
modelstringIdentyfikator modelu
statestringZobacz tabelę wyżej
inputobjectZweryfikowane parametry; obrazy inline są zastępowane URI plików platformy. Pole pomijane po wygaśnięciu przechowywania danych wejściowych
outputobjectOpis wyniku. Ma znaczenie tylko przy succeeded
errorCodestringObecne 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
errorMessagestringCzytelny powód niepowodzenia, po angielsku, chyba że wybrano inny język. Obecne tylko przy niepowodzeniu
coststringKwota rozliczona albo rezerwacja, jeżeli rozliczenie się nie zakończyło; opłata końcowa nie może przekroczyć tej rezerwacji
settledbooleanCzy rozliczenie jest ostateczne. Przy false wartość cost może się jeszcze zmienić
createdAtstringRFC 3339
deadlineAtstringTermin wykonania zapisany przy przyjęciu; starsze zdarzenia webhooków mogą go pomijać
completedAtstringRFC 3339. Tylko stany końcowe
contentStatestringpresent, expired (usunięcie po okresie przechowywania) lub purged (usunięcie przez Ciebie). Zobacz Przechowywanie i usuwanie danych
contentRemovedBystringuser lub system; pomijane, dopóki treść istnieje
purgedAtstringRFC 3339. Tylko po usunięciu treści na Twoje żądanie
retentionobjectoutputsExpireAt (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 taskId i później odpytaj to samo zadanie, aby wznowić obsługę.
  • deadlineAt jest 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 i settled, zamiast wnioskować o zwrocie z lokalnego zegara.
  • output.assets[].expiresAt okreś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.

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"
}

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[].

PoleTypUwagi
keystringIdentyfikator zgodności do opcjonalnego odświeżania linku pobierania. Pusty przy pending
urlstringBezpośredni podpisany adres GET. Pomijany przy oczekującym lub niedostępnym pliku; nie dołączaj klucza API
expiresAtstringTermin wygaśnięcia adresu w RFC 3339; odpytaj ponownie, aby odświeżyć go w okresie przechowywania
mimestringTyp treści, np. image/png, video/mp4
width · heightintegerWymiary w pikselach. Brak dla dźwięku i tekstu
durationSecondsnumberDługość w sekundach. Tylko film i dźwięk
bytesintegerRozmiar w bajtach
pendingbooleanZobacz niżej
unavailablebooleanZobacz niżej
layerobjectTylko 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, a key i url są na razie puste. Odpytaj ponownie za kilka sekund; wywołanie teraz download-url bez key zwraca 409.
  • 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.

CallbackOdpytywanie
OpóźnienieW chwili zakończenia zadaniaZależy od odstępu między zapytaniami
Żądania1 na zadanie, wysłane przez nasN na zadanie, wysłane przez Ciebie
Zużywa Twój limit częstotliwościNieTak — zobacz Limity częstotliwości
Wymaga publicznego endpointu HTTPSTakNie

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 deadlineAt z odpowiedzi createTask, 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ń.

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. 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
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.

Na tej stronie