spicyapiDocumentazione
SDK
Contenuto principale

SDK TypeScript

Una guida passo passo all’SDK TypeScript @spicyapi/sdk — installalo, ottieni il tuo primo risultato e consulta ogni metodo, opzione ed errore spiegati con parole semplici. Gli SDK per Python, Go, PHP e Java hanno una pagina ciascuno.

@spicyapi/sdk è la libreria ufficiale di SpicyAPI per TypeScript e JavaScript. Questa pagina dà per scontato che tu non l’abbia mai usata. Ti accompagna da una cartella vuota fino a un’immagine salvata, poi descrive ogni funzione del pacchetto: a che cosa serve, che cosa le passi, che cosa restituisce, se costa denaro e che cosa può andare storto.

Che cos’è l’SDK

Un SDK è una libreria di codice già pronta che aggiungi al tuo programma. Senza, il tuo programma deve costruire a mano ogni richiesta HTTP: allegare la chiave API, trasformare la risposta da JSON in dati, chiedere più e più volte se un’attività è finita, riprovare quando cade la rete, caricare i file in tre passaggi separati e controllare la firma di ogni webhook. L’SDK fa tutto questo al posto tuo, così un’intera generazione sta in una sola chiamata:

const task = await client.run({ model: modelId, input: { prompt: "A paper lantern at night" } });

Fa al caso tuo se scrivi, o stai per scrivere, un programma in Node.js (JavaScript o TypeScript) che gira su un server, in uno script sul tuo computer o in un job in background. Chi usa TypeScript ottiene anche il completamento automatico e il controllo dei tipi su ogni campo.

È lo strumento giusto?

Vuoi…Usa
Chiamare SpicyAPI dal tuo codice Node.js o TypeScriptQuesto SDK
Generare contenuti da un terminale, senza scrivere codiceLa CLI
Far chiamare SpicyAPI al posto tuo da un assistente IA come Claude Code o CursorIl server MCP e l’Agent Skill
Usare un modello di chat o di testoIl client ufficiale di OpenAI puntato su SpicyAPI: vedi Testo e streaming
Usare Python, Go, PHP o JavaL’SDK Python, l’SDK Go, l’SDK PHP o l’SDK Java ufficiali
Usare qualsiasi altro linguaggioSemplici chiamate HTTP, oppure un client generato a partire da OpenAPI ed elenco degli endpoint

Che cosa non fa

  • Non gira in un browser né in un’app mobile. Ha bisogno di Node.js, e la tua chiave API non deve mai uscire dal tuo server: chiunque può leggere una chiave da una pagina web o da un’app. Fai in modo che il tuo sito chiami il tuo backend, e che sia il backend a chiamare SpicyAPI.
  • Non gestisce i modelli di chat. La generazione di testo passa per endpoint compatibili con OpenAI, e il pacchetto ufficiale openai li gestisce già bene. Vedi Testo e streaming.
  • Non sceglie il modello e non compila i campi al posto tuo. Ogni modello accetta i propri campi. L’SDK ti dà l’elenco dei modelli e lo schema di ciascuno (l’elenco dei campi che accetta); che cosa inviare lo decidi tu.
  • Non può annullare un’attività. Una volta che SpicyAPI accetta un’attività, questa arriva fino in fondo. Fermare il tuo programma interrompe soltanto la tua attesa.
  • Non gestisce il tuo account. Ricariche, creazione delle chiavi, limiti di spesa e chiavi di firma dei webhook si impostano nella console.

La tua chiave API resta sul tuo server

Non mettere mai SPICY_API_KEY nel JavaScript di un browser, in un’app mobile, in un repository pubblico, in uno screenshot o in un messaggio di chat. Una chiave trapelata può spendere il tuo saldo finché non la disattivi. L’SDK legge la chiave da una variabile d’ambiente, così non deve mai comparire nel tuo codice.

Le parole usate in questa pagina

ParolaChe cosa significa qui
ID del modelloIl nome esatto di un modello, nella forma publisher/model/task. Copialo sempre dal catalogo in tempo reale (listModels(), cioè GET /api/v1/models), mai a memoria
InputLe impostazioni che invii con un’attività, come prompt o image_url. Ogni modello ha il proprio elenco di campi ammessi, chiamato schema
AttivitàUn singolo lavoro di generazione. La crei, SpicyAPI ci lavora in background e tu ne ritiri il risultato. Ogni attività ha un ID che inizia con job_
AccantonamentoQuando un’attività viene accettata, il suo prezzo massimo viene messo da parte dal saldo. Le attività fallite o scadute normalmente liberano l’intero importo accantonato. Alcuni rifiuti di contenuto vengono addebitati al preventivo accettato, solo se la pagina del modello indica esplicitamente questa eccezione. Controlla settled e cost per conoscere l’importo finale.
Chiave di idempotenzaUn’etichetta che inventi tu per un singolo lavoro, come order-8814-image-1. Se invii due volte la stessa richiesta con la stessa etichetta, SpicyAPI restituisce la prima attività invece di crearne, e addebitarne, una seconda
WebhookUn tuo indirizzo web che SpicyAPI chiama quando un’attività finisce, così non devi continuare a chiedere

Che cosa puoi fare

Ogni funzione qui sotto è un metodo del client (client.listModels()), salvo indicazione contraria. «Costa denaro» significa che la chiamata può portare a un addebito sul tuo saldo.

Che cosa vuoi fareMetodoCosta denaro?
Verificare che SpicyAPI sia raggiungibile (senza chiave)getStatusNo
Vedere l’elenco dei modelli in tempo reale, i tuoi prezzi e i campi di ogni modellolistModels, getModelNo
Conoscere il prezzo esatto di una richiesta prima di spendere qualsiasi cosaquoteTaskNo
Avviare una generazionecreateTaskSì
Avviare una generazione e attenderne il risultato in una sola chiamatarunSì
Consultare un’attività una voltagetTaskNo
Attendere che un’attività finiscawaitForTaskNo
Ripetere un’attività fallitaretryTaskSì (una nuova attività)
Elencare le attività create con la tua chiavelistTasksNo
Controllare il saldogetBalanceNo
Vedere quanto hai speso, per giorno e per modellogetUsageNo
Caricare un file locale di immagine, video o audiouploadFile, uploadBytes, uploadBase64No
Ottenere un nuovo link di download per un risultatocreateDownloadUrlNo
Eliminare i file archiviati e il prompt di un’attività conclusapurgeTaskNo (e nessun rimborso)
Verificare che un webhook arrivi davvero da SpicyAPIverifyWebhook (una funzione semplice)No
Cercare nella documentazione o leggere il contratto dell’API offlinesearchDocumentation, readOpenApiContract (funzioni semplici)No

Prima di iniziare

Ti servono una decina di minuti. Ogni passaggio spiega che cosa fa il comando, così nulla viene eseguito alla cieca.

Installa Node.js 22.13 o successivo

Node.js è il programma che esegue JavaScript fuori da un browser. Installa la versione LTS da nodejs.org; va bene qualsiasi versione dalla 22.13 in su. Poi apri un terminale (Terminale su macOS, PowerShell su Windows) e verifica:

node -v

Il comando stampa la versione installata, per esempio v22.13.0 o v24.8.0. Se il numero è inferiore a 22.13, o il comando non viene trovato, installa di nuovo Node.js e apri una nuova finestra del terminale.

Crea una cartella di progetto e installa l’SDK

Esegui questi comandi uno alla volta. Funzionano allo stesso modo su macOS, Linux e Windows PowerShell.

mkdir spicy-demo
cd spicy-demo
npm init -y
npm pkg set type=module
npm install @spicyapi/sdk
ComandoChe cosa fa
mkdir spicy-demoCrea una cartella vuota per il tuo progetto
cd spicy-demoSposta il terminale in quella cartella; i comandi successivi agiscono lì
npm init -yCrea package.json, il file che elenca le impostazioni e le librerie del tuo progetto
npm pkg set type=moduleDice a Node.js che i tuoi file usano la sintassi moderna import, richiesta da questo SDK
npm install @spicyapi/sdkScarica l’SDK in una cartella node_modules e lo registra in package.json

L’SDK è pubblicato solo come modulo ES moderno: import { SpicyClient } from "@spicyapi/sdk" funziona, mentre il vecchio require("@spicyapi/sdk") fallisce con ERR_PACKAGE_PATH_NOT_EXPORTED. Se salti npm pkg set type=module, dai invece ai tuoi file l’estensione .mjs.

Crea una chiave API

  1. Accedi a spicyapi.ai. Non hai ancora un account? Registrati su spicyapi.ai/register; se le nuove registrazioni sono sospese, quella pagina offre una lista d’attesa.
  2. Apri la pagina Chiavi API della console e fai clic su Crea chiave. Serve solo un nome.
  3. Copia subito la chiave. Inizia con sk-spicy- e viene mostrata una sola volta. Se la perdi, creane una nuova.

Puoi impostare un limite di spesa giornaliero nella stessa pagina; le chiavi nuove non ne hanno alcuno finché non ne aggiungi uno. Per eseguire attività a pagamento ti serve anche un saldo, che aggiungi in Fatturazione.

Metti la chiave in una variabile d’ambiente

