Szybki start
Uzyskaj klucz, utwórz zadanie i odbierz wynik. Cały proces od początku do końca.
SpicyAPI udostępnia generowanie obrazów i wideo przez jeden protokół uwierzytelniania, zadań i wyników, a modele tekstowe obsługuje w znanych ci formatach OpenAI, Anthropic i Gemini. Zachowaj ten sam przebieg wywołań przy zmianie modeli i buduj input na podstawie inputSchema danego modelu (listy akceptowanych pól, ich typów i dozwolonych wartości).
Ten poradnik używa natywnego API zadań pod https://api.spicyapi.ai/api/v1. Endpointy zgodności dla tekstu i wideo używają https://api.spicyapi.ai/v1. Oba przyjmują ten sam klucz API, ale mają różne formaty żądań i odpowiedzi.
Przejdź przez tę stronę, aby wybrać model i pobrać wynik. Aby poznać dokładną cenę przed zapłatą, zobacz Wyceny i zgodność. Dla czatu zobacz Tekst i strumieniowanie. Przed wdrożeniem integracji przejdź przez Integrację produkcyjną; gdy któryś krok się nie powiedzie, skorzystaj z Rozwiązywania problemów. Nieznane pojęcia wyjaśnia słowniczek.
Chcesz zobaczyć wynik przed napisaniem kodu?
Otwórz dowolny model na spicyapi.ai/explore i użyj zakładki Playground; zakładka API pokazuje to samo żądanie jako polecenie gotowe do skopiowania. Aby działać z terminala bez pisania kodu, użyj CLI lub powierz to asystentowi AI przez serwer MCP.
Oficjalne narzędzia
Bezpośrednie wywoływanie API HTTP jest w pełni obsługiwane i pokazuje je dalsza część strony. Jeśli wolisz nie pisać tych wywołań samodzielnie, dostępne są cztery pakiety npm na licencji MIT, każdy instalowany oddzielnie i z własnym poradnikiem:
@spicyapi/sdk
SDK TypeScript: typy OpenAPI, adaptacyjne oczekiwanie, przesyłanie plików i weryfikacja webhooków
@spicyapi/cli
Polecenie spicyapi: katalog, wycena, tworzenie i śledzenie zadań z terminala
@spicyapi/mcp
Serwer MCP udostępniający agentowi programującemu katalog, wyceny i zadania, z potwierdzeniem każdego płatnego kroku
@spicyapi/skill
Przenośny Agent Skill: proces pracy i granice wydatków, do zainstalowania w agencie
Cztery dodatkowe oficjalne SDK obsługują języki spoza Node.js. Każde realizuje ten sam proces i jest na licencji MIT; obecnie tylko moduł Go instaluje się z repozytorium zwykłym poleceniem, a pozostałe trzy strony podają sposób instalacji stosowany do czasu publikacji pakietów:
SDK Python
spicyapi dla Python 3.11+, bez zależności czasu wykonania
SDK Go
github.com/spicyapi-ai/spicy-go dla Go 1.23+, wyłącznie biblioteka standardowa
SDK PHP
spicyapi/spicyapi dla PHP 8.1+, bez zależności od bibliotek
SDK Java
ai.spicyapi:spicyapi-java dla Java 17+, z zamkniętą hierarchią typów błędów
Pracujesz w ComfyUI? Węzły ComfyUI umieszczają każdy model obrazów, wideo i audio na twoim obszarze roboczym jako węzeł, z ceną widoczną przed uruchomieniem.
Zanim zaczniesz
Potrzebujesz czterech rzeczy. Pominięcie jednej spowoduje później błąd, który nie wskaże, czego zabrakło.
- Konto ze zweryfikowanym adresem e-mail. Zarejestruj się na spicyapi.ai i kliknij link w e-mailu weryfikacyjnym. Do tego czasu każde wywołanie twoim kluczem zwraca
403. - Saldo. Generowanie jest opłacane z salda w dolarach amerykańskich (nie ma kredytów), a nowe konto zaczyna od zera. Dodaj środki w Konsola → Rozliczenia. Bez wystarczającego salda tworzenie zadania kończy się kodem biznesowym
40201. Konfiguracja konta prowadzi przez rejestrację, doładowanie i tworzenie klucza. - Terminal. macOS: aplikacja Terminal. Linux: dowolna powłoka. Windows: PowerShell. Gdy polecenia się różnią, każdy krok ma zakładkę Windows PowerShell. Jeśli wolisz, polecenia macOS / Linux działają też bez zmian w WSL lub Git Bash w Windows.
- Kilka centów. Krok 3 tworzy jedno rzeczywiste zadanie obrazu, rozliczane po powodzeniu. Krok 2 pokazuje cenę każdego modelu, a opcjonalne polecenie w tym kroku pozwala najpierw bezpłatnie poznać dokładną kwotę.
Najpierw oceń ofertę: modele i ceny bez konta
Nie potrzebujesz klucza ani konta, aby zobaczyć dostępną ofertę i ceny. Publiczny katalog używany przez stronę marketingową można odczytać anonimowo, a w tym poleceniu niczego nie trzeba zastępować:
curl "https://api.spicyapi.ai/console/v1/catalog/models?locale=en"Struktura odpowiedzi jest taka sama jak opisana na końcu tej strony. Każda pozycja w data.items zawiera:
model— dokładną wartość, którą później wyślesz jakomodel(slugzawiera tę samą wartość);callable— czy model może działać w tej chwili. Wybieraj pozycje ztrue:statusokreśla nasz zamierzony stan operacyjny, a modellivenadal może przejściowo nie nadawać się do wywołania;modalityitasks— rodzaj generowanego wyniku;price— najtańszy poziom:amountw USD naunit(per_image,per_second,per_requestlubper_1k_tokens), a takżetiers, czyli liczba poziomów ceny. Dla modeli tekstowych odczytujprice.perMillionTokens, które podaje osobne ceny za milion tokenów wejściowych, wejściowych z pamięci podręcznej i wyjściowych; samoamountjest tylko stawką wyjściową.
Dodaj identyfikator modelu, aby uzyskać jego pełny rekord, w tym inputSchema, paramNotes i opracowane examples:
curl "https://api.spicyapi.ai/console/v1/catalog/models/MODEL_ID_FROM_CATALOG?locale=en"MODEL_ID_FROM_CATALOG jest symbolem zastępczym używanym w całej dokumentacji, a nie prawdziwym modelem: zawsze kopiuj rzeczywisty identyfikator z katalogu na żywo.
locale domyślnie ma wartość en; przekaż ja, ko, de, fr, es, pt-BR, ru, it lub pl, aby uzyskać przetłumaczone nazwy, opisy i notatki parametrów. Każda inna wartość zwraca 400. strict=1 usuwa pozycje, których tekst nie jest opublikowany w tym języku, zamiast wyświetlać angielski; zwykle nie jest potrzebne. Ten obszar ma limit według adresu źródłowego, a nie konta, i własną pulę — zobacz Limity żądań. Jest też buforowany, więc zmiana ceny może pojawić się tu z opóźnieniem.
Gdy tiers jest większe od 1, podane price oznacza najtańszy poziom, a nie cenę twoich ustawień. price.variant pokazuje ustawienia tego najtańszego poziomu (na przykład 720p lub generate_audio=false;resolution=480p, gdy łączy się kilka pól), a inputSchema["x-pricing"].variantFields w rekordzie pojedynczego modelu, jeśli jest obecne, wymienia wszystkie pola wybierające poziom ceny. pricingNote rekordu jest ogólnym przypomnieniem, że wiążąca jest aktualna wycena; nie wymienia pól. Aby poznać dokładną kwotę jednego żądania, użyj POST /api/v1/jobs/quote, gdy już masz klucz.
Publiczny katalog do oceny oferty, uwierzytelniony katalog do wywołań
Publiczny katalog odpowiada na pytanie „co jest w sprzedaży i jaka jest podana cena”. Nie odpowie na „co może wywołać mój klucz” ani „ile zapłaci moje konto”, ponieważ oba zależą od konta. Po uzyskaniu klucza przejdź na GET /api/v1/models w kroku 2 i traktuj go jako jedyne źródło identyfikatorów dostępnych do wywołania — zapamiętany identyfikator, migawka premiery ani strona marketingowa nie są źródłem zastępczym.
Pięć kroków
Uzyskaj klucz
Klucz API jest hasłem, które twój kod wysyła z każdym żądaniem. Identyfikuje konto i umożliwia jego rozliczanie.
- Otwórz Konsola → Klucze API i kliknij Utwórz klucz.
- Nadaj nazwę, na przykład
my-first-test. Pozostaw wszystkie ustawienia Zaawansowane bez zmian. - Skopiuj klucz zaczynający się od
sk-spicy-lub kliknij Zapisz jako plik. Jest pokazywany dokładnie raz; jeśli go stracisz, utwórz nowy. - W terminalu używanym do kolejnych kroków zapisz go w zmiennej środowiskowej (nazwanej wartości przekazywanej przez terminal do każdego uruchamianego polecenia), aby nigdy nie wpisywać klucza bezpośrednio w poleceniu:
export SPICY_API_KEY="sk-spicy-paste-your-key-here"
# Check: this should print sk-spicy-
echo "${SPICY_API_KEY:0:9}"Co powinno się pojawić: sk-spicy-. Wartość istnieje do zamknięcia tego okna terminala, więc każdy następny krok wykonuj w tym samym oknie.
Jeśli nie ma przycisku Utwórz klucz, twoja rola w aktualnym zespole nie pozwala tworzyć kluczy: poproś właściciela zespołu lub użyj Załóż własny zespół na tej samej stronie. Nowe klucze nie mają limitu wydatków, dopóki go nie ustawisz; dzienny limit w konsoli ogranicza skutki błędnej konfiguracji lub wycieku. Zobacz Uwierzytelnianie.
Wybierz model dostępny do wywołania
Uwierzytelniony katalog jest jedynym źródłem identyfikatorów dostępnych do wywołania, aktualnych schematów i przykładów. Pobierz go w dwóch wywołaniach: lekka lista wskazuje, co możesz wywołać, a jeden rekord daje schemat wybranego modelu. Odczyt katalogu jest bezpłatny.
curl "https://api.spicyapi.ai/api/v1/models?modality=image&task=text-to-image" \
-H "Authorization: Bearer $SPICY_API_KEY"Odpowiedź jest długa. Potrzebny fragment wygląda tak (w skrócie):
{
"code": 200,
"msg": "success",
"data": {
"total": 1,
"items": [
{
"model": "MODEL_ID_FROM_CATALOG",
"displayName": "…",
"modality": "image",
"tasks": ["text-to-image"],
"enabled": true,
"available": true,
"startingPrice": { "variant": "", "unit": "per_image", "price": "0.008", "currency": "USD" }
}
]
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}Wybierz pozycję, której enabled i available mają wartość true, sprawdź cenę, skopiuj model i zachowaj tę wartość do końca tej strony. Z zainstalowanym jq polecenie curl … | jq -r '.data.items[] | select(.enabled and .available) | .model' wypisuje tylko identyfikatory dostępne do wywołania.
# Replace MODEL_ID_FROM_CATALOG with the model value you copied
export SPICY_MODEL="MODEL_ID_FROM_CATALOG"curl "https://api.spicyapi.ai/api/v1/models/${SPICY_MODEL}" \
-H "Authorization: Bearer $SPICY_API_KEY"Ukośniki w identyfikatorze przechodzą przez ścieżkę bez zmian; koduj je procentowo tylko wtedy, gdy klient HTTP wymaga traktowania parametru ścieżki jako pojedynczego segmentu. inputSchema rekordu jest wiążącym źródłem dla input: required wymienia pola, które musisz wysłać, a properties opisuje każde z nich. examples zawiera dane wejściowe już zwalidowane przez serwer. Odczyt pozostałych pól rekordu, w tym pricing, opisuje Katalog modeli i schematy.
Pobieraj schematy tylko tam, gdzie ich potrzebujesz. Dodanie includeSchema=1&includeExamples=1 do wywołania listy zwraca pełny JSON Schema każdego włączonego modelu w jednej odpowiedzi, co daje znacznie większą treść. Odczyty katalogu korzystają z osobnego limitu odczytów konta i nie zużywają limitu tworzenia zadań. Nie odczytuj też katalogu przed każdym zadaniem; zobacz Buforowanie katalogu.
Jeśli żadna pozycja nie pasuje, zatrzymaj się przed utworzeniem płatnego zadania. Migawka premiery, strona marketingowa ani zapamiętany identyfikator modelu nie zastępują pustego katalogu na żywo. Aby wybrać model z kodu zamiast ręcznie, zobacz Wybór modelu z kodu.
Opcjonalnie: najpierw sprawdź dokładną cenę. Wycena jest bezpłatna, niczego nie tworzy i niczego nie rezerwuje. data.estimatedCost wskazuje maksymalny koszt tego dokładnego żądania w USD:
curl -X POST https://api.spicyapi.ai/api/v1/jobs/quote \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
--data "{\"model\":\"${SPICY_MODEL}\",\"input\":{\"prompt\":\"A cinematic night portrait\"}}"Utwórz zadanie
Zadanie to generowanie wykonywane w tle: od razu otrzymujesz identyfikator, a wynik później. Ten krok jest płatny.
Każde nowe zadanie otrzymuje nowy klucz idempotencji: losowy identyfikator, który dołączasz, aby dwukrotne wysłanie tego samego żądania, na przykład po błędzie sieci, nigdy nie utworzyło dwóch zadań. Wygeneruj go osobnym poleceniem, a następnie wyślij zadanie:
SPICY_IDEMPOTENCY_KEY="$(uuidgen)"
# Linux without uuidgen: SPICY_IDEMPOTENCY_KEY="$(cat /proc/sys/kernel/random/uuid)"curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
--data "{\"model\":\"${SPICY_MODEL}\",\"input\":{\"prompt\":\"A cinematic night portrait\"}}"Jeśli 3b przekroczy czas oczekiwania lub połączenie się zerwie, uruchom 3b ponownie bez wykonywania 3a: ten sam klucz zwróci już rozpoczęte zadanie zamiast naliczyć drugą opłatę. Aby wygenerować inny obraz, najpierw wykonaj 3a. Odpowiedź 409 oznacza ponowne użycie klucza z innym promptem lub modelem. Więcej w Idempotencji.
To żądanie wysyła jedno pole. Jeśli schemat odczytany w kroku 2 oznacza inne pola jako wymagane, dodaj je — to schemat każdego modelu określa, co wolno umieścić w input, a jedno nieznane pole oznacza 400.
Połączenie nigdy nie pozostaje otwarte. Identyfikator zadania otrzymujesz od razu:
HTTP/1.1 202 Accepted{
"code": 200,
"msg": "success",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"state": "queued",
"estimatedCost": "0.008",
"deadlineAt": "2026-08-28T09:22:04Z"
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}W macOS / Linux skopiuj taskId z odpowiedzi i zachowaj go (PowerShell zrobił to już za ciebie):
export SPICY_TASK_ID="paste-the-taskId-here"202 oznacza przyjęcie, a nie ukończenie
Nowo utworzone zadanie ma state: "queued"; bezpieczne powtórzenie z tym samym Idempotency-Key zwraca pierwotne zadanie w jego aktualnym stanie. HTTP 202 oznacza przyjęcie żądania, nie ukończenie generowania. Przy pierwszej akceptacji środki są blokowane, a nie pobierane: estimatedCost jest odkładane z twojego salda. Ta blokada jest też maksymalną opłatą za zadanie: niewykorzystane środki są zwalniane i później nie pobiera się kwoty ponad nią. Nieudane zadania zwykle zwalniają blokadę; niektóre odmowy związane z treścią, ujawnione na stronie modelu, są rozliczane według zaakceptowanej wyceny. Sprawdź settled i cost. deadlineAt określa najpóźniejszy czas zakończenia, po którym zadanie przechodzi do expired. Kod biznesowy powodzenia code w strukturze odpowiedzi pozostaje 200. Zaakceptowanego zadania nie można anulować.
Odbierz wynik
Jeśli żądanie produkcyjne zawiera callBackUrl, wywołamy twój serwer, gdy zadanie osiągnie stan końcowy: callback (webhook) to adres HTTPS na twoim serwerze wywoływany po zakończeniu zadania, dzięki czemu nie trzeba ciągle pytać o stan. Minimalne powyższe żądanie go pomija, aby mogło działać bez publicznego endpointu. Podpisy callbacków trzeba weryfikować — zobacz Webhooki.
Bez endpointu callback odpytaj zadanie:
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${SPICY_TASK_ID}" \
-H "Authorization: Bearer $SPICY_API_KEY"Używaj tego samego klucza API, który utworzył zadanie: inny klucz otrzyma 404. Sprawdź data.state:
data.state | Znaczenie | Co zrobić |
|---|---|---|
queued lub running | Generowanie nadal trwa. Środki są zablokowane, nie wydane | Poczekaj kilka sekund i wykonaj to samo polecenie ponownie. Obrazy zwykle powstają w kilka sekund; wideo może trwać minuty. deadlineAt jest górną granicą |
succeeded | Gotowe | Przejdź do następnego kroku |
failed lub expired | Nie powiodło się. Blokada jest zwykle zwalniana; niektóre odmowy związane z treścią, wyraźnie ujawnione na stronie modelu, są płatne. Sprawdź settled i cost | Odczytaj data.errorCode (stabilny) i data.errorMessage (dla człowieka), popraw dane wejściowe i zacznij od 3a |
code struktury odpowiedzi wynosi 200 nawet przy nieudanym zadaniu: code mówi, czy powiódł się odczyt zadania. Własny wynik zadania zawsze znajduje się w data.state. Skrócony wygląd odpowiedzi na kolejnych etapach:
{
"code": 200,
"msg": "success",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"model": "MODEL_ID_FROM_CATALOG",
"state": "running",
"cost": "0.008",
"settled": false,
"createdAt": "2026-08-28T09:12:04Z",
"deadlineAt": "2026-08-28T09:22:04Z"
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w4"
}Gdy settled ma wartość false, cost jest kwotą zablokowaną, a nie ostateczną opłatą.
Pobierz plik wynikowy
Weź data.output.assets[0].url z udanego wyniku i pobierz go bezpośrednio. Nie dołączaj klucza API. Link działa około 20 minut (expiresAt); jeśli wygasł, wykonaj ponownie krok 4, ponieważ każdy odczyt zwraca nowy link. Wygenerowane pliki są usuwane po najwyżej 14 dniach, więc zachowaj własną kopię potrzebnych materiałów.
# Paste data.output.assets[0].url. Keep the double quotes: the URL contains & characters.
RESULT_URL="paste-the-url-here"
curl -L "$RESULT_URL" -o result.png # use result.mp4 when mime is video/mp4Z zainstalowanym jq możesz pominąć kopiowanie i wklejanie:
RESULT_URL="$(curl -s "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${SPICY_TASK_ID}" \
-H "Authorization: Bearer $SPICY_API_KEY" | jq -r '.data.output.assets[0].url')"Co powinno się pojawić: plik result.png w folderze, w którym działa terminal. Sprawdź opłatę w Konsola → Logi lub w cost, gdy settled ma wartość true.
Modele tekstowe: jedno żądanie, odpowiedź bezpośrednio w odpowiedzi HTTP
Modele tekstowe odpowiadają w tej samej odpowiedzi HTTP: bez odpytywania i pobierania pliku. Nagłówek odpowiedzi X-Spicy-Task-Id wskazuje zadanie zawierające opłatę, jeśli chcesz sprawdzić ją później. W kroku 2 zamiast tego pobierz modele z modality=text, wyeksportuj jeden jako SPICY_MODEL, a następnie:
curl https://api.spicyapi.ai/v1/chat/completions \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
--data "{\"model\":\"${SPICY_MODEL}\",\"messages\":[{\"role\":\"user\",\"content\":\"Say hello in one sentence.\"}]}"Odpowiedź znajduje się w choices[0].message.content. Wywołanie jest rozliczane za zużyte tokeny. Endpoint używa formatu odpowiedzi OpenAI, a nie struktury {code, msg, data}. Zobacz Tekst i strumieniowanie.
Wybór modelu z kodu
Powyższe pięć kroków pozostawia człowieka w procesie. Wdrożona integracja wybiera model sama, bezpośrednio przed wysłaniem: model dostępny w zeszłym tygodniu może być dziś wstrzymany, a ceny zależą od konta. Pobierz schemat i przykład zwalidowany przez serwer w tym samym wywołaniu, a następnie wymagaj enabled i available:
CATALOG_JSON="$(curl --fail-with-body \
"https://api.spicyapi.ai/api/v1/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1" \
-H "Authorization: Bearer $SPICY_API_KEY")"
MODEL_ID="$(printf '%s' "$CATALOG_JSON" | jq -er \
'[.data.items[] | select(.enabled == true and .available == true and (.examples | length > 0))][0].model')"
MODEL_INPUT="$(printf '%s' "$CATALOG_JSON" | jq -cer \
'[.data.items[] | select(.enabled == true and .available == true and (.examples | length > 0))][0].examples[0].input')"
printf 'Selected %s\n' "$MODEL_ID"SPICY_IDEMPOTENCY_KEY="$(uuidgen)" # new task, new key; reuse it only to resend this same request
TASK_PAYLOAD="$(jq -cn \
--arg model "$MODEL_ID" \
--argjson input "$MODEL_INPUT" \
'{model: $model, input: $input}')"
curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
--data "$TASK_PAYLOAD"Jeśli wybór niczego nie zwróci, zatrzymaj się zamiast używać zapamiętanego identyfikatora. Trzy poniższe programy wykonują ten sam wybór bez jq.
Kompletny proces
Wszystkie trzy programy poniżej odczytują katalog, tworzą zadanie, odpytują je ze stopniowym wydłużaniem odstępu i wypisują adres pobierania. W produkcji zamiast tego używaj webhooków — odpytywanie jest rozwiązaniem zapasowym, nie głównym planem.
Uruchomienie tych programów tworzy rzeczywiste, płatne zadanie
Każdy program wybiera pierwszy model text-to-image dostępny do wywołania w twoim katalogu (nie najtańszy) i wysyła jeden z jego zwalidowanych przykładów. Najpierw sprawdź cenę tego modelu albo zmień wybór na dokładnie ten model, którego chcesz użyć.
| Język | Zapisz jako | Najpierw zainstaluj | Uruchom |
|---|---|---|---|
| Node.js 20 lub nowszy | generate.mjs | nic | node generate.mjs |
| Python 3.9 lub nowszy | generate.py | pip install requests | python3 generate.py (Windows: py generate.py) |
| Go 1.21 lub nowszy | main.go w pustym folderze | go mod init example | go run . |
Wszystkie trzy odczytują klucz z SPICY_API_KEY (krok 1) i wypisują adres wyniku po zakończeniu zadania. Czekają najwyżej dziesięć minut, co pasuje do modeli obrazów; dla wideo zamiast tego czekaj do deadlineAt zadania.
const BASE = 'https://api.spicyapi.ai/api/v1';
const KEY = process.env.SPICY_API_KEY;
const headers = {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
};
async function call(path, init) {
const res = await fetch(`${BASE}${path}`, {
...init,
headers: { ...headers, ...init?.headers },
signal: AbortSignal.timeout(30_000),
});
const body = await res.json();
// Branch on `code`, not on res.ok — the business code says far more
if (body.code !== 200) {
throw new Error(`${body.code} ${body.msg} (request_id=${body.request_id})`);
}
return body.data;
}
async function generate() {
const catalog = await call(
'/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1',
);
const selected = catalog.items.find(
(item) => item.enabled && item.available && item.examples?.length,
);
if (!selected) throw new Error('No callable text-to-image model with a validated example');
const idempotencyKey = crypto.randomUUID();
const { taskId } = await call('/jobs/createTask', {
method: 'POST',
headers: { 'Idempotency-Key': idempotencyKey },
body: JSON.stringify({
model: selected.model,
input: selected.examples[0].input,
}),
});
// Start at 2s, multiply by 1.5, cap at 15s. The first check is almost
// certainly still `queued`, so a shorter start just wastes a round trip.
const deadline = Date.now() + 10 * 60_000;
let wait = 2000;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, wait));
wait = Math.min(wait * 1.5, 15000);
const task = await call(`/jobs/recordInfo?taskId=${taskId}`);
if (task.state === 'succeeded') {
const asset = task.output?.assets?.find((item) => item.url);
if (asset) return asset.url;
if (task.output?.assets?.some((item) => item.pending)) continue;
throw new Error('Result is unavailable');
}
// Anything other than queued / running is terminal. This also handles
// historical canceled records and future terminal states safely.
if (task.state !== 'queued' && task.state !== 'running') {
throw new Error(`${task.state}: ${task.errorMessage ?? 'no detail'}`);
}
}
throw new Error(`task ${taskId} timed out`);
}
generate().then(console.log);import os, time, uuid, requests
BASE = "https://api.spicyapi.ai/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['SPICY_API_KEY']}",
"Content-Type": "application/json",
}
def call(method: str, path: str, headers=None, **kwargs) -> dict:
res = requests.request(
method, f"{BASE}{path}", headers={**HEADERS, **(headers or {})}, timeout=30, **kwargs
)
body = res.json()
# Branch on `code`, not on status_code
if body["code"] != 200:
raise RuntimeError(f'{body["code"]} {body["msg"]} (request_id={body.get("request_id")})')
return body["data"]
def generate() -> str:
catalog = call(
"GET",
"/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1",
)
selected = next(
(
item
for item in catalog["items"]
if item["enabled"] and item["available"] and item.get("examples")
),
None,
)
if selected is None:
raise RuntimeError("No callable text-to-image model with a validated example")
idempotency_key = str(uuid.uuid4())
task_id = call("POST", "/jobs/createTask", headers={"Idempotency-Key": idempotency_key}, json={
"model": selected["model"],
"input": selected["examples"][0]["input"],
})["taskId"]
# Start at 2s, multiply by 1.5, cap at 15s
deadline = time.monotonic() + 600
wait = 2.0
while time.monotonic() < deadline:
time.sleep(wait)
wait = min(wait * 1.5, 15.0)
task = call("GET", "/jobs/recordInfo", params={"taskId": task_id})
if task["state"] == "succeeded":
assets = task.get("output", {}).get("assets", [])
ready = next((asset for asset in assets if asset.get("url")), None)
if ready:
return ready["url"]
if any(asset.get("pending") for asset in assets):
continue
raise RuntimeError("Result is unavailable")
# Anything other than queued / running is terminal.
if task["state"] not in {"queued", "running"}:
raise RuntimeError(f'{task["state"]}: {task.get("errorMessage", "no detail")}')
raise TimeoutError(f"task {task_id} timed out")
print(generate())package main
import (
"bytes"
"crypto/rand"
"encoding/json"
"encoding/hex"
"fmt"
"math"
"net/http"
"os"
"time"
)
const base = "https://api.spicyapi.ai/api/v1"
// envelope is the shape every endpoint returns. Data stays raw so each
// call site can decode it into whatever it actually expects.
type envelope struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data json.RawMessage `json:"data"`
RequestID string `json:"request_id"`
}
var client = &http.Client{Timeout: 30 * time.Second}
func newIdempotencyKey() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return "job-" + hex.EncodeToString(b)
}
func call(method, path string, body any, idempotencyKey string, out any) error {
var rdr *bytes.Reader
if body != nil {
raw, err := json.Marshal(body)
if err != nil {
return err
}
rdr = bytes.NewReader(raw)
} else {
rdr = bytes.NewReader(nil)
}
req, err := http.NewRequest(method, base+path, rdr)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("SPICY_API_KEY"))
req.Header.Set("Content-Type", "application/json")
if idempotencyKey != "" {
req.Header.Set("Idempotency-Key", idempotencyKey)
}
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return err
}
// Branch on Code, not on res.StatusCode
if env.Code != 200 {
return fmt.Errorf("%d %s (request_id=%s)", env.Code, env.Msg, env.RequestID)
}
return json.Unmarshal(env.Data, out)
}
func generate() (string, error) {
var catalog struct {
Items []struct {
Model string `json:"model"`
Enabled bool `json:"enabled"`
Available bool `json:"available"`
Examples []struct {
Input map[string]any `json:"input"`
} `json:"examples"`
} `json:"items"`
}
if err := call(http.MethodGet,
"/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1",
nil, "", &catalog); err != nil {
return "", err
}
var modelID string
var modelInput map[string]any
for _, item := range catalog.Items {
if item.Enabled && item.Available && len(item.Examples) > 0 {
modelID, modelInput = item.Model, item.Examples[0].Input
break
}
}
if modelID == "" {
return "", fmt.Errorf("no callable text-to-image model with a validated example")
}
var created struct {
TaskID string `json:"taskId"`
}
err := call(http.MethodPost, "/jobs/createTask", map[string]any{
"model": modelID,
"input": modelInput,
}, newIdempotencyKey(), &created)
if err != nil {
return "", err
}
// Start at 2s, multiply by 1.5, cap at 15s
wait := 2 * time.Second
deadline := time.Now().Add(10 * time.Minute)
for time.Now().Before(deadline) {
time.Sleep(wait)
wait = time.Duration(math.Min(float64(wait)*1.5, float64(15*time.Second)))
var task struct {
State string `json:"state"`
ErrorMsg string `json:"errorMessage"`
Output struct { Assets []struct { URL string `json:"url"`; Pending bool `json:"pending"` } `json:"assets"` } `json:"output"`
}
if err := call(http.MethodGet, "/jobs/recordInfo?taskId="+created.TaskID, nil, "", &task); err != nil {
return "", err
}
if task.State == "succeeded" {
pending := false
for _, asset := range task.Output.Assets {
if asset.URL != "" { return asset.URL, nil }
pending = pending || asset.Pending
}
if pending { continue }
return "", fmt.Errorf("result is unavailable")
}
// Terminal means "not one of the two in-flight states" — do not
// enumerate the terminal states, that breaks when a new one appears
if task.State != "queued" && task.State != "running" {
return "", fmt.Errorf("%s: %s", task.State, task.ErrorMsg)
}
}
return "", fmt.Errorf("task %s timed out", created.TaskID)
}
func main() {
url, err := generate()
if err != nil {
panic(err)
}
fmt.Println(url)
}Wspólna struktura odpowiedzi
Odpowiedzi JSON natywnego API zadań używają poniższej struktury; endpointy zgodności /v1 i SSE używają struktur własnych protokołów:
{ "code": 200, "msg": "success", "data": { }, "request_id": "req_…" }codejest kodem biznesowym, a nie statusem HTTP.200oznacza powodzenie. Status HTTP jest również ustawiony poprawnie, więc można opierać logikę na dowolnym z nich — ale wybierz jeden. Różnią się przy konkretnych kodach (niewystarczające saldo tocode: 40201z HTTP402), a ich mieszanie ostatecznie prowadzi do pominięcia któregoś przypadku.- Przy błędzie
datajest nieobecne — nie ma wartościnull. request_idjest obecne zarówno przy powodzeniu, jak i błędzie, oraz powtarzane w nagłówku odpowiedziX-Request-Id. Podawaj je przy zgłaszaniu problemu.
Dalej
Uwierzytelnianie
Format klucza, limity poszczególnych kluczy, postępowanie po wycieku
Katalog modeli i schematy
Katalog na żywo, ceny i renderowanie tabeli parametrów z inputSchema
Zadania asynchroniczne
Stany zadań, odbieranie wyników, wydłużanie odstępów odpytywania
Webhooki
Struktura danych, weryfikacja podpisu, ponowienia
Media
Bezpośrednie przesyłanie materiałów referencyjnych i pobieranie wyników
Idempotencja
Bezpieczne ponawianie bez podwójnego generowania i opłat
Błędy
Wszystkie kody biznesowe i ponowienia, które nie mają sensu
Przygotuj konto
Od braku konta do działającego klucza API w mniej więcej piętnaście minut. Rejestracja, weryfikacja adresu e-mail, doładowanie, utworzenie klucza i bezpłatne wywołanie testowe – z objaśnieniem każdego ekranu.
Uwierzytelnianie
Format klucza, ograniczenia poszczególnych kluczy, widoczność zadań oraz działania, które należy podjąć natychmiast po wycieku klucza.

