Attività asincrone
Contratti createTask e recordInfo, significato dei sei stati e scelta tra callback e polling.
La generazione è asincrona: invio e recupero sono due chiamate separate.
In parole semplici:
- Invia l'attività e ricevi subito un
taskId. Non è ancora completata; il suo prezzo viene accantonato dal saldo come riserva. - Torna più tardi e chiedi «questo
taskIdè terminato?»: è il polling, e basta chiedere ogni pochi secondi. Oppure fornisci un indirizzo web e ti inviamo il risultato lì quando è pronto: è una callback, cioè un webhook. - Quando
stateèsucceeded, il link al file si trova indata.output.assets[0].url. Aprilo o scaricalo entro circa 20 minuti; se è scaduto, interroga di nuovo l'attività per ottenerne uno nuovo. Un'attività riuscita non costa più della riserva; le attività fallite e scadute normalmente la liberano, con eccezioni dichiarate nella pagina del modello per alcuni rifiuti di contenuto addebitati. Leggisettledecostper l'addebito finale.
Perché non una sola chiamata? Le immagini richiedono secondi; i video normalmente minuti. Un endpoint sincrono lascerebbe il processo bloccato su una connessione e qualsiasi interruzione ti lascerebbe senza risultato né un modo sicuro per riprovare: non sapresti se il tentativo precedente è già in esecuzione. Separare le chiamate rende l'ID dell'attività il riferimento stabile: riconnettiti e continua a interrogare lo stesso ID.
1. Crea un'attività
POST /api/v1/jobs/createTaskQuesta chiamata comporta una spesa
Un'attività accettata riserva subito estimatedCost dal saldo e non può essere annullata. Per verificare prima il prezzo senza creare nulla, richiedi un preventivo. I limiti di spesa della chiave limitano quanto può spendere; consulta Fatturazione.
Header
| Header | Obbligatorio | Note |
|---|---|---|
Authorization | Sì | Bearer sk-spicy-… |
Content-Type | Sì | application/json |
Idempotency-Key | No | Fortemente consigliato: rende sicuro un reinvio dopo un errore di rete. Consulta Idempotenza |
X-Spicy-Retention | No | Conserva output e prompt di questa attività per un periodo più breve, in secondi. Consulta Conservazione e distruzione |
Corpo
Proprietà
Tipo
callBackUrl deve essere un indirizzo HTTPS pubblico raggiungibile dai nostri server su internet. Vengono rifiutati URL http:// in chiaro, una porta esplicita diversa da 443 o 80, URL con nome utente o password, indirizzi IP privati o di loopback e nomi host che non si risolvono: il corpo della callback contiene il prompt e i link firmati ai risultati. Per questo http://localhost non funziona nello sviluppo locale: usa il polling oppure esponi la macchina tramite un tunnel che fornisca un indirizzo HTTPS pubblico. I valori non validi restituiscono HTTP 400 con code: 400 applicativo, msg: "Invalid callback URL" e un request_id. L'errore non ripete URL, nome host, indirizzo IP o dettagli DNS. Controlliamo nuovamente la destinazione alla connessione; consulta Webhook.
Scrivi callBackUrl con la B maiuscola
Soltanto input viene convalidato campo per campo. Un campo di primo livello non riconosciuto, come callback_url o webhookUrl, viene attualmente ignorato: l'attività viene quindi accettata, ma non viene mai inviata una callback. Se ti aspetti una callback e non arriva, controlla prima questa grafia.
Risposta
HTTP/1.1 202 Accepted
Location: /api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3
Retry-After: 2Lo stato del trasporto è 202 Accepted. Il corpo usa comunque la struttura standard, quindi il code applicativo di successo resta 200. Location è l'indirizzo da interrogare e Retry-After: 2 suggerisce quanti secondi attendere prima della prima interrogazione.
{
"code": 200,
"msg": "success",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"state": "queued",
"estimatedCost": "0.008",
"deadlineAt": "2026-08-28T09:42:04Z"
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}| Campo | Note |
|---|---|
taskId | ID dell'attività, con prefisso job_. Serve per tutti i passaggi successivi: salvalo |
state | queued per una nuova attività; stato corrente dell'attività originale in un reinvio idempotente con la stessa chiave |
estimatedCost | Importo riservato all'accettazione, per esempio "0.008", in dollari statunitensi come stringa, e massimo addebito dell'attività. I fondi inutilizzati vengono liberati; non viene riscosso successivamente alcun importo oltre la riserva |
deadlineAt | Ultimo momento in cui il server continua a lavorare sull'attività. Se non termina entro quel momento, si conclude come expired senza addebito. Usalo per decidere quanto aspettare |
I campi monetari sono stringhe senza simbolo di valuta
estimatedCost, cost e i tre campi del saldo sono stringhe decimali semplici come "0.008": niente $ né separatori delle migliaia. parseFloat / float() funzionano direttamente su queste stringhe.
Sono stringhe anziché numeri JSON perché nella maggior parte dei linguaggi un numero JSON viene rappresentato come float64, e 0.008 non ha una rappresentazione binaria esatta. Dopo il passaggio attraverso un float non puoi più affermare quale importo esatto sia stato addebitato: usa un tipo decimale (decimal.Decimal, BigDecimal) se il numero deve entrare nella contabilità.
La valuta è sempre USD. Consulta Fatturazione.
Errori di creazione
Questi sono gli errori normalmente incontrati nella prima integrazione. Quando createTask restituisce un errore non viene riservato né addebitato nulla.
| HTTP | code | Significato abituale | Azione |
|---|---|---|---|
| 400 | 400 | Un campo in input non appartiene allo schema del modello, un valore è fuori intervallo oppure callBackUrl non è valido | Confronta con inputSchema del modello; msg indica il problema |
| 400 | 40004 | Ogni campo è valido, ma questa combinazione non può essere eseguita al momento | Cambia la combinazione, per esempio risoluzione o durata |
| 401 | 401 | La chiave API manca, è errata o è disabilitata | Controlla l'header Authorization |
| 402 | 40201 | Il saldo non basta a riservare estimatedCost | Aggiungi fondi nella console, poi reinvia con lo stesso Idempotency-Key |
| 402 | 40202 | È stato raggiunto un limite di spesa della chiave, del team o dell'account | Aumenta il limite nella console oppure aspetta il ripristino del limite giornaliero o mensile |
| 403 | 40301 | Questa chiave non è autorizzata a chiamare il modello | Usa un'altra chiave o modifica le restrizioni sui modelli della chiave |
| 409 | 409 / 40901 | Conflitto di idempotenza o preventivo che non corrisponde più | Consulta Idempotenza e Preventivi |
| 413 | 413 | Corpo della richiesta troppo grande, normalmente per contenuti multimediali incorporati | Carica il file e passa il suo URI spicy://; consulta Contenuti multimediali |
| 429 | 429 | Troppe richieste in poco tempo | Attendi Retry-After, poi reinvia con la stessa chiave |
| 503 | 50301 | Modello temporaneamente non disponibile | Riprova più tardi con la stessa chiave |
Elenco completo: Errori.
Prima di iniziare: scegli un modello e un input valido
Gli esempi seguenti richiedono un ID di modello reale e un input accettato da quel modello. Il catalogo può fornirli entrambi: includeExamples=1 aggiunge input che hanno già superato la convalida dello schema di ciascun modello. Serve jq, un piccolo strumento JSON da riga di comando; sostituisci image con video o audio per altri tipi di modello.
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}'Ogni riga stampata è un corpo di richiesta pronto con due campi, model e input. Scegline una. Se una riga mostra "input": null, quel modello non ha un esempio salvato: leggi il suo inputSchema da GET /api/v1/models/{model} e scrivi tu l'input. Consulta Catalogo dei modelli e schemi.
Esempi
MODEL_ID_FROM_CATALOG sotto è soltanto un segnaposto: sostituisci l'intero corpo della richiesta con la riga scelta dal catalogo corrente. Non inviare letteralmente il segnaposto.
# 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"Dovresti vedere il corpo 202 mostrato sopra, con code: 200 e un taskId. Conserva taskId: serve per recuperare il risultato. Per ricevere una callback, aggiungi nel JSON "callBackUrl":"https://…" con il tuo indirizzo 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"])Una risposta 202 non è sempre queued
La prima richiesta accettata crea un'attività queued. Se la stessa chiave API reinvia la stessa richiesta con lo stesso Idempotency-Key, questo 202 restituisce lo stato corrente dell'attività originale. Potrebbe già essere running o terminale, ma non viene creata una seconda attività né una seconda riserva.
2. Stati
queuedrunningsucceededfailedexpiredUn'attività accettata è irreversibile: né queued né running prevede un'azione di annullamento. L'API può ancora leggere i record storici canceled, ma nessuna nuova azione di un utente o operatore crea questo stato.
| Stato | Significato | Terminale | Fatturazione |
|---|---|---|---|
queued | Creata, fondi riservati, in attesa di essere presa in carico | No | Riservati, non regolati |
running | Il modello sta generando; risultato in attesa | No | Riservati, non regolati |
succeeded | Generazione riuscita | Sì | Addebito sull'utilizzo effettivo, limitato alla riserva |
failed | Generazione fallita | Sì | Normalmente rilascio completo; determinati esiti content_rejected dichiarati espressamente nella pagina del modello sono addebitati al preventivo accettato. Controlla settled e cost |
canceled | Solo compatibilità storica; non è disponibile un'azione di annullamento | Sì | Rilascio completo |
expired | Superato il tempo massimo di esecuzione del modello | Sì | Rilascio completo |
Leggi il regolamento, non soltanto lo stato dell'attività
Le attività fallite o scadute normalmente liberano l'intera riserva. Alcuni rifiuti di contenuto vengono addebitati al preventivo accettato soltanto quando la pagina del modello dichiara espressamente questa eccezione. Controlla settled e cost per l'addebito finale. Un rifiuto addebitato mantiene state: "failed" ed errorCode: "content_rejected". Un processo di recupero completa automaticamente qualsiasi addebito o rilascio ancora in sospeso.
Verifica lo stato terminale con state non appartenente a {queued, running}, invece di elencare i valori terminali. Questo gestisce in sicurezza i record storici canceled e impedisce che un futuro stato terminale provochi polling senza fine.
2.5 Evita il ciclo di polling con wait
Aggiungi ?wait=<seconds> a createTask e manteniamo aperta la connessione fino allo stato terminale dell'attività, così la prima chiamata può evitare del tutto un ciclo di polling.
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"}}'| Esito | Risposta |
|---|---|
| Stato terminale entro il tempo disponibile | 200 con il record dell'attività, nella stessa struttura restituita da recordInfo |
| Tempo disponibile esaurito | Il consueto 202, identico a quello ottenuto omettendo wait. Da lì continua con il polling |
| Disconnessione del client | Non cambia nulla per l'attività. Continua l'esecuzione e viene fatturata normalmente |
Uno stato terminale non significa necessariamente successo
Qui 200 significa che l'attività è terminata, non che è riuscita. Controlla data.state: failed ed expired arrivano con lo stesso stato HTTP, proprio come da recordInfo.
Il massimo è 60 secondi. I valori superiori vengono limitati anziché rifiutati; qualsiasi valore che non sia un intero positivo viene ignorato anziché rifiutato. Un refuso in un parametro di ottimizzazione non dovrebbe far fallire una generazione altrimenti valida.
Il tetto dipende dal timeout di 100 secondi del livello edge davanti all'API. Oltre quella soglia la connessione viene interrotta dalla CDN, non da noi, e riceveresti un 524 senza ID dell'attività né stato recuperabile, mentre l'attività esiste e i fondi sono già riservati. Sessanta secondi lasciano spazio alla richiesta stessa.
Usalo per le immagini, non per i video
I modelli video normalmente richiedono minuti. wait scadrà, aggiungendo la latenza della connessione mantenuta aperta, e finirai comunque a interrogare lo stato. È utile quando il modello normalmente termina in secondi, come la maggior parte degli endpoint di immagini ed editing; negli altri casi è un'attesa inutile. Per scelta progettuale non è possibile renderlo predefinito.
Per le integrazioni in produzione i webhook restano la soluzione migliore: nessuna connessione resta aperta e ricevi notizie anche delle attività più lunghe di qualsiasi periodo di attesa avresti scelto.
3. Interroga un'attività
GET /api/v1/jobs/recordInfo?taskId=job_…Un solo parametro di query obbligatorio: taskId. Un'attività può essere letta soltanto con la stessa chiave API che l'ha creata. L'interrogazione con una chiave diversa, anche un'altra chiave del tuo account o quella sostitutiva dopo la rotazione, restituisce 404, come un'attività inesistente, così le differenze negli errori non possono essere usate per enumerare ID altrui. La stessa regola vale per download-url e retry. Prima di ruotare una chiave, completa il recupero dei risultati delle attività create con quella vecchia.
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3" \
-H "Authorization: Bearer $SPICY_API_KEY"L'interrogazione è gratuita. Sostituisci l'ID dell'esempio con il tuo taskId.
Campi della risposta
| Campo | Tipo | Note |
|---|---|---|
taskId | string | ID dell'attività |
sourceTaskId | string | Solo per un'attività creata tramite retry: attività da cui è stato effettuato il nuovo tentativo |
model | string | Identificatore del modello |
state | string | Vedi la tabella sopra |
input | object | Parametri convalidati; le immagini incorporate diventano URI di file della piattaforma. Omesso dopo la scadenza della conservazione dell'input |
output | object | Descrizione del risultato. Ha significato soltanto in caso di succeeded |
errorCode | string | Presente quando state è failed o expired; un'attività scaduta ha timeout. Uno di nove valori: consulta Codici di errore delle attività. Prendi decisioni in base a questo, mai a errorMessage |
errorMessage | string | Motivo dell'errore leggibile, in inglese salvo che sia selezionata un'altra lingua. Presente soltanto in caso di errore |
cost | string | Importo regolato, oppure riserva se non ancora regolato; l'addebito finale non può superare tale riserva |
settled | boolean | Indica se la fatturazione è definitiva. Quando è false, cost può ancora cambiare |
createdAt | string | RFC 3339 |
deadlineAt | string | Scadenza di esecuzione memorizzata all'accettazione; gli eventi webhook meno recenti possono ometterla |
completedAt | string | RFC 3339. Solo stati terminali |
contentState | string | present, expired, rimosso per scadenza della conservazione, o purged, distrutto da te. Consulta Conservazione e distruzione |
contentRemovedBy | string | user o system; omesso finché il contenuto è presente |
purgedAt | string | RFC 3339. Soltanto dopo la distruzione del contenuto da parte tua |
retention | object | outputsExpireAt, quando vengono eliminati gli output; promptsExpireAt, quando viene cancellato il prompt; source, quale impostazione ha determinato la scadenza |
Quattro scadenze distinte
- Il tuo tempo di attesa lo scegli tu. Interrompere l'attesa non annulla l'attività remota: conserva
taskIde interroga la stessa attività più tardi per riprendere. deadlineAtè la scadenza di esecuzione del server. Un reinvio idempotente non la estende; un retry esplicito crea una nuova attività con una propria scadenza. Leggi lo stato terminale esettled, invece di dedurre un rimborso da un timer locale.output.assets[].expiresAtfa scadere un link temporaneo al risultato, normalmente dopo 20 minuti. Una nuova interrogazione aggiorna il link senza creare un'altra attività.- I file generati vengono conservati al massimo per circa 14 giorni dal completamento; gli input caricati sulla piattaforma per un giorno al massimo. Sono limiti della piattaforma: puoi impostare periodi più brevi in Conservazione e distruzione o distruggere su richiesta il contenuto di un'attività. Aggiornare un link non estende la conservazione dei file sottostanti.
Se usi l'SDK JavaScript, client.run({ model, input }, { idempotencyKey, onAccepted, timeoutMs }) invia una volta e interroga con attese crescenti da 2 a un massimo di 10 secondi; timeoutMs è soltanto il tempo di attesa locale, quindi salva l'ID da onAccepted. Consulta SDK. Preferisci i webhook per le attività in background. Dopo aver verificato una callback completa del risultato, usa direttamente il risultato senza un'altra interrogazione dell'attività.
Usa direttamente l'URL del risultato
I file pronti includono url ed expiresAt insieme a key e ai metadati multimediali. Recupera direttamente output.assets[].url senza credenziali API, per esempio con curl -L -o result.png "<url>". Interroga di nuovo quando l'URL scade; /common/download-url resta facoltativo per le integrazioni esistenti. Gli URL normalmente durano 20 minuti e non possono estendere i 14 giorni di conservazione del risultato.
{
"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"
}Un'interrogazione riuscita non equivale a un'attività riuscita
Entrambe le risposte contengono code: 200: l'interrogazione è riuscita. L'esito dell'attività si trova in data.state. Il codice applicativo descrive la consegna; state descrive l'attività. Non confonderli.
Il corpo di un webhook usa questa stessa struttura: la stessa risposta con code: 200, gli stessi campi data. Il codice che interpreta questa sezione interpreta una callback senza modifiche: due canali, un solo parser.
Struttura dell'output
output ha due parti indipendenti. I file generati, immagini, video e audio, arrivano in output.assets[]. I risultati testuali arrivano in output.text, come stringa semplice.
La maggior parte dei modelli compila una sola parte, ma un modello può compilarle entrambe. Quella ricevuta dipende dall'endpoint, non dalla sua modality: un endpoint speech-to-text è raggruppato sotto audio nel catalogo perché legge audio, ma risponde con testo.
`output.assets` non è sempre presente
Un endpoint con risultato testuale non ha affatto la chiave assets, anziché avere un array vuoto. Un codice che apre il risultato con task.output.assets[0].url genera un'eccezione per quelle attività, invece di restituire qualcosa da controllare. Leggi prima output.text quando presente e considera assets facoltativo:
const { text, assets = [] } = task.output ?? {};
if (text) console.log(text);
for (const asset of assets) console.log(asset.url);Per un risultato testuale non c'è nulla da scaricare né un link che scade, ma il testo fa comunque parte dell'output: viene rimosso secondo lo stesso programma di qualsiasi altro risultato e contentState passa a expired. Copia tutto ciò che vuoi conservare. Un risultato composto da più parti, per esempio i segmenti di una trascrizione, viene unito in una sola stringa con interruzioni di riga tra le parti.
Il resto della sezione descrive assets[].
| Campo | Tipo | Note |
|---|---|---|
key | string | Identificatore di compatibilità per l'aggiornamento facoltativo del link di download. Vuoto durante pending |
url | string | URL GET firmato diretto. Omesso se in attesa o non disponibile; non allegare una chiave API |
expiresAt | string | Scadenza RFC 3339 dell'URL; interroga di nuovo per aggiornarlo entro il periodo di conservazione |
mime | string | Tipo di contenuto, per esempio image/png, video/mp4 |
width · height | integer | Dimensioni in pixel. Assenti per audio e testo |
durationSeconds | number | Durata in secondi. Solo video e audio |
bytes | integer | Dimensione in byte |
pending | boolean | Vedi sotto |
unavailable | boolean | Vedi sotto |
layer | object | Solo nei risultati di scomposizione in livelli: zIndex, intero; boundingBox, [x1, y1, x2, y2] facoltativo; name, facoltativo. Vedi sotto |
I campi di cui non disponiamo vengono omessi, non impostati a null o 0. Gestiscili come facoltativi.
Due indicatori richiedono una gestione:
pending: true: il file è ancora in fase di copia nella nostra archiviazione ekeyeurlsono temporaneamente vuoti. Interroga di nuovo dopo qualche secondo; chiamaredownload-urlsenzakeyin quel momento restituisce409.unavailable: true: il file è scaduto o definitivamente indisponibile dopo un errore di trasferimento. È perso definitivamente; aspettare non aiuta, quindi rigenera. Se non è stato possibile archiviare nessun file di un'attività riuscita, quella attività non viene addebitata.
I risultati di scomposizione in livelli aggiungono il campo layer. Il primo file è l'immagine di base, cioè la sorgente senza gli elementi separati, con zIndex 0; tutti gli altri file sono livelli PNG trasparenti. Sovrapponili in ordine crescente di zIndex per ricostruire l'immagine. boundingBox contiene [x1, y1, x2, y2] in pixel dell'immagine di base: ridimensiona il livello per riempire quel riquadro e tornerà al suo posto. I modelli che non riportano le posizioni lo omettono, e i livelli restano utilizzabili come elementi ritagliati indipendenti. name è l'etichetta assegnata dal modello e compare soltanto quando la fornisce. La fatturazione conta ogni immagine restituita, inclusa la base.
4. Prima le callback, polling come alternativa
Il polling consiste nell'interrogare ripetutamente recordInfo finché l'attività non termina. Una callback significa che inviamo una volta l'attività conclusa al tuo server. Entrambi forniscono gli stessi dati.
| Callback | Polling | |
|---|---|---|
| Latenza | Al momento del completamento | Intervallo di polling |
| Richieste | 1 per attività, effettuata da noi | N per attività, effettuate da te |
| Consuma il tuo limite di frequenza | No | Sì: consulta Limiti di frequenza |
| Richiede un endpoint HTTPS pubblico | Sì | No |
Usa le callback in produzione. Un video di tre minuti interrogato una volta al secondo genera 180 richieste con pochissime informazioni nuove. L'attuale protezione dell'API pubblica da 1.000 richieste ogni 10 secondi non limita il normale utilizzo a pagamento, ma un polling serrato crea traffico inutile e un ciclo fuori controllo esaurisce la propria disponibilità di lettura finché recordInfo non comincia a restituire 429.
Il polling ha senso in due situazioni: sviluppo locale e servizi senza un ingresso pubblico.
Aumenta gradualmente l'attesa durante il polling
recordInfo usa il contatore di lettura e createTask quello di scrittura, quindi il polling non può sottrarre capacità agli invii. Se però interroghi troppo spesso, recordInfo comincia a restituire 429: perdi visibilità sulle attività finché il contatore non recupera disponibilità.
Un'impostazione che funziona:
- Parti da 2–3 secondi. Un intervallo più breve è inutile; anche i modelli di immagini più veloci richiedono alcuni secondi.
- Moltiplica per 1,5, con un massimo di 10–15 secondi.
- Fermati alla scadenza dell'attività, non dopo un numero fisso di minuti. Alcuni modelli video possono impiegare fino a un'ora, quindi un timeout fisso di 10 minuti abbandona attività che stanno funzionando normalmente. Usa
deadlineAtdalla risposta dicreateTask, aggiungendo qualche minuto affinché il server registri l'esito.
Smettere di aspettare non significa fallimento
Quando il tuo periodo di attesa finisce, hai soltanto smesso di aspettare. L'attività continua e può ancora riuscire ed essere addebitata. Non reinviarla con un nuovo Idempotency-Key: creeresti e pagheresti una seconda attività. Interroga più tardi lo stesso taskId oppure ritrovalo nella cronologia delle attività.
// 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. Controlla il saldo
GET /api/v1/chat/credit restituisce available, ciò che può finanziare nuove attività, held, bloccato dalle attività in corso, e total. available può essere negativo quando utilizzi credito approvato, e i fondi promozionali elencati in funding.grants possono finanziare soltanto alcuni modelli. Dettagli completi: Controllare il saldo.
curl https://api.spicyapi.ai/api/v1/chat/credit \
-H "Authorization: Bearer $SPICY_API_KEY"Cronologia delle attività e recupero
GET /api/v1/jobs elenca le attività visibili create dalla chiave API corrente. Usalo per ritrovare il lavoro dopo un riavvio dell'applicazione o una callback persa. Sono esclusi i record nascosti e le attività di altre chiavi. Le risposte contengono ID, modello, stato, costo, stato del regolamento, date e ore e, quando registrato, il requestId della chiamata che ha creato l'attività; per una chiamata di testo coincide con l'header X-Request-Id. Non includono prompt, input, output o link ai file. Recupera un'attività selezionata con recordInfo per ottenere il risultato.
Filtra per state, un model esatto del catalogo o un alias, e date UTC from / to (YYYY-MM-DD, estremo iniziale incluso e finale escluso). L'intervallo predefinito è di sette giorni fino a domani UTC; il massimo è 92 giorni. limit è 20 per impostazione predefinita e non può superare 100.
Ometti i filtri che non usi: un valore vuoto come model=, un parametro ripetuto o sconosciuto restituisce 400. Un'interrogazione che dura più di cinque secondi restituisce 503; riprova con un intervallo di date più breve.
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"Leggi data.items. Se data.hasMore è true, passa data.nextCursor come cursor della richiesta successiva, lasciando invariati tutti i filtri e le date esplicite. L'ordine è decrescente per createdAt, poi per ID dell'attività. Le pagine riflettono lo stato corrente delle attività, non una fotografia immutabile. Non costruire autonomamente i cursori. Elencare la cronologia non crea né ritenta mai un'attività.
La stessa funzione è disponibile tramite client.listTasks(...), spicyapi tasks list e lo strumento MCP di sola lettura spicyapi_tasks_list. Mantieni cost in USD come stringa decimale; quando settled è false, è ancora la stima riservata, non un addebito finale.
Se la risposta di creazione è andata persa, riprova prima la richiesta invariata con il suo Idempotency-Key originale. Una pagina della cronologia vuota non autorizza a creare di nuovo lo stesso lavoro a pagamento con una nuova chiave.