Una variabile d’ambiente è un valore con un nome che il terminale passa a ogni programma che avvia. L’SDK ne cerca una chiamata SPICY_API_KEY, così la chiave non deve mai essere scritta nel tuo codice.

# Impostala per questa finestra del terminale (incolla la tua chiave)
export SPICY_API_KEY="sk-spicy-paste-your-key-here"

# Verifica: dovrebbe stampare sk-spicy-
echo "${SPICY_API_KEY:0:9}"

Il valore sparisce quando chiudi la finestra. Per conservarlo sul tuo computer, aggiungi la riga export a ~/.zshrc (macOS) o a ~/.bashrc (Linux) e apri una nuova finestra.

Preferisci un file .env? Crea nella cartella del progetto un file chiamato .env che contenga una sola riga, SPICY_API_KEY=sk-spicy-paste-your-key-here, e avvia il programma con node --env-file=.env yourfile.mjs. L’SDK non legge da solo i file .env: è quel flag a farlo. Aggiungi .env al file .gitignore prima di eseguire git add anche una sola volta, così la chiave non finisce mai in un commit.

Su un server vero, imposta SPICY_API_KEY nelle impostazioni dei segreti o delle variabili d’ambiente del tuo provider di hosting, oppure passa la chiave dal tuo gestore di segreti con new SpicyClient({ apiKey }).

Esegui il tuo primo file

Nella cartella del progetto crea, con un editor di testo qualsiasi, un file chiamato check.mjs, incollaci questo codice e salva:

check.mjs
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();
const status = await client.getStatus();

console.log(status.operational ? "SpicyAPI is up" : "SpicyAPI is not reachable right now");
console.log("Checked at", status.checkedAt);

Eseguilo:

node check.mjs
SpicyAPI is up
Checked at 2026-09-17T08:00:00.000Z

Questa verifica non richiede la chiave e non costa nulla, quindi dimostra che Node.js e l’SDK sono installati correttamente.

Usi TypeScript? Salva lo stesso codice come check.ts. Le versioni recenti di Node.js 22 e 24 lo eseguono direttamente con node check.ts. Se la tua si lamenta dell’estensione .ts, esegui node --experimental-strip-types check.ts, oppure npx tsx check.ts.

Il tuo primo risultato

Questo programma fa l’intero percorso: controlla il servizio, sceglie un modello di immagini che la tua chiave può usare, chiede il prezzo esatto e si ferma. Eseguilo una seconda volta con --yes e il programma crea l’immagine, la attende e la salva accanto allo script.

L’esecuzione con --yes costa denaro

Senza --yes il programma si limita a leggere: non viene creato nulla e non viene addebitato nulla. Con --yes crea un’attività di immagine, che accantona l’importo del preventivo e lo addebita se l’immagine riesce.

first-result.mjs
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { SpicyApiError, SpicyClient } from "@spicyapi/sdk";

// Legge la tua chiave dalla variabile d’ambiente SPICY_API_KEY.
const client = new SpicyClient();

async function main() {
  // Passo 1 — SpicyAPI è raggiungibile? Gratis, e senza chiave.
  const status = await client.getStatus();
  if (!status.operational) {
    throw new Error("SpicyAPI is not reachable right now. Try again in a few minutes.");
  }

  // Passo 2 — Quali modelli di immagini può chiamare questa chiave? Gratis.
  const catalog = await client.listModels({ modality: "image", includeExamples: true });
  const model = catalog.items.find(
    (item) => item.enabled && item.available && item.examples?.length,
  );
  if (!model) throw new Error("No image model is available to this key right now.");
  console.log(`Model: ${model.displayName} (${model.model})`);

  // Passo 3 — Costruisci l’input. Parti da un esempio che il server ha già
  // verificato, e metti il tuo prompt se il modello ne accetta uno.
  const input = { ...model.examples?.[0]?.input };
  if ("prompt" in input) input.prompt = "A paper lantern glowing on a rainy street at night";

  // Passo 4 — Chiedi il prezzo esatto. Gratis: non viene creato né riservato nulla.
  const request = { model: model.model, input };
  const quote = await client.quoteTask(request);
  console.log(`Price: $${quote.estimatedCost} USD`);

  if (!process.argv.includes("--yes")) {
    console.log("Nothing was charged. Run again with --yes to generate the image.");
    return;
  }

  // Passo 5 — Crea l’attività e attendila. È questo il passaggio che costa denaro.
  const task = await client.run(
    { ...request, quoteId: quote.quoteId, expectedCost: quote.estimatedCost },
    {
      idempotencyKey: randomUUID(),
      onAccepted: (accepted) => console.log(`Task accepted: ${accepted.taskId}`),
      onUpdate: (update) => console.log(`State: ${update.state}`),
    },
  );

  // Passo 6 — Un’attività fallita è una risposta normale. Controlla la liquidazione.
  if (task.state !== "succeeded") {
    console.log(`The task ended as ${task.state}: ${task.errorCode} ${task.errorMessage ?? ""}`);
    console.log("Check settled and cost for the final charge.");
    return;
  }

  // Passo 7 — Scarica subito il risultato. Il link scade dopo circa 20 minuti.
  const asset = task.output?.assets?.find((item) => item.url);
  if (!asset?.url) throw new Error("The task succeeded but its file is unavailable.");
  const download = await fetch(asset.url); // non inviare mai qui la tua chiave API
  if (!download.ok) throw new Error(`Download failed with HTTP ${download.status}`);
  const extension = asset.mime?.split("/")[1] ?? "png";
  await writeFile(`result.${extension}`, Buffer.from(await download.arrayBuffer()));
  console.log(`Saved result.${extension}. Charged: $${task.cost}${task.settled ? "" : " (not final yet)"}`);
}

main().catch((error) => {
  if (error instanceof SpicyApiError) {
    console.error(`SpicyAPI error ${error.code}: ${error.message} (request_id ${error.requestId})`);
  } else {
    console.error(error);
  }
  process.exitCode = 1;
});

Eseguilo due volte:

node first-result.mjs
node first-result.mjs --yes
Model: <display name> (<model ID>)
Price: $<amount> USD
Nothing was charged. Run again with --yes to generate the image.

Model: <display name> (<model ID>)
Price: $<amount> USD
Task accepted: job_…
State: running
State: succeeded
Saved result.png. Charged: $<amount>

Che cosa fa ogni passaggio

  1. getStatus() chiede all’endpoint di stato di SpicyAPI se il servizio è attivo. Non genera mai un’eccezione per un problema di rete: riporta invece operational: false.
  2. listModels() restituisce il catalogo dei modelli in tempo reale con i prezzi del tuo account. Usa solo le voci in cui enabled e available valgono entrambi true. includeExamples: true aggiunge input di esempio che il server ha già convalidato, il punto di partenza più sicuro. Il valore che invierai dopo è il campo model della voce, cioè l’ID del modello.
  3. L’input. Ogni modello accetta campi diversi; un campo che il modello non conosce viene rifiutato con l’errore 400. Copiare un esempio convalidato e cambiare solo il prompt evita il problema. Per vedere tutti i campi accettati da un modello, chiama getModel() e leggi il suo inputSchema.
  4. quoteTask() calcola il prezzo di questa richiesta esatta. Non crea nulla e non riserva nulla. Gli importi sono dollari statunitensi scritti come testo, per esempio "0.04", così non si insinua alcun arrotondamento.
  5. run() crea l’attività e poi la controlla finché non finisce. Passare quoteId ed expectedCost significa «accetta l’attività solo al prezzo che mi è stato mostrato»: se il prezzo è cambiato, la chiamata fallisce con 40901 invece di addebitare un importo diverso. La idempotencyKey rende sicuro un nuovo tentativo dopo un problema di rete; in un’app reale, salvala prima della chiamata, così un programma riavviato può riutilizzarla. onAccepted ti consegna l’ID attività nel momento stesso in cui l’attività esiste: salvalo, così potrai controllare l’attività più tardi anche se il tuo programma si ferma.
  6. Lo stato dell’attività. run() ritorna normalmente anche per le attività fallite. Le attività fallite o scadute normalmente liberano l’intero importo accantonato. Alcuni rifiuti di contenuto vengono addebitati al preventivo accettato, solo se la pagina del modello indica esplicitamente questa eccezione. Controlla settled e cost per conoscere l’importo finale.
  7. Il download. Ogni risultato arriva con un url temporaneo. Scaricalo senza la tua chiave API e tieniti una copia: il link dura circa 20 minuti (chiama di nuovo getTask() per averne uno nuovo) e SpicyAPI elimina i file generati al massimo dopo circa 14 giorni. Vedi Conservazione e distruzione.

I video funzionano allo stesso modo, ma possono richiedere diversi minuti: passa a run() un timeoutMs più lungo (l’attesa predefinita è di 10 minuti). Negli snippet qui sotto l’ID del modello compare come MODEL_ID_FROM_CATALOG: è un segnaposto, non un modello reale. Sostituiscilo con il valore model di una voce del catalogo in tempo reale (live catalog) — listModels() nell’SDK, oppure l’endpoint autenticato GET /api/v1/models — in cui enabled e available valgono entrambi true.

Riferimento API

