spicyapiDocumentazione
Sviluppare con l’API
Contenuto principale

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:

  1. Invia l'attività e ricevi subito un taskId. Non è ancora completata; il suo prezzo viene accantonato dal saldo come riserva.
  2. 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.
  3. Quando state è succeeded, il link al file si trova in data.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. Leggi settled e cost per 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/createTask

Questa 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

HeaderObbligatorioNote
AuthorizationSìBearer sk-spicy-…
Content-TypeSìapplication/json
Idempotency-KeyNoFortemente consigliato: rende sicuro un reinvio dopo un errore di rete. Consulta Idempotenza
X-Spicy-RetentionNoConserva 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: 2

Lo 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"
}
CampoNote
taskIdID dell'attività, con prefisso job_. Serve per tutti i passaggi successivi: salvalo
statequeued per una nuova attività; stato corrente dell'attività originale in un reinvio idempotente con la stessa chiave
estimatedCostImporto 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
deadlineAtUltimo 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.

HTTPcodeSignificato abitualeAzione
400400Un campo in input non appartiene allo schema del modello, un valore è fuori intervallo oppure callBackUrl non è validoConfronta con inputSchema del modello; msg indica il problema
40040004Ogni campo è valido, ma questa combinazione non può essere eseguita al momentoCambia la combinazione, per esempio risoluzione o durata
401401La chiave API manca, è errata o è disabilitataControlla l'header Authorization
40240201Il saldo non basta a riservare estimatedCostAggiungi fondi nella console, poi reinvia con lo stesso Idempotency-Key
40240202È stato raggiunto un limite di spesa della chiave, del team o dell'accountAumenta il limite nella console oppure aspetta il ripristino del limite giornaliero o mensile
40340301Questa chiave non è autorizzata a chiamare il modelloUsa un'altra chiave o modifica le restrizioni sui modelli della chiave
409409 / 40901Conflitto di idempotenza o preventivo che non corrisponde piùConsulta Idempotenza e Preventivi
413413Corpo della richiesta troppo grande, normalmente per contenuti multimediali incorporatiCarica il file e passa il suo URI spicy://; consulta Contenuti multimediali
429429Troppe richieste in poco tempoAttendi Retry-After, poi reinvia con la stessa chiave
50350301Modello temporaneamente non disponibileRiprova 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.

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"

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.

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

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

queued
running
succeededfailedexpired
Un task si trova in un solo stato alla volta. Viene addebitato solo succeeded; ogni altro stato finale restituisce per intero l'importo trattenuto.

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

StatoSignificatoTerminaleFatturazione
queuedCreata, fondi riservati, in attesa di essere presa in caricoNoRiservati, non regolati
runningIl modello sta generando; risultato in attesaNoRiservati, non regolati
succeededGenerazione riuscitaSìAddebito sull'utilizzo effettivo, limitato alla riserva
failedGenerazione fallitaSìNormalmente rilascio completo; determinati esiti content_rejected dichiarati espressamente nella pagina del modello sono addebitati al preventivo accettato. Controlla settled e cost
canceledSolo compatibilità storica; non è disponibile un'azione di annullamentoSìRilascio completo
expiredSuperato il tempo massimo di esecuzione del modelloSì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
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"}}'
EsitoRisposta
Stato terminale entro il tempo disponibile200 con il record dell'attività, nella stessa struttura restituita da recordInfo
Tempo disponibile esauritoIl consueto 202, identico a quello ottenuto omettendo wait. Da lì continua con il polling
Disconnessione del clientNon 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
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

CampoTipoNote
taskIdstringID dell'attività
sourceTaskIdstringSolo per un'attività creata tramite retry: attività da cui è stato effettuato il nuovo tentativo
modelstringIdentificatore del modello
statestringVedi la tabella sopra
inputobjectParametri convalidati; le immagini incorporate diventano URI di file della piattaforma. Omesso dopo la scadenza della conservazione dell'input
outputobjectDescrizione del risultato. Ha significato soltanto in caso di succeeded
errorCodestringPresente 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
errorMessagestringMotivo dell'errore leggibile, in inglese salvo che sia selezionata un'altra lingua. Presente soltanto in caso di errore
coststringImporto regolato, oppure riserva se non ancora regolato; l'addebito finale non può superare tale riserva
settledbooleanIndica se la fatturazione è definitiva. Quando è false, cost può ancora cambiare
createdAtstringRFC 3339
deadlineAtstringScadenza di esecuzione memorizzata all'accettazione; gli eventi webhook meno recenti possono ometterla
completedAtstringRFC 3339. Solo stati terminali
contentStatestringpresent, expired, rimosso per scadenza della conservazione, o purged, distrutto da te. Consulta Conservazione e distruzione
contentRemovedBystringuser o system; omesso finché il contenuto è presente
purgedAtstringRFC 3339. Soltanto dopo la distruzione del contenuto da parte tua
retentionobjectoutputsExpireAt, 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 taskId e 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 e settled, invece di dedurre un rimborso da un timer locale.
  • output.assets[].expiresAt fa 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.

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

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

CampoTipoNote
keystringIdentificatore di compatibilità per l'aggiornamento facoltativo del link di download. Vuoto durante pending
urlstringURL GET firmato diretto. Omesso se in attesa o non disponibile; non allegare una chiave API
expiresAtstringScadenza RFC 3339 dell'URL; interroga di nuovo per aggiornarlo entro il periodo di conservazione
mimestringTipo di contenuto, per esempio image/png, video/mp4
width · heightintegerDimensioni in pixel. Assenti per audio e testo
durationSecondsnumberDurata in secondi. Solo video e audio
bytesintegerDimensione in byte
pendingbooleanVedi sotto
unavailablebooleanVedi sotto
layerobjectSolo 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 e key e url sono temporaneamente vuoti. Interroga di nuovo dopo qualche secondo; chiamare download-url senza key in quel momento restituisce 409.
  • 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.

CallbackPolling
LatenzaAl momento del completamentoIntervallo di polling
Richieste1 per attività, effettuata da noiN per attività, effettuate da te
Consuma il tuo limite di frequenzaNoSì: consulta Limiti di frequenza
Richiede un endpoint HTTPS pubblicoSì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 deadlineAt dalla risposta di createTask, 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à.

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

In questa pagina