Guida rapida
Ottieni una chiave, crea un'attività e recupera il risultato. Il percorso completo, dall'inizio alla fine.
SpicyAPI riunisce la generazione di immagini e video in un unico protocollo di autenticazione, attività e risultati, e offre modelli di testo nei formati OpenAI, Anthropic e Gemini che già utilizzi. Mantieni lo stesso flusso di chiamate quando cambi modello e costruisci input dal suo inputSchema, cioè l'elenco dei campi accettati dal modello, dei loro tipi e dei valori consentiti.
Questa guida usa l'API nativa delle attività su https://api.spicyapi.ai/api/v1. Gli endpoint di compatibilità per testo e video usano https://api.spicyapi.ai/v1. Entrambi accettano la stessa chiave API, ma i formati di richiesta e risposta sono diversi.
Segui questa pagina per scegliere un modello e recuperare un risultato. Per vedere il prezzo esatto prima di pagare, consulta Preventivi e compatibilità. Per la chat, consulta Testo e streaming. Prima di distribuire l'integrazione, segui Integrazione in produzione; usa Risoluzione dei problemi quando un passaggio fallisce. Le parole meno familiari sono spiegate nel glossario.
Vuoi vedere un risultato prima di scrivere codice?
Apri un modello su spicyapi.ai/explore e usa la scheda Playground; la scheda API mostra la stessa richiesta come comando da copiare. Per lavorare da terminale senza scrivere codice, usa la CLI, oppure lascia che un assistente AI lo faccia tramite il server MCP.
Strumenti ufficiali
Le chiamate dirette all'API HTTP sono pienamente supportate: è il metodo mostrato nel resto della pagina. Se preferisci non scrivere tu le chiamate HTTP, ci sono quattro pacchetti npm con licenza MIT, ciascuno installabile separatamente e con una propria guida:
@spicyapi/sdk
SDK TypeScript: tipi OpenAPI, attesa adattiva, caricamenti e verifica dei webhook
@spicyapi/cli
Il comando spicyapi: esplora il catalogo, richiedi preventivi, crea e segui attività da terminale
@spicyapi/mcp
Server MCP che fornisce catalogo, preventivi e attività a un agente di sviluppo, con conferma a ogni passaggio a pagamento
@spicyapi/skill
Agent Skill portabile: flusso di lavoro e limiti di spesa, installabile nel tuo agente
Altri quattro SDK ufficiali coprono i linguaggi esterni a Node.js. Tutti implementano lo stesso flusso e hanno licenza MIT; attualmente soltanto il modulo Go si installa dal repository con il comando consueto, mentre le altre tre pagine mostrano l'installazione da usare fino alla pubblicazione dei pacchetti:
SDK Python
spicyapi per Python 3.11+, senza dipendenze a runtime
SDK Go
github.com/spicyapi-ai/spicy-go per Go 1.23+, soltanto libreria standard
SDK PHP
spicyapi/spicyapi per PHP 8.1+, senza dipendenze da altre librerie
SDK Java
ai.spicyapi:spicyapi-java per Java 17+, con una tassonomia chiusa degli errori
Lavori invece in ComfyUI? I nodi ComfyUI portano ogni modello di immagini, video e audio sul tuo canvas come nodo, mostrando il prezzo prima dell'esecuzione.
Prima di iniziare
Ti servono quattro cose. Saltarne una causa più avanti un errore che non specifica quale requisito manchi.
- Un account con email verificata. Registrati su spicyapi.ai e apri il link nell'email di verifica. Fino a quel momento, ogni chiamata effettuata con la chiave restituisce
403. - Un saldo. Le generazioni vengono pagate da un saldo in dollari statunitensi, senza crediti, e un nuovo account parte da zero. Aggiungi fondi in Console → Fatturazione. Senza un saldo sufficiente, la creazione di un'attività fallisce con il codice applicativo
40201. Configurazione dell'account spiega registrazione, ricarica e creazione di una chiave. - Un terminale. Su macOS, l'app Terminale; su Linux, qualsiasi shell; su Windows, PowerShell. Quando i comandi differiscono, ogni passaggio ha una scheda Windows PowerShell. Se preferisci, i comandi macOS / Linux funzionano senza modifiche anche in WSL o Git Bash su Windows.
- Qualche centesimo. Il passaggio 3 crea una vera attività di generazione di immagini, addebitata quando riesce. Il passaggio 2 mostra il prezzo di ogni modello e un comando facoltativo indica prima l'importo esatto, gratuitamente.
Valutare prima: modelli e prezzi senza account
Non ti servono né una chiave né un account per vedere cosa è disponibile e quanto costa. Il catalogo pubblico usato dal sito è leggibile senza autenticazione e questo comando non richiede alcuna sostituzione:
curl "https://api.spicyapi.ai/console/v1/catalog/models?locale=en"La struttura della risposta è quella descritta in fondo alla pagina. In data.items, ogni voce contiene:
model: il valore esatto che in seguito invierai comemodel;slugcontiene lo stesso valore;callable: indica se il modello può essere eseguito adesso. Scegli le voci con valoretrue:statusindica lo stato operativo previsto, ma un modellolivepuò comunque essere temporaneamente non eseguibile;modalityetasks: il tipo di output che produce;price: il livello più economico, conamountin USD perunit(per_image,per_second,per_requestoper_1k_tokens) etiers, il numero di livelli disponibili. Per i modelli di testo, leggiprice.perMillionTokens, che fornisce separatamente i prezzi di input, input in cache e output per milione di token;amountda solo è soltanto la tariffa di output.
Aggiungi un ID di modello per ottenere il record completo di quel modello, inclusi inputSchema, paramNotes ed examples:
curl "https://api.spicyapi.ai/console/v1/catalog/models/MODEL_ID_FROM_CATALOG?locale=en"MODEL_ID_FROM_CATALOG è un segnaposto usato nella documentazione, non un modello reale: copia sempre un ID effettivo dal catalogo corrente.
locale usa en per impostazione predefinita; passa ja, ko, de, fr, es, pt-BR, ru, it o pl per nomi, riepiloghi e note sui parametri tradotti. Qualsiasi altro valore restituisce 400. strict=1 esclude le voci il cui testo non è pubblicato in quella lingua, invece di usare l'inglese come ripiego; normalmente non serve. Questa interfaccia ha un limite di frequenza basato sull'indirizzo di origine, non sull'account, e dispone di un proprio contatore: consulta Limiti di frequenza. È anche memorizzata in cache, quindi una variazione di prezzo può richiedere tempo prima di comparire qui.
Quando tiers è maggiore di 1, il price indicato è il livello più economico, non il prezzo delle tue impostazioni. price.variant mostra quali impostazioni usa quel livello, per esempio 720p oppure generate_audio=false;resolution=480p quando si combinano più campi. Il campo inputSchema["x-pricing"].variantFields del record di un singolo modello, quando presente, elenca tutti i campi che selezionano un livello. pricingNote è un promemoria generale che fa riferimento alla stima corrente come fonte autorevole; non elenca campi. Per l'importo esatto di una richiesta, usa POST /api/v1/jobs/quote dopo aver ottenuto una chiave.
Catalogo pubblico per valutare, catalogo autenticato per chiamare
Il catalogo pubblico risponde a «che cosa è in vendita e qual è il prezzo di listino». Non può rispondere a «che cosa può chiamare la mia chiave» o «quanto verrà addebitato al mio account», perché entrambe le risposte dipendono dall'account. Quando hai una chiave, passa a GET /api/v1/models nel passaggio 2 e consideralo l'unica fonte degli ID eseguibili: un ID ricordato, una fotografia del catalogo al lancio o una pagina promozionale non sono un'alternativa.
Cinque passaggi
Ottieni una chiave
Una chiave API è la password che il codice invia con ogni richiesta. Identifica il tuo account e determina a chi viene addebitato l'utilizzo.
- Apri Console → Chiavi API e fai clic su Crea chiave.
- Assegna un nome, per esempio
my-first-test. Lascia invariate tutte le impostazioni in Avanzate. - Copia la chiave che inizia con
sk-spicy-, oppure fai clic su Salva come file. Viene mostrata una sola volta; se la perdi devi crearne una nuova. - Nel terminale che userai per i passaggi successivi, salvala in una variabile d'ambiente, cioè un valore con nome che il terminale passa a ogni comando eseguito, così non dovrai digitare la chiave in un comando:
export SPICY_API_KEY="sk-spicy-paste-your-key-here"
# Check: this should print sk-spicy-
echo "${SPICY_API_KEY:0:9}"Che cosa dovresti vedere: sk-spicy-. Il valore dura fino alla chiusura di quella finestra del terminale, quindi esegui tutti i passaggi successivi nella stessa finestra.
Se non c'è il pulsante Crea chiave, il tuo ruolo nel team corrente non permette di creare chiavi: chiedi al proprietario del team oppure usa Crea il tuo team nella stessa pagina. Le nuove chiavi non hanno un limite di spesa, salvo che tu ne imposti uno; un limite giornaliero nella console mette un tetto ai danni di una configurazione errata o di una chiave compromessa. Consulta Autenticazione.
Scegli un modello eseguibile
Il catalogo autenticato è l'unica fonte di ID eseguibili, schemi correnti ed esempi. Leggilo con due chiamate: l'elenco è leggero e indica che cosa puoi chiamare; poi un singolo record fornisce lo schema del modello scelto. Leggere il catalogo è gratuito.
curl "https://api.spicyapi.ai/api/v1/models?modality=image&task=text-to-image" \
-H "Authorization: Bearer $SPICY_API_KEY"La risposta è lunga. La parte che serve è simile a questa, abbreviata:
{
"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"
}Scegli una voce con enabled e available entrambi true, controlla il prezzo, copia il valore model e conservalo per il resto della pagina. Se hai installato jq, curl … | jq -r '.data.items[] | select(.enabled and .available) | .model' stampa soltanto gli ID che puoi chiamare.
# 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"Le barre nell'ID passano nel percorso senza modifiche; codificale in percentuale soltanto se il client HTTP esige che un parametro di percorso sia un singolo segmento. inputSchema del record è la fonte autorevole per input: required elenca i campi obbligatori e properties descrive ciascuno. examples contiene input già convalidati dal server. Catalogo dei modelli e schemi spiega come leggere il resto del record, incluso pricing.
Richiedi gli schemi soltanto dove servono. Aggiungere includeSchema=1&includeExamples=1 alla chiamata dell'elenco restituisce in una sola risposta il JSON Schema completo di ogni modello abilitato, con un corpo molto più grande. Le letture del catalogo usano la disponibilità di lettura dell'account, indipendente dalla creazione delle attività. Non rileggere nemmeno il catalogo prima di ogni attività; consulta Memorizzare il catalogo in cache.
Se non c'è alcuna voce corrispondente, fermati prima di creare un'attività a pagamento. Una fotografia del catalogo al lancio, una pagina promozionale o un ID ricordato non sostituiscono un catalogo corrente vuoto. Per selezionare il modello dal codice invece che manualmente, consulta Selezionare il modello dal codice.
Facoltativo: verifica prima il prezzo esatto. Un preventivo è gratuito, non crea nulla e non riserva fondi. data.estimatedCost è il costo massimo di questa esatta richiesta, in 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\"}}"Crea un'attività
Un'attività è una generazione eseguita in background: ottieni subito il suo ID e il risultato più tardi. Questo passaggio è a pagamento.
Ogni nuova attività riceve una nuova chiave di idempotenza: un ID casuale che alleghi affinché l'invio della stessa richiesta due volte, per esempio dopo un errore di rete, non crei mai due attività. Generala con un comando separato e poi invia l'attività:
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\"}}"Se 3b supera il tempo massimo o la connessione si interrompe, esegui di nuovo 3b senza eseguire 3a: la stessa chiave restituisce l'attività già avviata invece di farti pagare due volte. Per creare un'immagine diversa, esegui prima 3a. Una risposta 409 significa che hai riutilizzato una chiave con un prompt o modello diverso. Maggiori dettagli in Idempotenza.
Questa richiesta invia un solo campo. Se lo schema letto nel passaggio 2 indica altri campi obbligatori, aggiungili: i valori ammessi in input sono definiti dallo schema di ogni modello e un solo campo sconosciuto provoca un 400.
La connessione non resta mai aperta. Ricevi subito un ID di attività:
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"
}Su macOS / Linux, copia taskId dalla risposta e conservalo; PowerShell lo ha già fatto per te:
export SPICY_TASK_ID="paste-the-taskId-here"202 significa accettata, non completata
Un'attività appena creata ha state: "queued"; un reinvio sicuro con lo stesso Idempotency-Key restituisce l'attività originale nel suo stato corrente. HTTP 202 significa che la richiesta è stata accettata, non che la generazione è completata. Alla prima accettazione, i fondi vengono riservati, non addebitati: estimatedCost viene accantonato dal saldo. La riserva è anche il tetto all'addebito dell'attività: i fondi inutilizzati vengono liberati e non viene riscosso successivamente alcun importo superiore. Le attività fallite normalmente liberano la riserva; determinati rifiuti di contenuto dichiarati nella pagina del modello vengono addebitati al preventivo accettato. Controlla settled e cost. deadlineAt è l'ultimo momento in cui l'attività può terminare prima di concludersi come expired. Il code applicativo di successo nella risposta resta 200. Un'attività accettata non può essere annullata.
Recupera il risultato
Se la richiesta in produzione include callBackUrl, ti inviamo una callback quando l'attività raggiunge uno stato terminale: una callback, o webhook, è un indirizzo HTTPS sul tuo server che chiamiamo al termine dell'attività, così non devi continuare a chiedere lo stato. La richiesta minima sopra lo omette per poter funzionare senza un endpoint pubblico. La firma delle callback deve essere verificata: consulta Webhook.
Senza un endpoint di callback, interroga l'attività:
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${SPICY_TASK_ID}" \
-H "Authorization: Bearer $SPICY_API_KEY"Usa la stessa chiave API che ha creato l'attività: un'altra chiave riceve 404. Guarda data.state:
data.state | Significato | Azione |
|---|---|---|
queued o running | La generazione è in corso. I fondi sono riservati, non spesi | Aspetta qualche secondo ed esegui di nuovo lo stesso comando. Le immagini normalmente richiedono secondi; i video possono richiedere minuti. deadlineAt è il limite superiore |
succeeded | Completata | Passa al punto successivo |
failed o expired | La generazione non è riuscita. Normalmente la riserva viene liberata; alcuni rifiuti di contenuto dichiarati espressamente nella pagina del modello vengono addebitati. Controlla settled e cost | Leggi data.errorCode, stabile, e data.errorMessage, leggibile da una persona, correggi l'input e ricomincia da 3a |
Il code della risposta è 200 anche se l'attività è fallita: code indica se la lettura dell'attività è riuscita. L'esito dell'attività si trova sempre in data.state. Ecco le risposte delle varie fasi, abbreviate:
{
"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"
}Finché settled è false, cost è l'importo riservato, non l'addebito finale.
Scarica il file generato
Prendi data.output.assets[0].url dal risultato riuscito e scaricalo direttamente. Non allegare la chiave API. Il link funziona per circa 20 minuti, indicati da expiresAt; se è scaduto, esegui di nuovo il passaggio 4, perché ogni lettura restituisce un nuovo link. I file generati vengono eliminati entro un massimo di 14 giorni, quindi conserva una tua copia di tutto ciò che ti serve.
# 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/mp4Se hai installato jq, puoi evitare di copiare e incollare:
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')"Che cosa dovresti vedere: un file result.png nella cartella corrente del terminale. Controlla l'addebito in Console → Log, oppure in cost quando settled è true.
Modelli di testo: una richiesta, risposta immediata
I modelli di testo rispondono nella stessa risposta HTTP: niente polling né download. L'header di risposta X-Spicy-Task-Id identifica l'attività che contiene l'addebito, se vuoi controllarlo in seguito. Nel passaggio 2, elenca invece i modelli con modality=text, esportane uno come SPICY_MODEL, poi esegui:
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.\"}]}"La risposta si trova in choices[0].message.content. Questa chiamata è addebitata in base ai token utilizzati. L'endpoint usa il formato di risposta OpenAI, non la struttura {code, msg, data}. Consulta Testo e streaming.
Selezionare il modello dal codice
I cinque passaggi sopra prevedono l'intervento di una persona. Un'integrazione distribuita sceglie da sola il modello subito prima dell'invio: un modello eseguibile la scorsa settimana potrebbe essere sospeso oggi e i prezzi dipendono dall'account. Richiedi lo schema e un esempio convalidato dal server nella stessa chiamata, poi verifica enabled e 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"Se la selezione non restituisce nulla, fermati invece di usare un ID ricordato come ripiego. I tre programmi seguenti eseguono la stessa selezione senza jq.
Il flusso completo
I tre programmi seguenti leggono il catalogo, creano un'attività, interrogano lo stato con attese crescenti e stampano un URL di download. In produzione usa invece i webhook: il polling è un'alternativa di ripiego, non il percorso principale.
Eseguire questi programmi crea una vera attività a pagamento
Ogni programma sceglie il primo modello text-to-image eseguibile nel tuo catalogo, non il più economico, e invia uno dei suoi esempi convalidati. Controlla prima il prezzo del modello oppure modifica la selezione per usare esattamente quello che vuoi.
| Linguaggio | Salva come | Prima installa | Esegui |
|---|---|---|---|
| Node.js 20 o successivo | generate.mjs | Nulla | node generate.mjs |
| Python 3.9 o successivo | generate.py | pip install requests | python3 generate.py (Windows: py generate.py) |
| Go 1.21 o successivo | main.go in una cartella vuota | go mod init example | go run . |
Tutti e tre leggono la chiave da SPICY_API_KEY, impostata nel passaggio 1, e stampano l'URL del risultato quando l'attività termina. Attendono fino a dieci minuti, una durata adatta ai modelli di immagini; per i video attendi invece fino a deadlineAt dell'attività.
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)
}Struttura della risposta
Le risposte JSON dell'API nativa delle attività usano questa struttura; la compatibilità /v1 e SSE usano le forme previste dai rispettivi protocolli:
{ "code": 200, "msg": "success", "data": { }, "request_id": "req_…" }codeè un codice applicativo, non uno stato HTTP.200significa successo. Anche lo stato HTTP è impostato correttamente, quindi puoi prendere decisioni in base all'uno o all'altro, ma scegline uno. I due differiscono nei codici specifici: un saldo insufficiente ècode: 40201con HTTP402. Mescolarli significa prima o poi non gestire un caso.- In caso di errore,
dataè assente, nonnull. request_idè presente sia in caso di successo sia di errore, ed è ripetuto nell'header di rispostaX-Request-Id. Riportalo quando segnali un problema.
Passi successivi
Autenticazione
Formato delle chiavi, limiti per chiave e azioni dopo una compromissione
Catalogo dei modelli e schemi
Catalogo corrente, prezzi e tabella dei parametri ricavata da inputSchema
Attività asincrone
Macchina a stati, recupero dei risultati e polling con attese crescenti
Webhook
Struttura del payload, verifica delle firme e tentativi di consegna
Contenuti multimediali
Caricamento diretto dei riferimenti e download dei file generati
Idempotenza
Reinvii sicuri senza doppie generazioni né doppi addebiti
Errori
Tutti i codici applicativi e i tentativi che non servono
Configura il tuo account
Da nessun account a una chiave API funzionante in circa quindici minuti. Registrazione, verifica dell’email, ricarica del saldo, creazione di una chiave e una chiamata di prova gratuita, con ogni schermata spiegata.
Autenticazione
Formato delle chiavi, restrizioni, attività visibili per ciascuna chiave e cosa fare immediatamente in caso di esposizione.