Ogni voce qui sotto spiega a che cosa serve il metodo, come chiamarlo, che cosa restituisce, se può costare denaro e quali errori aspettarsi. Le classi di errore come SpicyApiError sono spiegate in Gestire gli errori. Tutti gli importi sono dollari statunitensi scritti come testo ("0.04"), e tutti gli orari sono stringhe ISO 8601 in UTC.

Creare un client

new SpicyClient(options?: SpicyClientOptions)

Quasi tutti i programmi non passano nulla: new SpicyClient() legge la chiave da SPICY_API_KEY e dialoga con il servizio di produzione. Un’opzione non valida genera subito un TypeError, prima che parta qualsiasi richiesta.

Proprietà

Tipo

Nuovi tentativi automatici

L’SDK ripete da solo una richiesta solo quando farlo non può costarti due volte:

ChiamataRitentata automaticamente?
Letture: getStatus, listModels, getModel, getTask, listTasks, getBalance, getUsage; inoltre quoteTask, purgeTask, commitUploadedFileSì
createTask, run (la sua fase di creazione), retryTaskSolo se passi una idempotencyKey. Senza, un errore di rete ti arriva subito, perché inviare di nuovo la richiesta potrebbe creare una seconda attività
createUploadUrl, l’invio vero e proprio dei byte, createDownloadUrlNo

Un nuovo tentativo scatta quando non arriva alcuna risposta (errore di rete o timeout locale) oppure quando la risposta è HTTP 408, 429, 500, 502, 503 o 504. La pausa è quella chiesta dall’intestazione Retry-After del server; in mancanza, 250 ms, poi 500 ms, poi 1 s e così via, ognuna variata fino al 25% e mai oltre maxRetryDelayMs. Quando falliscono tutti i tentativi, ricevi l’ultimo errore.

Fermare una chiamata con signal

Ogni metodo accetta un’opzione signal, un AbortSignal standard. Usa AbortSignal.timeout(5_000) per fissare una scadenza, oppure un AbortController per fermare la chiamata quando vuoi. La chiamata genera allora il motivo dell’interruzione. Fermarla chiude solo la tua richiesta o la tua attesa: un’attività che SpicyAPI ha già accettato continua a girare e può comunque essere addebitata.

getStatus()

Verifica se SpicyAPI è raggiungibile. Comodo come prima riga di uno script o come controllo di stato nel tuo sistema di monitoraggio.

getStatus(options?: { signal?: AbortSignal }): Promise<ServiceStatus>
Campo restituitoSignificato
operationaltrue quando il servizio risponde. È il campo da usare
healthIl risultato grezzo del controllo di stato: path, ok, lo status HTTP, i primi 512 caratteri di body ed error quando non è arrivata alcuna risposta
readinessStessa forma, per un secondo controllo. Sul servizio pubblico risponde 404: è normale e non significa che il servizio sia fuori uso
checkedAtQuando è stato eseguito il controllo

Costa denaro: No. Richiede una chiave: No. Errori: non genera eccezioni per i problemi di rete: ottieni operational: false e il motivo in health.error. Genera un’eccezione solo se lo interrompi con signal. Per un esempio, vedi Esegui il tuo primo file.

listModels(options?)

Elenca il catalogo dei modelli in tempo reale, con i prezzi del tuo account. È da qui che arriva ogni ID del modello.

listModels(options?: ListModelsOptions): Promise<ModelList>

Proprietà

Tipo

Restituisce { total, items }. I campi più utili di ogni voce:

CampoSignificato
modelL’ID del modello. Invia esattamente questo valore quando crei un’attività
displayNameUn nome leggibile
modality, tasksChe cosa produce e quale lavoro svolge
provider, familyChi ha creato il modello e a quale linea di prodotti appartiene
enabled, availableDevono valere entrambi true prima di usare il modello
pricingUna voce per ogni fascia di prezzo: price, unit (per_image, per_second, per_request o per_1k_tokens) e variant (quale fascia, spesso una risoluzione). Durante uno sconto, regularPrice mostra il prezzo non scontato
startingPriceLa fascia più economica. Per una cifra esatta, usa quoteTask
quantityFieldIl campo di input che determina la quantità fatturata, per esempio una durata. Se è vuoto, si conta un’unità per chiamata
taskTimeoutSecondsQuanto può durare un’attività prima di chiudersi come expired
maxOutputDurationSecondsL’output più lungo consentito dal modello, quando l’output ha una durata
inputSchema, examplesPresenti solo se li hai richiesti
updatedAt, versionCambiano ogni volta che cambiano i dettagli del modello; usali per capire quando aggiornare uno schema salvato

Costa denaro: No. Richiede una chiave: Sì. Errori: SpicyApiError (per esempio 401 per un problema con la chiave, 429 per troppe richieste), SpicyTransportError.

const { items } = await client.listModels({ modality: "video", task: "image-to-video" });

for (const item of items) {
  if (!item.enabled || !item.available) continue;
  const from = item.startingPrice ? `from $${item.startingPrice.price} ${item.startingPrice.unit}` : "";
  console.log(item.model, "|", item.displayName, "|", from);
}

Le letture del catalogo usano la disponibilità di lettura dell’account, indipendentemente dalla creazione delle attività. Salva l’elenco e aggiornalo ogni tanto, oppure dopo un 400 o un 50301, invece di leggerlo prima di ogni attività.

getModel(modelId)

Restituisce un modello con il suo schema completo: l’elenco dei campi di input che accetta, quali sono obbligatori e quali valori sono ammessi.

getModel(modelId: string, options?: { signal?: AbortSignal }): Promise<ApiModel>

Il risultato contiene tutti i campi descritti in listModels, sempre inclusi inputSchema ed examples. Lo schema è JSON Schema standard: properties elenca ogni campo con il suo type e spesso enum (i valori ammessi), minimum / maximum e default; required elenca i campi che devi inviare.

Costa denaro: No. Richiede una chiave: Sì. Errori: SpicyApiError con 404 quando nessun modello ha quell’ID; TypeError se l’ID è vuoto.

const model = await client.getModel("MODEL_ID_FROM_CATALOG");

console.log("Required fields:", model.inputSchema?.required);
console.log(JSON.stringify(model.inputSchema?.properties, null, 2));
console.log("A request that is known to work:", model.examples?.[0]?.input);

quoteTask(request)

Fornisce il prezzo esatto di una richiesta senza creare nulla. Usalo per mostrare un prezzo a una persona, o per rifiutare i lavori che superano un budget.

quoteTask(request: { model: string; input: object; callBackUrl?: string }, options?: { signal?: AbortSignal }): Promise<TaskQuote>

Passa gli stessi model, input ed eventuale callBackUrl con cui intendi creare l’attività.

Campo restituitoSignificato
quoteIdPassalo a createTask o a run per bloccare questo prezzo
estimatedCostL’importo che verrà accantonato se crei l’attività adesso
maxChargeIl massimo che questa richiesta può costare
currencySempre USD
quantity, unitLa quantità fatturata, per esempio "5" e per_second
expiresAtIl preventivo vale cinque minuti

Costa denaro: No. Richiede una chiave: Sì. Errori: SpicyApiError, per esempio 400 quando l’input non corrisponde allo schema del modello, 50301 quando il modello non è disponibile, 429.

const request = { model: "MODEL_ID_FROM_CATALOG", input: { prompt: "A paper lantern at night" } };
const quote = await client.quoteTask(request);

console.log(`This will cost $${quote.estimatedCost}. Quote valid until ${quote.expiresAt}.`);

createTask(request, options?)

Avvia una generazione e ritorna subito con un ID attività. Non attende il risultato: prosegui con waitForTask o con un webhook, oppure usa run per fare entrambe le cose in una sola chiamata.

createTask(request: CreateTaskInput, options?: CreateTaskOptions): Promise<CreateTaskResult>

La richiesta:

Proprietà

Tipo

Le opzioni:

Proprietà

Tipo

Campo restituitoSignificato
taskIdL’ID della nuova attività. Salvalo subito
statequeued per un’attività nuova. Se hai ripetuto una chiave di idempotenza, lo stato attuale dell’attività originale
estimatedCostL’importo ora accantonato, che è anche il massimo che questa attività può addebitare
deadlineAtQuando SpicyAPI smette di lavorare sull’attività, se non è finita

Costa denaro: Sì. L’estimatedCost viene accantonato dal tuo saldo. Se l’attività riesce ti viene addebitato il consumo effettivo, mai più dell’accantonamento, e il resto viene svincolato. Le attività fallite o scadute normalmente liberano l’intero importo accantonato. Alcuni rifiuti di contenuto vengono addebitati al preventivo accettato, solo se la pagina del modello indica esplicitamente questa eccezione. Controlla settled e cost per conoscere l’importo finale.

Errori:

error.codeChe cosa significaChe cosa fare
400L’input non corrisponde allo schema del modello, oppure callBackUrl non è ammessoConfronta con getModel().inputSchema e correggi la richiesta
401La chiave manca, è malformata o è stata revocataControlla SPICY_API_KEY
40201Il saldo non basta a coprire l’accantonamentoRicarica da Fatturazione nella console
40202È stato raggiunto un limite di spesa giornaliero, mensile o complessivoAlza il limite nella pagina Chiavi API, oppure aspetta che si azzeri
40301Questa chiave non può usare questo modelloModifica l’elenco dei modelli della chiave nella console
409Questa chiave di idempotenza è già stata usata per una richiesta diversaUsa una chiave nuova per ogni nuovo lavoro
40901Il preventivo è scaduto, oppure il prezzo o la richiesta sono cambiatiChiedi un nuovo preventivo e conferma il nuovo prezzo
413Il corpo della richiesta è troppo grandeCarica i file multimediali invece di inserirli nella richiesta
429Troppe richiesteAspetta error.retryAfterSeconds, poi riprova
50301Il modello al momento non può accettare attivitàRiprova più tardi, oppure scegli un altro modello

Uno SpicyTransportError o uno SpicyTimeoutError significano che non è arrivata alcuna risposta, quindi non puoi sapere se l’attività è stata creata. Invia di nuovo la stessa richiesta con la stessa chiave di idempotenza: o la crei, o ti torna l’attività già esistente. Un TypeError significa che un’opzione non era valida (per esempio un retentionSeconds negativo) e che non è stato inviato nulla.

import { randomUUID } from "node:crypto";

const idempotencyKey = randomUUID(); // in un’app reale, salvala prima insieme al tuo ordine
const accepted = await client.createTask(
  { model: "MODEL_ID_FROM_CATALOG", input: { prompt: "A paper lantern at night" } },
  { idempotencyKey },
);

console.log(accepted.taskId, accepted.state, "on hold: $" + accepted.estimatedCost);

run(request, options?)

Il modo più semplice per generare qualcosa: chiama createTask, poi waitForTask, e restituisce l’attività conclusa.

run(request: CreateTaskInput, options?: RunTaskOptions): Promise<TaskRecord>

request è lo stesso di createTask. Le opzioni sono tutte quelle accettate da createTask, più:

Proprietà

Tipo

Restituisce l’attività conclusa, con i campi descritti in getTask. Per un’attività riuscita attende anche che i file del risultato abbiano finito di essere copiati, così output.assets[].url è pronto per il download.

Costa denaro: Sì, esattamente come createTask. Un’attività fallita viene restituita, non generata come eccezione: controlla task.state.

Errori: tutti quelli che può generare createTask, più:

  • SpicyWaitTimeoutError quando timeoutMs si esaurisce dopo che l’attività è stata accettata. L’attività continua a girare su SpicyAPI. Chiama più tardi waitForTask(error.taskId) per ritirarla.
  • SpicyTimeoutError quando il tempo si esaurisce prima che SpicyAPI abbia confermato l’attività. Invia la stessa richiesta con la stessa chiave di idempotenza per scoprire com’è andata.
  • Qualsiasi errore generato dentro i tuoi onAccepted o onUpdate interrompe l’attesa; l’attività in sé continua a girare.
  • TypeError per un timeoutMs o un intervalMs non validi. Vengono controllati prima di creare qualsiasi cosa.

Per un esempio completo, vedi Il tuo primo risultato.

getTask(taskId)

Legge un’attività una volta: il suo stato, i link ai risultati e quanto è costata.

getTask(taskId: string, options?: { signal?: AbortSignal }): Promise<TaskRecord>
CampoSignificato
taskId, modelQuale attività, creata con quale modello
statequeued o running mentre è in lavorazione; succeeded, failed o expired quando è finita (canceled compare solo nei record più vecchi)
output.assetsI file del risultato, descritti nella tabella successiva. Solo per succeeded
output.textL’output testuale, per i modelli che restituiscono testo
errorCode, errorMessagePerché un’attività è fallita. Prendi le decisioni in base a errorCode; la formulazione del messaggio può cambiare
costFinché settled è false, l’importo accantonato; quando è true, l’addebito definitivo
settledSe cost è definitivo
createdAt, completedAt, deadlineAtQuando è stata creata, quando è finita e quando scadrebbe
inputL’input così come è stato archiviato. Viene rimosso quando termina il periodo di conservazione dei prompt
sourceTaskIdPresente su un’attività creata da retryTask: punta all’originale
contentStatepresent; expired (rimosso dalle regole di conservazione); oppure purged (l’hai eliminato tu con purgeTask)
retentionQuando verranno rimossi i file (outputsExpireAt) e il prompt (promptsExpireAt)

Ogni elemento di output.assets:

CampoSignificato
urlUn link di download temporaneo. Scaricalo senza la tua chiave API
expiresAtQuando quel link smette di funzionare, di norma dopo circa 20 minuti. Chiama di nuovo getTask per un link nuovo
mimeIl tipo di file, per esempio image/png o video/mp4
width, height, durationSeconds, bytesDati sulle dimensioni, quando pertinenti
pendingtrue mentre il file viene ancora copiato nell’archivio. Ricontrolla tra qualche secondo
unavailabletrue quando il file è sparito per sempre. Aspettare non serve: genera di nuovo
keyUn nome di file interno, necessario solo a createDownloadUrl

Un campo per cui SpicyAPI non ha un valore viene omesso invece di essere impostato a null, quindi considera facoltativi tutti i campi qui sopra.

Costa denaro: No. Errori: SpicyApiError con 404 quando la tua chiave non vede alcuna attività con quell’ID.

const task = await client.getTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3"); // l’ID che hai salvato

if (task.state === "succeeded") {
  for (const asset of task.output?.assets ?? []) console.log(asset.mime, asset.url);
} else if (task.state === "queued" || task.state === "running") {
  console.log("Still working. On hold: $" + task.cost);
} else {
  console.log(`Finished as ${task.state}: ${task.errorCode}`);
}

waitForTask(taskId, options?)

Controlla un’attività ripetutamente finché non finisce, poi la restituisce. Usalo per un’attività creata in precedenza, per esempio dopo che il tuo programma si è riavviato.

waitForTask(taskId: string, options?: WaitForTaskOptions): Promise<TaskRecord>

Opzioni: timeoutMs (predefinito 600000, fino a 86400000), intervalMs, onUpdate e signal, tutte come descritte in run.

Ritorna quando state è succeeded, failed, expired o canceled e, per un’attività riuscita, quando nessun file del risultato è più pending. Ogni controllo è una richiesta getTask e conta nel limite di richieste del tuo account: per questo l’intervallo predefinito rallenta col passare del tempo.

Costa denaro: No. Errori: SpicyWaitTimeoutError quando timeoutMs si esaurisce. Contiene taskId, timeoutMs e lastTask (l’ultimo stato osservato). L’attività non viene annullata: puoi semplicemente attendere di nuovo. Inoltre SpicyApiError (per esempio 404) e SpicyTransportError.

const task = await client.waitForTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3", {
  timeoutMs: 30 * 60_000,
  onUpdate: (update) => console.log(new Date().toISOString(), update.state),
});
console.log(task.state, task.output?.assets?.[0]?.url);

retryTask(taskId, options?)

Esegue di nuovo un’attività fallita, come attività del tutto nuova con lo stesso modello e lo stesso input. L’attività originale resta esattamente com’era, e quella nuova rimanda a essa tramite sourceTaskId.

retryTask(taskId: string, options?: { idempotencyKey?: string; signal?: AbortSignal }): Promise<RetryTaskResult>

Funziona sulle attività failed ed expired (e sulle vecchie canceled). Un’attività riuscita o ancora in corso viene rifiutata con 409. Il prezzo, il tuo saldo e i permessi della chiave vengono controllati di nuovo, e la nuova attività mantiene l’impostazione di conservazione dell’attività originale.

Poiché l’input viene riutilizzato senza modifiche, ripetere un’attività serve solo quando il problema era temporaneo. Leggi prima l’errorCode dell’attività di origine: rate_limited, upstream_unavailable, timeout e upstream_failed meritano un nuovo tentativo dopo una pausa; generation_failed e invalid_asset richiedono una richiesta modificata, inviata con createTask; content_rejected, invalid_request e unsupported_combination non vanno ripetuti senza modifiche. Vedi Codici di errore comuni ed Errori.

Restituisce taskId (la nuova attività), sourceTaskId (l’originale), state, estimatedCost e deadlineAt.

Costa denaro: Sì. La nuova attività ha un proprio accantonamento e viene addebitata a parte se riesce. Passa una idempotencyKey, così eseguire due volte il tuo codice di ripetizione crea un solo nuovo tentativo.

Errori: 409 (l’attività non si può ripetere), più gli errori di saldo, limite di spesa e disponibilità elencati in createTask. Se l’input originale usava un file caricato spicy://, quel file è utilizzabile solo per un giorno: caricalo di nuovo e usa invece createTask.

retryTask non serve per gli errori di rete

Se è la chiamata createTask stessa a fallire per un errore di rete, non hai alcuna attività da ripetere. Invia di nuovo createTask con la stessa chiave di idempotenza. Usa retryTask solo per un’attività di cui hai letto lo state e hai visto failed o expired.

Trovi un esempio completo in Ripetere un’attività fallita.

listTasks(options?)

Elenca le attività create con questa chiave API, dalla più recente, una pagina alla volta. Utile per ritrovare il lavoro dopo un riavvio o un webhook perso.

listTasks(options?: ListTasksOptions): Promise<TaskList>

Proprietà

Tipo

Restituisce items, hasMore e, quando ci sono altre pagine, nextCursor. Ogni voce ha taskId, model, state, cost, settled, createdAt, deadlineAt e completedAt. Input, risultati e link non sono inclusi: chiama getTask per l’attività che ti serve.

Costa denaro: No. Errori: SpicyApiError (per esempio 400 per un intervallo di date non valido), SpicyTransportError.

let cursor;
do {
  const page = await client.listTasks({
    from: "2026-09-01", // mantieni le stesse date su ogni pagina
    to: "2026-09-08",
    state: "failed",
    limit: 100,
    cursor,
  });
  for (const item of page.items) console.log(item.taskId, item.model, item.createdAt);
  cursor = page.hasMore ? page.nextCursor : undefined;
} while (cursor);

getBalance()

Mostra quanto denaro c’è nel tuo account.

getBalance(options?: { signal?: AbortSignal }): Promise<Balance>
CampoSignificato
availableIl denaro che può finanziare nuove attività. Può essere negativo quando il tuo account usa una linea di credito approvata
heldIl denaro accantonato per le attività ancora in corso
totalavailable più held
fundingDettaglio facoltativo: fondi prepagati, bonus promozionali (con i modelli a cui si applicano e la loro scadenza) ed eventuale linea di credito. Le risposte più vecchie possono ometterlo

Costa denaro: No. Non devi controllare il saldo prima di ogni attività: SpicyAPI lo controlla quando ne crei una e risponde 40201 se non basta. Vedi Fatturazione per tutti i campi di funding.

const balance = await client.getBalance();
console.log(`Available: $${balance.available} (on hold: $${balance.held})`);

getUsage(options?)

Riporta quanto ha speso questa chiave API, raggruppato per giorno e per modello.

getUsage(options?: { from?: string; to?: string; signal?: AbortSignal }): Promise<UsageReport>

from e to sono date UTC (YYYY-MM-DD); from è incluso, to no. Omettili entrambi per gli ultimi sette giorni, oggi compreso. L’intervallo può coprire al massimo 92 giorni.

CampoSignificato
totalCallsNumero di attività nell’intervallo
totalSpendSolo gli addebiti definitivi; il denaro ancora accantonato non viene contato
daysUna voce per ogni giorno con chiamate: day, calls, succeeded, failed, spend
modelsUna voce per modello: model, calls, succeeded, failed, spend

Costa denaro: No. Questo report è più pesante delle altre letture, quindi ha un limite aggiuntivo di 30 richieste al minuto: usalo per i report, non in un ciclo. Errori: SpicyApiError (429 oltre quel limite), SpicyTransportError.

const usage = await client.getUsage({ from: "2026-09-01", to: "2026-09-08" });
console.log(`${usage.totalCalls} tasks, $${usage.totalSpend} spent`);
for (const row of usage.models) console.log(row.model, row.calls, "$" + row.spend);

Caricare file

Quando un modello richiede in input un’immagine, un video o un audio, puoi mettere direttamente nel campo di input un link HTTPS pubblico al file. Se il file è sul tuo computer, caricalo prima. Ti torna un indirizzo spicy://: mettilo nel campo di input al posto del link. Il caricamento è gratuito.

  • Immagini (JPEG, PNG, WebP, GIF): fino a 10 MiB.
  • Video (MP4, WebM) e audio (MP3, WAV): fino a 90 MiB e 600 secondi di durata.
  • Un modello può fissare nel suo schema un limite più basso.
  • Un file caricato si può usare per un giorno, e solo dal tuo account.

uploadFile(path, options?)

Carica un file dal disco.

uploadFile(path: string, options?: { contentType?: string; signal?: AbortSignal }): Promise<UploadedFile>

Il tipo di file viene riconosciuto dall’estensione: .jpg, .jpeg, .png, .webp, .gif, .mp4, .webm, .mp3 o .wav. Per qualsiasi altro nome, passa contentType: image/jpeg, image/png, image/webp, image/gif, video/mp4, video/webm, audio/mpeg o audio/wav.

Campo restituitoSignificato
uriL’indirizzo spicy:// da mettere nell’input di un’attività
expiresAtQuando l’indirizzo smette di funzionare (dopo un giorno)
fileId, status, bytes, contentType, sha256I dettagli di ciò che è stato archiviato; status vale ready
width, height, durationSecondsDimensioni o durata misurate, quando pertinenti

Costa denaro: No. Errori: TypeError quando il tipo non si può riconoscere (il messaggio elenca tutte le estensioni riconosciute: contentType is required unless the file extension is one of: gif, jpeg, jpg, png, webp, mp4, webm, mp3, wav); un errore ENOENT di Node.js quando il percorso è sbagliato; SpicyUploadError (con status) quando l’archivio ha rifiutato i byte o il file supera la dimensione consentita (413); SpicyApiError quando è SpicyAPI ad aver rifiutato il caricamento; SpicyTimeoutError quando l’invio ha richiesto più di uploadTimeoutMs.

const file = await client.uploadFile("./photo.jpg");
console.log(file.uri, "can be used until", file.expiresAt);

uploadBytes(data, options)

Carica byte che hai già in memoria, per esempio un file che un utente ha inviato al tuo server. Va bene anche un Buffer di Node.js.

uploadBytes(data: Uint8Array, options: { contentType: string; signal?: AbortSignal }): Promise<UploadedFile>

contentType è obbligatorio. Risultato ed errori sono gli stessi di uploadFile.

const response = await fetch("https://example.com/reference.png");
const bytes = new Uint8Array(await response.arrayBuffer());
const file = await client.uploadBytes(bytes, { contentType: "image/png" });

uploadBase64(value, options?)

Carica un file che hai sotto forma di testo Base64: un data URI come data:image/png;base64,iVBOR…, oppure Base64 semplice insieme a contentType.

uploadBase64(value: string, options?: { contentType?: string; signal?: AbortSignal }): Promise<UploadedFile>

Codifica e dimensione vengono controllate prima sul tuo computer: dati non validi o troppo grandi generano un TypeError senza caricare nulla. Il risultato è lo stesso di uploadFile.

createUploadUrl e commitUploadedFile

I tre helper qui sopra sono costruiti con queste due chiamate di livello più basso. Ti servono solo se vuoi inviare i byte da un posto diverso dal tuo processo Node.js.

createUploadUrl(contentType: string, bytes: number, options?: { signal?: AbortSignal }): Promise<UploadTicket>
commitUploadedFile(fileId: string, options?: { signal?: AbortSignal }): Promise<UploadedFile>
  1. createUploadUrl restituisce fileId, uploadUrl, method (PUT), headers, maxBytes ed expiresAt.
  2. Invia il file con un PUT a uploadUrl, con esattamente le intestazioni elencate in headers e senza intestazione Authorization. La dimensione dichiarata deve corrispondere al file.
  3. commitUploadedFile(fileId) controlla il file archiviato e restituisce l’uri spicy://. Un file caricato ma mai confermato con il commit non si può usare.

L’intero flusso, compresi i caricamenti dal browser, è descritto in Media.

createDownloadUrl(taskId, key?)

Crea un nuovo link di download temporaneo per un file del risultato. Ti serve di rado: getTask, waitForTask, run e i webhook includono già un url pronto per ogni risultato.

createDownloadUrl(taskId: string, key?: string, options?: { signal?: AbortSignal }): Promise<DownloadTicket>

Passa la key dell’asset per scegliere un file, oppure omettila per avere il primo. Restituisce key, url ed expiresAt (circa 20 minuti).

Costa denaro: No. Errori: SpicyApiError con 409 mentre il file viene ancora copiato (aspetta qualche secondo), oppure con 404 quando l’attività non è riuscita o la key non le appartiene.

purgeTask(taskId)

Elimina definitivamente, prima di quanto farebbe il periodo di conservazione, i media generati di un’attività conclusa, il payload del risultato (i dati del risultato archiviati), il prompt e gli altri testi di input. Usalo quando un risultato non deve restare conservato.

purgeTask(taskId: string, options?: { signal?: AbortSignal }): Promise<PurgeTaskResult>
Campo restituitoSignificato
contentStatepurged quando i contenuti sono stati eliminati. present significa che questa volta non è stato rimosso nulla; riprova più tardi
purgedAtQuando i contenuti sono stati eliminati la prima volta
contentRemovedByuser quando li hai eliminati tu
billingRetainedSempre true: il record di quanto è costata l’attività viene conservato
mediaDeletionPendingtrue mentre i file archiviati vengono ancora rimossi in background, di norma per meno di un minuto

Costa denaro: No, e non è un rimborso. La voce contabile, l’importo addebitato, il modello, lo stato, gli orari e l’ID della richiesta restano registrati. L’eliminazione è irreversibile. Chiamarla di nuovo sulla stessa attività non crea problemi. Errori: SpicyApiError con 400 quando l’attività è ancora queued o running, 404 quando non esiste. Un’attività accettata non si può annullare e non esiste un’API di annullamento, quindi attendi che finisca con waitForTask e poi distruggine i contenuti. Vedi Conservazione e distruzione.

const result = await client.purgeTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3");
console.log(result.contentState, "billing kept:", result.billingRetained);

Funzioni per i webhook

verifyWebhook e computeWebhookSignature sono funzioni semplici, non metodi del client. Sono descritte, con un esempio funzionante, in Webhook.

Helper offline e tipi

Funzionano senza connessione di rete e senza chiave.

ExportChe cosa ti dà
searchDocumentation(query, limit)Cerca in un piccolo indice integrato di questo sito di documentazione. Restituisce fino a limit voci (da 1 a 25, predefinito 10), ognuna con slug, title, summary, keywords e url, dove url ha la forma https://docs.spicyapi.ai/docs/<slug>. Se non trova nulla restituisce un array vuoto; con una query vuota restituisce le prime voci
DOCUMENTATIONL’intero indice integrato come array, una voce per ogni pagina della documentazione
readOpenApiContract()Il contratto OpenAPI 3.1 incluso in questa versione dell’SDK, come testo YAML
DEFAULT_API_BASE_URL, DEFAULT_SERVICE_BASE_URL, DEFAULT_TIMEOUT_MS, DEFAULT_UPLOAD_TIMEOUT_MS, DEFAULT_WAIT_TIMEOUT_MS, DEFAULT_WAIT_INTERVAL_MS, MAX_API_RESPONSE_BYTESI valori predefiniti descritti in questa pagina. Le risposte API più grandi di MAX_API_RESPONSE_BYTES (4 MiB) vengono rifiutate

Vengono esportati anche i tipi TypeScript di ogni richiesta e risposta, per esempio import type { TaskRecord, ApiModel, Balance } from "@spicyapi/sdk".

Percorso di importContiene
@spicyapi/sdkTutto ciò che è descritto in questa pagina
@spicyapi/sdk/webhooksSolo verifyWebhook, computeWebhookSignature e SpicyWebhookVerificationError: un import leggero per un ricevitore di webhook
@spicyapi/sdk/openapiSolo i tipi OpenAPI grezzi paths, components e operations

Webhook

Un webhook permette a SpicyAPI di avvisarti quando un’attività è finita, invece di costringere il tuo programma a chiederlo a intervalli di pochi secondi. Assegni a ogni attività un callBackUrl, cioè un indirizzo sul tuo server, e SpicyAPI ci invia il record dell’attività conclusa.

Perché devi verificare ogni webhook

L’indirizzo del tuo webhook è un URL pubblico: chiunque lo indovini può inviargli un messaggio falso che dice «attività riuscita, ecco il file». Se il tuo codice crede a quel messaggio, potrebbe consegnare il file sbagliato a un cliente o segnare come evaso un ordine.

Ogni consegna autentica è firmata con una chiave di firma segreta che conoscete solo tu e SpicyAPI. verifyWebhook ricalcola quella firma a partire dai byte esatti che hai ricevuto e confronta le due. Controlla anche l’orario della consegna, così un malintenzionato non può intercettare un messaggio autentico e rispedirtelo ore dopo. Agisci su un messaggio solo dopo che ha superato la verifica.

Configurazione

Ottieni la tua chiave di firma

Al momento della registrazione viene creata automaticamente una chiave di firma, ma il suo valore non viene mai mostrato, quindi devi crearne una che puoi copiare. Nella console apri Webhook e fai clic su Genera (oppure su Ruota, se la scheda indica che una chiave è già configurata). La nuova chiave viene mostrata una sola volta: copiala e salvala sul tuo server come SPICY_WEBHOOK_SECRET. Se la perdi, ruotala di nuovo.

La rotazione non prevede alcun periodo di sovrapposizione: la vecchia chiave smette subito di firmare, e anche le consegne per cui sono ancora in corso nuovi tentativi vengono firmate con quella nuova. Aggiorna ogni ricevitore in esecuzione subito dopo la rotazione. I dettagli sulla chiave di firma sono in Webhook.

Esegui un ricevitore su un indirizzo HTTPS pubblico

SpicyAPI può chiamare solo un indirizzo raggiungibile da internet che inizia con https://. Il semplice http://, localhost e gli indirizzi di reti private vengono rifiutati. Mentre sviluppi, esponi il tuo server locale tramite un servizio di tunnel che gli assegni un indirizzo HTTPS pubblico, oppure usa il polling finché non vai online. Le regole complete sull’URL di callback sono in Webhook.

Invia callBackUrl con le tue attività

Aggiungi callBackUrl: "https://your-app.example.com/hooks/spicy" alla richiesta che passi a createTask. Vedi Ricevere i risultati via webhook.

Un ricevitore minimo

Questo ricevitore usa solo il modulo http integrato in Node.js, quindi non c’è nient’altro da installare. Salvalo come receiver.mjs e avvialo con node receiver.mjs.

receiver.mjs
import { createServer } from "node:http";
import { SpicyWebhookVerificationError, verifyWebhook } from "@spicyapi/sdk/webhooks";

const secret = process.env.SPICY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set SPICY_WEBHOOK_SECRET first");

// ID delle consegne già gestite. In produzione usa una tabella del database,
// così l’elenco sopravvive a un riavvio.
const handled = new Set();

const server = createServer(async (request, response) => {
  if (request.method !== "POST" || request.url !== "/hooks/spicy") {
    response.writeHead(404).end();
    return;
  }
  if (request.headers["x-webhook-payload-version"] !== "2") {
    response.writeHead(400).end();
    return;
  }

  // 1. Leggi il corpo come byte grezzi. Non analizzarlo prima della verifica.
  const chunks = [];
  for await (const chunk of request) chunks.push(chunk);
  const rawBody = Buffer.concat(chunks);

  // 2. Controlla la firma e il timestamp.
  let event;
  try {
    event = verifyWebhook({
      rawBody,
      secret,
      timestamp: String(request.headers["x-webhook-timestamp"] ?? ""),
      signature: String(request.headers["x-webhook-signature"] ?? ""),
      payloadVersion: 2,
    });
  } catch (error) {
    const reason = error instanceof SpicyWebhookVerificationError ? error.reason : String(error);
    console.warn("Rejected a webhook:", reason);
    response.writeHead(401).end();
    return;
  }

  // 3. Rispondi in fretta. SpicyAPI aspetta al massimo 15 secondi, poi ritenta.
  response.writeHead(204).end();

  // 4. La stessa consegna può arrivare più di una volta. Gestisci ciascuna una sola volta.
  if (!event.deliveryId || handled.has(event.deliveryId)) return;
  handled.add(event.deliveryId);

  if (!("data" in event.payload)) return; // i corpi della versione 2 tengono l’attività sotto data
  const task = event.payload.data; // gli stessi campi restituiti da getTask()
  console.log("Task", task.taskId, "is", task.state);
  if (task.state === "succeeded") {
    console.log("Result:", task.output?.assets?.[0]?.url);
  }
});

server.listen(8080, () => console.log("Listening on http://localhost:8080/hooks/spicy"));

Che cosa conta in questo codice:

  • Byte grezzi. La firma copre i byte esatti inviati da SpicyAPI. Se un framework analizza prima il JSON e tu lo riconverti in testo, i byte cambiano e ogni verifica fallisce. In Express, usa express.raw({ type: "application/json" }) su questa route.
  • Rispondi in fretta. Qualsiasi risposta 2xx significa «ricevuto». Nessuna risposta entro 15 secondi, o qualsiasi altro stato, fa sì che SpicyAPI ritenti più volte nelle ore successive. Fai il lavoro lento dopo aver risposto.
  • I duplicati capitano. I nuovi tentativi e gli intoppi di rete possono consegnare due volte lo stesso messaggio. event.deliveryId resta uguale da un tentativo all’altro, quindi tieni traccia degli ID che hai già gestito.
  • L’esito sta in state. Anche una consegna per un’attività fallita è una consegna valida. Leggi task.state, mai il code della struttura di risposta.
  • In produzione, salva prima l’evento verificato nel tuo database e rispondi 2xx solo dopo che il salvataggio è riuscito; ignora inoltre gli ID attività che la tua applicazione non ha creato. La guida Webhook ne contiene una versione basata su database.

Prova il ricevitore prima di andare in produzione

computeWebhookSignature produce una firma vera, così puoi inviarti una consegna di prova firmata. Con receiver.mjs in esecuzione in un terminale e la stessa SPICY_WEBHOOK_SECRET impostata in un altro, esegui:

send-test.mjs
import { computeWebhookSignature } from "@spicyapi/sdk";

const secret = process.env.SPICY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set SPICY_WEBHOOK_SECRET first");

const body = JSON.stringify({
  code: 200,
  msg: "success",
  request_id: "whd_local_test_1",
  data: {
    taskId: "job_local_test",
    state: "succeeded",
    cost: "0.01",
    settled: true,
    output: { assets: [{ url: "https://example.com/result.png" }] },
  },
});
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = computeWebhookSignature("job_local_test", timestamp, body, secret);

const response = await fetch("http://localhost:8080/hooks/spicy", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Webhook-Timestamp": timestamp,
    "X-Webhook-Signature": signature,
    "X-Webhook-Payload-Version": "2",
  },
  body,
});
console.log("Your receiver answered", response.status); // ti aspetti 204

Eseguilo due volte: il ricevitore stampa l’attività una sola volta, perché la seconda consegna ha lo stesso request_id. Se cambi un carattere di body dopo la firma, il ricevitore risponde 401.

verifyWebhook(options)

verifyWebhook(options: VerifyWebhookOptions): VerifiedWebhook

Proprietà

Tipo

Restituisce payload (il corpo analizzato; per la versione 2 l’attività sta in payload.data), payloadVersion, taskId, deliveryId (il request_id della versione 2, identico da un tentativo all’altro) e timestamp. La funzione è sincrona: non serve await.

Quando la verifica fallisce genera SpicyWebhookVerificationError, il cui reason è uno tra invalid_signature, stale_timestamp, invalid_timestamp, invalid_json, invalid_payload, body_too_large e invalid_secret. Rispondi 401 e non agire sul messaggio.

computeWebhookSignature(taskId, timestamp, rawBody, secret) restituisce la firma Base64 di un corpo. Ti serve solo per test come quello qui sopra. L’algoritmo di firma completo, per altri linguaggi, è in Webhook.

Suggerimento per TypeScript: payload ha come tipo uno dei due formati. Restringilo con "data" in event.payload, come fa il ricevitore, oppure passa il tipo atteso: verifyWebhook<components["schemas"]["TaskRecordEnvelope"]>(…) con import type { components } from "@spicyapi/sdk".

Gestire gli errori

Le cose possono andare storte in due modi diversi, e vanno gestiti in modo diverso:

  1. Una chiamata fallisce. L’SDK genera un errore: la chiave era sbagliata, il saldo era troppo basso, la rete è caduta. Intercettalo con try / catch.
  2. Un’attività fallisce. La chiamata ha funzionato, ma la generazione no. Non viene generata alcuna eccezione: l’attività restituita ha state: "failed" e un errorCode. Le attività fallite o scadute normalmente liberano l’intero importo accantonato. Alcuni rifiuti di contenuto vengono addebitati al preventivo accettato, solo se la pagina del modello indica esplicitamente questa eccezione. Controlla settled e cost per conoscere l’importo finale.

Le classi di errore

ClasseQuando la incontriCampi utili
SpicyApiErrorSpicyAPI ha risposto con un errore, per esempio una chiave rifiutata o un saldo insufficientecode (il codice applicativo su cui basare le decisioni, per esempio 40201), status (stato HTTP), message, requestId, retryAfterSeconds, rateLimitLimit, rateLimitRemaining
SpicyTransportErrorNon è arrivata alcuna risposta utilizzabile: errore di rete, problema DNS o risposta più grande di 4 MiBmessage, cause (l’errore sottostante)
SpicyTimeoutErrorUna singola richiesta è durata più di timeoutMs (o di uploadTimeoutMs). È un tipo di SpicyTransportErrortimeoutMs
SpicyWaitTimeoutErrorrun o waitForTask hanno esaurito il tempo. L’attività è ancora in corsotaskId, timeoutMs, lastTask
SpicyUploadErrorL’archivio ha rifiutato un file caricato, oppure il file era troppo grandestatus
SpicyWebhookVerificationErrorUn webhook non ha superato la verificareason
TypeErrorLa chiamata in sé non era valida: nessuna chiave impostata, un’opzione fuori intervallo, un ID vuoto. Non è stato inviato nullamessage

Decidi che cosa fare in base a code, mai in base a message: i messaggi sono pensati per le persone, la loro formulazione può cambiare e seguono l’impostazione della lingua degli errori del tuo account. Indica sempre il requestId quando contatti l’assistenza: ci permette di trovare la richiesta esatta.

handle-errors.mjs
import { randomUUID } from "node:crypto";
import {
  SpicyApiError,
  SpicyClient,
  SpicyTransportError,
  SpicyWaitTimeoutError,
} from "@spicyapi/sdk";

const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // un ID reale preso da listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

const idempotencyKey = randomUUID(); // conservala: per riprovare ti serve la stessa

try {
  const task = await client.run(
    { model: MODEL_ID, input: { prompt: "A paper lantern on a rainy street" } },
    { idempotencyKey, onAccepted: ({ taskId }) => console.log("Task ID:", taskId) },
  );
  if (task.state === "succeeded") console.log("Done:", task.output?.assets?.[0]?.url);
  else console.log(`Finished as ${task.state} (${task.errorCode}). Cost: ${task.cost}; settled: ${task.settled}.`);
} catch (error) {
  if (error instanceof SpicyWaitTimeoutError) {
    // La tua attesa è finita, l’attività no. Controllala più tardi con lo stesso ID.
    console.log(`Still running: ${error.taskId}. Call waitForTask() again later.`);
  } else if (error instanceof SpicyApiError) {
    console.log(`SpicyAPI said no: ${error.code} (HTTP ${error.status}) ${error.message}`);
    console.log(`Quote this if you contact support: ${error.requestId}`);
    if (error.code === 40201) console.log("Your balance is too low. Top up in the console.");
    if (error.code === 40202) console.log("A spend cap was reached. Raise it on the API keys page.");
    if (error.code === 429) console.log(`Too many requests. Wait ${error.retryAfterSeconds ?? 1} s.`);
  } else if (error instanceof SpicyTransportError) {
    // Errore di rete o timeout locale: l’attività può esistere oppure no.
    // Esegui di nuovo la stessa richiesta con la STESSA idempotencyKey per scoprirlo in sicurezza.
    console.log(`Network problem (${error.message}). Retry with key ${idempotencyKey}.`);
  } else {
    throw error; // un TypeError qui è un errore nella chiamata stessa, per esempio una chiave mancante
  }
}

Codici di errore comuni

error.codeSignificatoConviene riprovare?
400Richiesta non valida, di solito per un campo di input che il modello non accettaNo. Correggi la richiesta
401Chiave mancante, malformata, revocata o scadutaNo. Controlla la chiave
40201Saldo insufficiente per l’accantonamentoNo. Prima ricarica
40202È stato raggiunto un limite di spesaNo. Alza il limite, oppure aspetta l’azzeramento giornaliero o mensile
40301Questa chiave non può usare questo modelloNo. Modifica l’elenco dei modelli della chiave
404Per questa chiave non esiste un modello o un’attività del genereNo
409Conflitto con lo stato attuale, per esempio una chiave di idempotenza riutilizzata per un lavoro diversoDipende; vedi Errori
40901Preventivo scaduto o prezzo cambiatoChiedi un nuovo preventivo, poi conferma
429Troppe richiesteSì, dopo retryAfterSeconds. Per le letture l’SDK riprova già da solo
500Qualcosa si è rotto dalla parte di SpicyAPISì, dopo una pausa
50301Il modello al momento non è disponibileSì, più tardi, oppure scegli un altro modello

Quando un’attività fallisce, task.errorCode è uno tra invalid_request, unsupported_combination, content_rejected, rate_limited, upstream_unavailable, generation_failed, timeout, invalid_asset e upstream_failed. retryTask invia di nuovo l’input originale senza modifiche, quindi scegli il passo successivo in base al codice:

task.errorCodeChe cosa fare
rate_limited, upstream_unavailable, timeout, upstream_failedAspetta, poi usa retryTask una volta. Se un timeout si ripete, prova con createTask una durata più breve o una risoluzione più bassa
generation_failedCambia il prompt o l’immagine e invia una nuova attività con createTask; di solito lo stesso input fallisce allo stesso modo
invalid_assetCarica di nuovo il file e invia una nuova attività con createTask usando il nuovo uri; un nuovo tentativo riutilizzerebbe il vecchio file
invalid_request, unsupported_combinationNon ripetere senza modifiche. Cambia le impostazioni, poi usa createTask
content_rejectedNon ripetere senza modifiche. Il modello scelto ha rifiutato la richiesta in base alle proprie regole: cambia il prompt o scegli un altro modello

La tabella completa è in Errori.

Conserva i tuoi risultati

  • L’url di un risultato funziona per circa 20 minuti. Chiama di nuovo getTask per averne uno nuovo.
  • I file generati vengono eliminati al massimo dopo circa 14 giorni, prima se lo prevedono il tuo account o retentionSeconds. I file di input caricati durano un giorno, i prompt archiviati 30 giorni.
  • Non appena un’attività riesce, scarica nel tuo archivio ciò che vuoi tenere. Non salvare nel database l’url temporaneo in sé. Dettagli: Conservazione e distruzione.

Ricette comuni

Le ricette usano MODEL_ID_FROM_CATALOG come segnaposto. Prima di eseguirne una, sostituiscilo con il valore model di una voce adatta del catalogo in tempo reale (listModels() o GET /api/v1/models) in cui enabled e available valgono entrambi true. Se te ne dimentichi, ogni ricetta si ferma con un messaggio chiaro.

Da immagine a video: carica prima un’immagine locale

image-to-video.mjs
import { randomUUID } from "node:crypto";
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();

// Sostituiscilo con l’ID di un modello da immagine a video preso dal catalogo in tempo reale:
// await client.listModels({ modality: "video", task: "image-to-video" })
const MODEL_ID = "MODEL_ID_FROM_CATALOG";
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

// 1. Carica l’immagine locale. Gratis. L’indirizzo spicy:// funziona per un giorno.
const file = await client.uploadFile("./photo.jpg");
console.log("Uploaded:", file.uri);

// 2. Stampa lo schema del modello: quale campo riceve l’immagine, quali campi sono obbligatori.
const { inputSchema } = await client.getModel(MODEL_ID);
console.log(JSON.stringify(inputSchema, null, 2));

// 3. Crea il video e attendilo. Un video richiede spesso diversi minuti.
const task = await client.run(
  {
    model: MODEL_ID,
    input: {
      prompt: "Slow push-in, light rain, neon reflections on wet stone",
      image_url: file.uri, // usa il nome del campo mostrato dallo schema qui sopra
    },
  },
  {
    idempotencyKey: randomUUID(),
    timeoutMs: 30 * 60_000,
    onAccepted: ({ taskId }) => console.log("Task ID (save it):", taskId),
  },
);

console.log(task.state, task.output?.assets?.[0]?.url ?? task.errorCode);

Sui nomi dei campi e sui campi obbligatori fa fede lo schema stampato; un modello video può richiedere anche una durata o una risoluzione, e spesso il prezzo dipende proprio da queste. Se la tua immagine è già online a un indirizzo HTTPS pubblico, salta il caricamento e metti quell’indirizzo direttamente nel campo dell’immagine. Se vuoi vedere il prezzo, esegui prima quoteTask con la stessa richiesta.

Eseguire un lotto con concorrenza limitata

Avviare centinaia di attività tutte insieme può farti raggiungere i limiti di spesa e il limite di richieste. Questo script esegue un elenco di prompt con al massimo due attività in corso alla volta.

batch.mjs
import { SpicyApiError, SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();

const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // un ID reale preso da listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

// Cambia il nome del lotto ogni volta che cambi i prompt.
const BATCH_NAME = "lanterns-2026-09-17";
const prompts = [
  "A paper lantern on a rainy street",
  "A paper lantern floating on a lake",
  "A paper lantern in a snowy forest",
  "A paper lantern above a night market",
];
const CONCURRENCY = 2; // quante attività possono girare contemporaneamente

const results = prompts.map((prompt) => ({ prompt, outcome: "not started", detail: "" }));
let nextIndex = 0;

async function worker() {
  while (nextIndex < prompts.length) {
    const index = nextIndex++;
    try {
      const task = await client.run(
        { model: MODEL_ID, input: { prompt: prompts[index] } },
        {
          // Stesso nome del lotto + stessa posizione = stessa chiave. Se lo script va in crash
          // e lo esegui di nuovo entro 24 ore, ti tornano le attività esistenti invece di
          // pagarle due volte.
          idempotencyKey: `${BATCH_NAME}-${index}`,
          timeoutMs: 20 * 60_000,
        },
      );
      results[index].outcome = task.state;
      results[index].detail = task.output?.assets?.[0]?.url ?? task.errorCode ?? "";
    } catch (error) {
      results[index].outcome = "error";
      results[index].detail = error instanceof Error ? error.message : String(error);
      // Saldo esaurito (40201) o limite di spesa superato (40202): smetti di avviare nuove attività.
      if (error instanceof SpicyApiError && (error.code === 40201 || error.code === 40202)) {
        nextIndex = prompts.length;
      }
    }
  }
}

await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
console.table(results);
  • Perché le chiavi sono costruite a partire dal nome del lotto. Una chiave di idempotenza legata a «questo lotto, elemento 3» fa sì che, se riesegui lo script dopo un crash, vengano riutilizzate le attività già create. Una chiave casuale a ogni esecuzione ti farebbe pagare di nuovo la stessa immagine.
  • Perché il nome del lotto deve cambiare insieme ai prompt. Riutilizzare una chiave per una richiesta diversa viene rifiutato con 409.
  • Come scegliere CONCURRENCY. Ogni attività in corso tiene accantonata una parte del tuo saldo, e ogni controllo di stato conta nel limite di richieste del tuo account. Parti da un valore basso e alzalo quando il lotto gira senza problemi.

Ricevere i risultati via webhook invece del polling

In un’app web o in un worker in background, restare in attesa dentro una richiesta è uno spreco. Crea l’attività con un callBackUrl, salva l’ID attività e lascia che sia il ricevitore a gestire il risultato quando arriva.

webhook-create.mjs
import { randomUUID } from "node:crypto";
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // un ID reale preso da listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

// In un’app reale, salva questa chiave insieme all’ordine del cliente prima della chiamata.
const idempotencyKey = randomUUID();

const accepted = await client.createTask(
  {
    model: MODEL_ID,
    input: { prompt: "A paper lantern on a rainy street" },
    callBackUrl: "https://your-app.example.com/hooks/spicy",
  },
  { idempotencyKey },
);

console.log(`Accepted ${accepted.taskId}; $${accepted.estimatedCost} is on hold.`);
// Salva subito accepted.taskId. Quando l’attività finisce, SpicyAPI chiama il tuo ricevitore.

Tieni una rete di sicurezza per le consegne che non arrivano mai, per esempio perché il tuo server è rimasto spento durante tutti i tentativi: ogni tanto chiama getTask per le attività salvate che hanno superato da un pezzo la loro deadlineAt senza ricevere un webhook, oppure sfoglia listTasks. La cronologia delle consegne dei webhook, con un pulsante per riprovare manualmente una consegna, si trova nella pagina Webhook della console.

Ripetere un’attività fallita

retry.mjs
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();

const taskId = process.argv[2]; // eseguilo così: node retry.mjs job_...
if (!taskId) throw new Error("Usage: node retry.mjs <taskId>");

// Un nuovo tentativo invia di nuovo l’input originale senza modifiche, quindi conviene solo per i fallimenti temporanei.
// Per tutti gli altri codici, modifica la richiesta e usa invece createTask.
const WORTH_ONE_RETRY = new Set(["rate_limited", "upstream_unavailable", "timeout", "upstream_failed"]);

const source = await client.getTask(taskId);

if (
  (source.state !== "failed" && source.state !== "expired") ||
  !WORTH_ONE_RETRY.has(source.errorCode ?? "")
) {
  console.log(`Not retrying: the task is ${source.state} (${source.errorCode ?? "no error code"}).`);
} else {
  // Una chiave fissa per ogni tentativo: se esegui due volte questo script, crei un solo nuovo tentativo, non due.
  const retry = await client.retryTask(source.taskId, {
    idempotencyKey: `retry-${source.taskId}-attempt-1`,
  });
  console.log(`New task ${retry.taskId}; $${retry.estimatedCost} is on hold.`);

  const finished = await client.waitForTask(retry.taskId, { timeoutMs: 20 * 60_000 });
  console.log("The retry finished as", finished.state);
}

Lo script ripete solo i fallimenti che la volta successiva possono andare diversamente (un servizio occupato o temporaneamente non disponibile, un timeout o un errore sconosciuto), e solo una volta, con una chiave di idempotenza fissa. Per invalid_asset, carica di nuovo il file e crea una nuova attività, perché il nuovo tentativo riutilizzerebbe il caricamento scaduto. Per generation_failed, cambia il prompt o l’immagine e crea una nuova attività. Per content_rejected, invalid_request e unsupported_combination, modifica la richiesta invece di ripeterla. Se fallisce anche quell’unico nuovo tentativo, fermati ed esamina l’errore; se un timeout si ripete, prova con createTask una durata più breve o una risoluzione più bassa. Vedi Errori.

Risoluzione dei problemi e domande frequenti

Codice sorgente

Il pacchetto ha licenza MIT ed è sviluppato in modo aperto su github.com/spicyapi-ai/spicy-sdk. È lì che si legge il codice dietro a ogni metodo di questa pagina, si segnala un comportamento diverso da quello descritto e si propone una modifica.

Prossimi passi

In questa pagina

Che cos’è l’SDKÈ lo strumento giusto?Che cosa non faLe parole usate in questa paginaChe cosa puoi farePrima di iniziareInstalla Node.js 22.13 o successivoCrea una cartella di progetto e installa l’SDKCrea una chiave APIMetti la chiave in una variabile d’ambienteEsegui il tuo primo fileIl tuo primo risultatoChe cosa fa ogni passaggioRiferimento APICreare un clientNuovi tentativi automaticiFermare una chiamata con signalgetStatus()listModels(options?)getModel(modelId)quoteTask(request)createTask(request, options?)run(request, options?)getTask(taskId)waitForTask(taskId, options?)retryTask(taskId, options?)listTasks(options?)getBalance()getUsage(options?)Caricare fileuploadFile(path, options?)uploadBytes(data, options)uploadBase64(value, options?)createUploadUrl e commitUploadedFilecreateDownloadUrl(taskId, key?)purgeTask(taskId)Funzioni per i webhookHelper offline e tipiWebhookPerché devi verificare ogni webhookConfigurazioneOttieni la tua chiave di firmaEsegui un ricevitore su un indirizzo HTTPS pubblicoInvia callBackUrl con le tue attivitàUn ricevitore minimoProva il ricevitore prima di andare in produzioneverifyWebhook(options)Gestire gli erroriLe classi di erroreCodici di errore comuniConserva i tuoi risultatiRicette comuniDa immagine a video: carica prima un’immagine localeEseguire un lotto con concorrenza limitataRicevere i risultati via webhook invece del pollingRipetere un’attività fallitaRisoluzione dei problemi e domande frequentiCodice sorgenteProssimi passi