spicyapiDocumentazione
Sviluppare con l’API
Contenuto principale

Media

Caricamenti diretti con URL firmati per i media di riferimento e link di download temporanei per i risultati.

Molti modelli ricevono come input un'immagine, un video o una clip audio (un riferimento) e un modello multimediale restituisce un file (un artefatto). Questa pagina copre entrambe le direzioni:

  • Input di riferimento: passa un URL HTTPS pubblico, una piccola immagine inserita direttamente nella richiesta oppure un file che carichi. Per caricare, richiedi un ticket, invia il file con PUT direttamente nello storage a oggetti e conferma il caricamento; la conferma restituisce un URI spicy:// da inserire nell'input dell'attività.
  • Download degli artefatti: recupera l'output.assets[].url firmato restituito con il risultato. Non ogni modello produce un file: un endpoint con risultato testuale risponde in output.text e non ha alcun assets, quindi niente da scaricare e nessun link che possa scadere. Consulta Struttura dell'output.

Ticket di caricamento e download degli artefatti sono sotto /api/v1/common/; le conferme dei caricamenti usano /api/v1/files/{fileId}/commit. Caricare è gratuito; paghi soltanto l'attività che usa il file.

Media HTTPS esistenti

Passa direttamente un URL HTTPS pubblico di immagine, video o audio nel campo supportato dal modello (per esempio image_url). Non servono un caricamento sulla piattaforma né una conversione in spicy://. L'URL deve restare raggiungibile senza header di autorizzazione personalizzati durante l'esecuzione dell'attività. I file locali più grandi usano il percorso di caricamento descritto sotto.

L'URL deve restituire il file stesso. Un link a una pagina di condivisione (drive cloud, album fotografico, post sui social) restituisce una pagina web invece dell'immagine e l'attività fallisce con invalid_asset. In quel caso, scarica il file e caricalo.

Base64 e file locali

Le immagini piccole possono essere passate direttamente nei campi immagine dichiarati dallo schema, per esempio image_url: "data:image/png;base64,...". Includi il tipo MIME e usa Base64 standard. Base64 senza prefisso, codifica URL-safe, spazi, SVG e audio o video inline non sono supportati da questo percorso.

  • JPEG, PNG, WebP e GIF: al massimo 1 MiB decodificato per immagine, 8192 pixel per lato e 8.388.608 pixel per immagine.
  • Al massimo 16 immagini e 16.777.216 pixel complessivi per richiesta. L'intero corpo JSON deve rientrare in 2 MiB, incluso l'aumento di dimensione di circa un terzo dovuto a Base64.
  • Restano validi lo schema e i limiti multimediali specifici del modello. I preventivi validano senza caricare né riservare fondi. La creazione conserva le immagini come caricamenti legati all'account, che durano un giorno come ogni altro caricamento; l'input dell'attività contiene URI di file invece di ripetere i byte Base64.

Per file più grandi, l'SDK gestisce ticket, PUT e conferma:

import { SpicyClient } from '@spicyapi/sdk';

const client = new SpicyClient();
const file = await client.uploadFile('./reference.png');
// With existing Base64, use uploadBase64(dataUri),
// or uploadBase64(rawBase64, { contentType: 'image/png' }).
console.log(file.uri);

uploadBase64 è una funzione di caricamento file che passa attraverso la stessa API di caricamento e i suoi limiti descritti sotto. Il limite di 1 MiB per le piccole immagini inline non si applica a questa funzione; i singoli modelli devono comunque supportare il tipo di media scelto. I media HTTPS pubblici esistenti non richiedono una funzione di caricamento.

1. Caricamento diretto dei riferimenti

Per un file di riferimento locale che non è già ospitato a un URL HTTPS pubblico, il caricamento richiede quattro passaggi. I comandi curl sotto vanno eseguiti uno dopo l'altro nella stessa finestra del terminale (macOS o Linux, con jq installato): ogni passaggio salva in una variabile shell ciò che serve al successivo. Su Windows usa l'SDK oppure l'esempio Python alla fine di questa sezione.

Richiedere un ticket

POST /api/v1/common/upload-url

Proprietà

Tipo

Non c'è un campo per il nome del file, ed è voluto

Le chiavi degli oggetti vengono sempre generate sul server. Permettere a chi chiama di scegliere un percorso consentirebbe a chiunque di creare un URL di caricamento verso l'oggetto di qualcun altro e sovrascriverlo.

Limiti di caricamento

contentTypeEstensioneDimensione massimaDurata massima
image/jpeg.jpg10 MiB—
image/png.png10 MiB—
image/webp.webp10 MiB—
image/gif.gif10 MiB—
audio/wav.wav90 MiB600 secondi
audio/mpeg.mp390 MiB600 secondi
video/webm.webm90 MiB600 secondi
video/mp4.mp490 MiB600 secondi
video/quicktime.mov90 MiB600 secondi
application/octet-stream.safetensors (pesi LoRA)2 GiB—

.mov (il formato registrato di default dalla fotocamera dell'iPhone) viene accettato e convertito in MP4 durante l'importazione, quindi il modello riceve sempre MP4. È accettato dal caricatore web e dall'API di caricamento; CLI, SDK e server MCP deducono il tipo dall'estensione e non lo elencano ancora, quindi lì passa esplicitamente contentType: video/quicktime.

I pesi LoRA si caricano come application/octet-stream: un file .safetensors fino a 2 GiB (il caricatore web arriva a 90 MiB). Il ticket resta valido due ore invece di 20 minuti, e il commit verifica che il file sia un safetensors completo. Metti l'URI restituito in loras[].path; un file di pesi non si può usare in nessun altro campo. Un file che per essere scaricato richiede l'accesso, come uno in un repository Hugging Face privato, si può importare dal suo link: vedi Pesi dell'adattatore.

Un modello può accettare meno: rispetta l'x-ui.max_size_mb del suo schema e considera maxBytes del ticket come il limite massimo autorevole per questo caricamento.

curl
FILE=reference.png            # the file to upload
CONTENT_TYPE=image/png        # must match the file type in the table above
BYTES=$(wc -c < "$FILE" | tr -d ' ')

TICKET=$(curl -s -X POST https://api.spicyapi.ai/api/v1/common/upload-url \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  --data "$(jq -n --arg type "$CONTENT_TYPE" --argjson bytes "$BYTES" '{contentType: $type, bytes: $bytes}')")
echo "$TICKET" | jq .

UPLOAD_URL=$(echo "$TICKET" | jq -r .data.uploadUrl)
FILE_ID=$(echo "$TICKET" | jq -r .data.fileId)

Dovresti vedere una risposta simile alla seguente; UPLOAD_URL e FILE_ID ora contengono il suo uploadUrl e fileId:

{
  "code": 200,
  "msg": "success",
  "data": {
    "fileId": "fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "key": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "uploadUrl": "https://<account-id>.r2.cloudflarestorage.com/spicy-production-inputs/tmp/…?X-Amz-Signature=…",
    "method": "PUT",
    "headers": {
      "Content-Type": "image/png",
      "Content-Length": "402118"
    },
    "expiresAt": "2026-08-28T09:32:11Z",
    "maxBytes": 10485760
  }
}

expiresAt è fra circa 20 minuti. Sia PUT sia la conferma devono finire prima; dopo, la conferma restituisce 409 e serve un nuovo ticket. Richiedi il ticket subito prima di caricare, non in anticipo.

Inviare il file con PUT

Invia i byte con PUT a uploadUrl. I client non browser come curl, Node.js e Python devono inviare ogni header di headers esattamente come fornito. I client browser devono gestire Content-Length come descritto sotto.

Omettere un header rende la firma non corrispondente

Sia Content-Type sia Content-Length sono firmati. Cambia un byte di uno dei due e lo storage rifiuta il caricamento, con un 403 proveniente dallo storage anziché da noi: per questo può sembrare incomprensibile.

I browser definiscono Content-Length come header di richiesta vietato, quindi il codice applicativo non può impostarlo tramite fetch o XMLHttpRequest. Nel browser invia un File o Blob con dimensione esattamente uguale ai bytes del ticket e lascia che lo user agent generi Content-Length dal corpo; imposta personalmente soltanto gli altri header del ticket. La risposta continua a elencare Content-Length per i client non browser e affinché il codice browser possa verificare la dimensione prima di inviare.

Non inviare un header Authorization in questa richiesta. L'URL firmato è la credenziale; un header di autenticazione aggiuntivo porta alcune implementazioni dello storage a rifiutarlo.

curl
# Prints only the HTTP status. Expect 200.
curl -s -o /dev/null -w "%{http_code}\n" -X PUT "$UPLOAD_URL" \
  -H "Content-Type: $CONTENT_TYPE" \
  -H "Content-Length: $BYTES" \
  --data-binary @"$FILE"
Browser
const uploadHeaders = new Headers(ticket.data.headers);
uploadHeaders.delete('Content-Length'); // the browser derives it from File/Blob

if (file.size !== Number(ticket.data.headers['Content-Length'])) {
  throw new Error('file size does not match the upload ticket');
}

await fetch(ticket.data.uploadUrl, {
  method: ticket.data.method,
  headers: uploadHeaders,
  body: file, // File or Blob; do not turn it into text, Base64 or FormData
});

Confermare il caricamento

Un PUT riuscito significa soltanto che è stato scritto un oggetto temporaneo. Prosegui con il fileId del primo passaggio:

curl
curl -s -X POST "https://api.spicyapi.ai/api/v1/files/$FILE_ID/commit" \
  -H "Authorization: Bearer $SPICY_API_KEY" | jq .

Aspettati "status": "ready" e un uri che inizia con spicy://:

{
  "code": 200,
  "msg": "success",
  "data": {
    "fileId": "fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "status": "ready",
    "bytes": 402118,
    "contentType": "image/png",
    "sha256": "a91f4c2e5b7d16809e3f2a4c8d0b61e7c4a890e5d2f71b3c609ae8427d15f0c6",
    "uri": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "expiresAt": "2026-08-29T09:12:11Z"
  },
  "request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}

La conferma confronta dimensione e tipo di media archiviato con il ticket, verifica la firma del media, calcola SHA-256 e copia i byte in un oggetto privato che il vecchio ticket non può sovrascrivere. Per audio e video può anche restituire durationSeconds come stringa decimale, più width e height numerici. Ripetere una conferma riuscita è idempotente.

RispostaSignificatoCorrezione
400, code: 40003Dimensione, tipo o formato del file archiviato non corrispondono a ciò che hai dichiaratoRichiedi un nuovo ticket con contentType corretto e bytes esatti, poi carica nuovamente
400Il file non c'è ancora: il PUT non è finito o è fallitoControlla che PUT abbia restituito 200, poi conferma di nuovo
404Nessun fileId corrispondente su questo accountUsa il fileId del tuo ticket
409Il ticket o il file caricato è scaduto oppure una conferma precedente ha rifiutato questo fileRicomincia dal passaggio 1
409 / 429La verifica del file è già in corso oppure il verificatore è occupatoAttendi un secondo e conferma di nuovo

Un PUT deve essere seguito dalla conferma

Un file non confermato non può essere usato per creare un'attività. Non usare il percorso temporaneo di storage del ticket e non usare il campo di compatibilità key finché la conferma non riesce.

Inserire l'URI spicy:// nell'input

Come campo di riferimento usa soltanto data.uri della risposta di conferma, non uploadUrl, una chiave di oggetto temporaneo o il semplice fileId.

Il campo che accetta un file dipende dal modello. Nell'inputSchema del modello, un campo file ha x-ui.widget impostato su upload (un file) oppure multi-upload (un elenco: metti l'URI in un array). x-ui.accept elenca i tipi di file e x-ui.max_size_mb la dimensione accettata dal modello. Consulta Catalogo dei modelli e schemi.

MODEL_ID_FROM_CATALOG e i restanti campi di input sotto sono segnaposto strutturali; seleziona il model esatto e un input valido per lo schema dalla stessa voce del catalogo autenticato aggiornato. L'attività è determinata dal model stesso, quindi input non contiene un campo endpoint; accetta soltanto i campi elencati nell'inputSchema del modello e un campo aggiuntivo restituisce 400:

{
  "model": "MODEL_ID_FROM_CATALOG",
  "input": {
    "prompt": "slow dolly in, rain on the window",
    "image_url": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "duration_seconds": 5
  }
}

La piattaforma risolve un URI spicy:// confermato quando esegue l'attività. Questo permette ai file caricati di restare utilizzabili mentre l'attività è in coda, senza dover gestire URL temporanei di download. Usa senza modifiche l'URI restituito dalla conferma.

Un URI non pronto, scaduto o appartenente a un altro account viene rifiutato alla creazione con 400.

Esempio completo

JavaScript
import { readFile } from 'node:fs/promises';

const BASE = 'https://api.spicyapi.ai/api/v1';
const AUTH = { Authorization: `Bearer ${process.env.SPICY_API_KEY}` };

async function uploadReference(path, contentType) {
  const file = await readFile(path);

  // 1. Get a ticket
  const ticketRes = await fetch(`${BASE}/common/upload-url`, {
    method: 'POST',
    headers: { ...AUTH, 'Content-Type': 'application/json' },
    body: JSON.stringify({ contentType, bytes: file.byteLength }),
  });
  const ticket = await ticketRes.json();
  if (ticket.code !== 200) throw new Error(`${ticket.code} ${ticket.msg}`);

  // 2. Upload directly. No Authorization here; pass the headers through as-is
  const put = await fetch(ticket.data.uploadUrl, {
    method: ticket.data.method,
    headers: ticket.data.headers,
    body: file,
  });
  if (!put.ok) throw new Error(`upload failed: ${put.status}`);

  // 3. Commit so the server verifies and freezes the uploaded bytes
  const commitRes = await fetch(
    `${BASE}/files/${encodeURIComponent(ticket.data.fileId)}/commit`,
    { method: 'POST', headers: AUTH },
  );
  const committed = await commitRes.json();
  if (committed.code !== 200) throw new Error(`${committed.code} ${committed.msg}`);

  // 4. Task input uses only the spicy:// URI returned by commit
  return committed.data.uri;
}

console.log(await uploadReference('./reference.png', 'image/png'));
Python
import os, requests

BASE = "https://api.spicyapi.ai/api/v1"
AUTH = {"Authorization": f"Bearer {os.environ['SPICY_API_KEY']}"}


def upload_reference(path: str, content_type: str) -> str:
    with open(path, "rb") as f:
        data = f.read()

    # 1. Get a ticket
    ticket = requests.post(
        f"{BASE}/common/upload-url",
        headers={**AUTH, "Content-Type": "application/json"},
        json={"contentType": content_type, "bytes": len(data)},
    ).json()
    if ticket["code"] != 200:
        raise RuntimeError(f'{ticket["code"]} {ticket["msg"]}')

    # 2. Upload directly. No Authorization; pass the headers through as-is
    put = requests.put(ticket["data"]["uploadUrl"], headers=ticket["data"]["headers"], data=data)
    put.raise_for_status()

    # 3. Commit so the server verifies and freezes the uploaded bytes
    committed = requests.post(
        f'{BASE}/files/{ticket["data"]["fileId"]}/commit',
        headers=AUTH,
    ).json()
    if committed["code"] != 200:
        raise RuntimeError(f'{committed["code"]} {committed["msg"]}')

    # 4. Task input uses only the spicy:// URI returned by commit
    return committed["data"]["uri"]


print(upload_reference("reference.png", "image/png"))

Un limite di frequenza aggiuntivo

Oltre al limite generale dell'API pubblica, upload-url ha una propria protezione generale di 1.000 richieste ogni 10 secondi contro autorizzazioni di scrittura nello storage abusive o fuori controllo.

Il costo qui è asimmetrico: firmare un ticket è puro calcolo locale per noi, ma ogni ticket è un'autorizzazione di scrittura che non passa più attraverso di noi. Una persona che sceglie file non può raggiungere quella velocità e uno script batch che carica qualche decina di riferimenti rientra comunque comodamente nel limite.

2. Scaricare i risultati

I risultati pronti includono già output.assets[].url ed expiresAt nel polling e nei callback v2. Recupera direttamente quell'URL senza chiave API; download-url sotto è facoltativo. I link normalmente durano 20 minuti e non superano mai la finestra di conservazione dell'attività, limitata a circa 14 giorni o meno se l'hai accorciata.

Per conservare un risultato, salvalo nel tuo storage mentre il link è valido. Per esempio:

curl -L -o result.png "PASTE_THE_OUTPUT_URL_HERE"

Non aggiungere un header Authorization a questa richiesta. Se il link è scaduto (lo storage risponde 403), interroga nuovamente l'attività con recordInfo per ottenerne uno nuovo.

Aggiornamento facoltativo del link di download

POST /api/v1/common/download-url

Proprietà

Tipo

curl
curl -X POST https://api.spicyapi.ai/api/v1/common/download-url \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "key": "tasks/2026/08/28/job_01k3m8x9q2z4v7n5p6r8s0t1w3/9f3c1d0a7b4e2f68c5a1d3e9b0472fa1.png"
  }'
{
  "code": 200,
  "msg": "success",
  "data": {
    "key": "tasks/2026/08/28/job_01k3m8x9q2z4v7n5p6r8s0t1w3/9f3c1d0a7b4e2f68c5a1d3e9b0472fa1.png",
    "url": "https://<account-id>.r2.cloudflarestorage.com/spicy-production-results/tasks/…?X-Amz-Signature=…",
    "expiresAt": "2026-08-28T09:32:11Z"
  }
}

Per un'attività con vari artefatti, chiama una volta per key. Come recordInfo, funziona soltanto con la chiave API che ha creato l'attività.

Un URL firmato è una stringa che attesta da sé l'autorizzazione: lo storage non chiede nuovamente chi la possieda. Quindi:

  • Non conservarlo nel database e non inserirlo in una cache di lunga durata che servi ai browser. Firmane uno quando serve: la firma è calcolo locale ed è di fatto gratuita.
  • Scarica tutto ciò che vuoi conservare. Gli artefatti restano da noi al massimo circa 14 giorni, meno se accorci il periodo; consulta Conservazione e distruzione.
SituazioneCosa vediCosa fare
Copia nel nostro storage ancora in corsoLa risorsa mostra pending: true in recordInfo; download-url senza key restituisce 409 (con un key esplicito restituisce 404 finché la copia non finisce)Attendi qualche secondo e interroga di nuovo
Copia fallita definitivamenteLa risorsa mostra unavailable: true e non ha keyAspettare non aiuta: genera nuovamente
Attività non ancora riuscita404, «Questa attività non ha output scaricabile»Attendi state: "succeeded"
Periodo di conservazione trascorso oppure contenuto distrutto da teHTTP 410 con code: 404, «Questo output è scaduto o è stato eliminato»Perso definitivamente: genera nuovamente

Un'attività non tua o un key che non appartiene all'attività restituiscono entrambi 404. Non distinguiamo «non esiste» da «appartiene a qualcun altro», perché la differenza potrebbe essere usata per enumerare gli ID di attività altrui.

Libreria di asset

Alcuni file li usi di continuo: un ritratto di personaggio, un riferimento di stile, una foto di prodotto, un campione di voce. La libreria di asset li conserva finché non li elimini e dà a ciascuno un riferimento stabile, spicy://a/<id>. Inserisci questo riferimento in qualsiasi campo media di qualsiasi input di attività, come faresti con un caricamento spicy://f/, ma senza la scadenza di un giorno.

  • Privata al tuo spazio di lavoro. Un riferimento spicy://a/ funziona solo nelle richieste fatte con chiavi dello stesso spazio di lavoro. Una chiave personale raggiunge la tua libreria personale; una chiave creata in uno spazio di lavoro di team raggiunge la libreria condivisa di quel team, e le due non si collegano mai (vedi sotto).
  • Conservata finché non viene eliminata. I risultati generati vengono ripuliti dopo circa 14 giorni e i caricamenti dopo 1 giorno; gli asset salvati restano finché non li elimini. Una libreria personale scompare quando chiudi l'account, una libreria di team insieme al suo team.
  • Non è un servizio di hosting di file. L'API ti dà il riferimento spicy://a/<id> e una previewUrl firmata solo per te, valida al massimo 20 minuti. Non esiste alcun link pubblico o permanente, e un riferimento non può diventarlo. Conserva uri, mai previewUrl.

Librerie personale e di team

La chiave decide la libreria. Una chiave creata nel tuo spazio personale raggiunge la tua libreria personale. Una chiave creata in uno spazio di lavoro di team, da qualsiasi membro, raggiunge la libreria condivisa di quel team. Una chiave di team non raggiunge mai la libreria personale di nessuno, una chiave personale non raggiunge mai una libreria di team, e un riferimento spicy://a/ dell'altra libreria viene rifiutato come se non esistesse.

Ciò che una chiave di team può modificare segue il ruolo attuale del membro che l'ha creata:

RuoloSalvareRinominare o eliminareElencare, leggere e usare
owner, adminSìQualsiasi assetSì
developerSìSolo quelli che ha salvatoSì
billing, readonlyNoNoSì

Se quel membro ha lasciato il team, la chiave riceve 403 con data.reason pari a asset_workspace_forbidden. Gli asset non indicano chi li ha salvati; vedi soltanto se puoi modificarne uno.

Salvare un file

POST /api/v1/assets copia un file nella tua libreria. L'header Idempotency-Key è obbligatorio. Scegli una delle tre origini:

sourceCampiNote
task_outputtaskId, e key oppure indexUna delle tue attività riuscite. index parte da 0, nell'ordine di output.assets in recordInfo.
uploadfileIdUn caricamento confermato: fil_… oppure spicy://f/fil_….
urlurlUn indirizzo http o https pubblico sulla porta 80 o 443, senza nome utente e password. Il file viene scaricato una volta e il tipo viene letto dal file stesso; sono accettati solo immagini, video e audio.

name è facoltativo (fino a 80 caratteri; predefinito Saved asset). Salvando di nuovo la stessa origine ottieni l'asset che hai già, senza occupare altro spazio. Dimensione, dimensioni dell'immagine e durata li misuriamo noi.

curl
curl -X POST https://api.spicyapi.ai/api/v1/assets \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "task_output",
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "index": 0,
    "name": "Hero portrait"
  }'
{
  "code": 200,
  "msg": "success",
  "data": {
    "id": "ast_01k3m9c4w8r2n6t5v1x7y0z3ab",
    "uri": "spicy://a/ast_01k3m9c4w8r2n6t5v1x7y0z3ab",
    "name": "Hero portrait",
    "kind": "image",
    "contentType": "image/png",
    "bytes": 1843201,
    "width": 1536,
    "height": 1024,
    "source": "task_output",
    "sourceId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "createdAt": "2026-10-11T08:20:11Z",
    "updatedAt": "2026-10-11T08:20:11Z",
    "previewUrl": "https://…",
    "previewUrlExpiresAt": "2026-10-11T08:40:11Z"
  }
}

Usarlo in un'attività

Inserisci data.uri in qualsiasi campo file che l'inputSchema del modello contrassegna con x-ui.widget pari a upload o multi-upload:

{
  "model": "MODEL_ID_FROM_CATALOG",
  "input": {
    "prompt": "slow dolly in, rain on the window",
    "image_url": "spicy://a/ast_01k3m9c4w8r2n6t5v1x7y0z3ab",
    "duration_seconds": 5
  }
}

Un riferimento eliminato, o presente in una libreria che questa chiave non raggiunge (quella di un altro account o dell'altro spazio di lavoro), viene rifiutato alla creazione dell'attività, esattamente come un ID sconosciuto.

Elencare, rinominare ed eliminare

GET /api/v1/assets restituisce i tuoi asset in items, più un nextCursor che manca nell'ultima pagina. Filtra con kind (image, video o audio), source (separate da virgole: task_output,upload,url), q (ricerca nel nome, senza distinzione tra maiuscole e minuscole), favorite (true solo per i preferiti, false per tutto il resto) e collectionId (una cartella, vedi sotto). Ordina con sort: recent (predefinito, dal più recente), oldest, name (dalla A alla Z, senza distinzione tra maiuscole e minuscole) o size (dal più grande). limit va da 1 a 100 e il valore predefinito è 100. Per la pagina successiva, passa il nextCursor precedente come cursor; un cursore funziona solo con il sort da cui proviene e uno di un altro ordinamento viene rifiutato con 400.

curl
curl "https://api.spicyapi.ai/api/v1/assets?kind=image&q=portrait&sort=name&limit=20" \
  -H "Authorization: Bearer $SPICY_API_KEY"

curl -X PATCH https://api.spicyapi.ai/api/v1/assets/ast_01k3m9c4w8r2n6t5v1x7y0z3ab \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Hero portrait v2"}'

curl -X DELETE https://api.spicyapi.ai/api/v1/assets/ast_01k3m9c4w8r2n6t5v1x7y0z3ab \
  -H "Authorization: Bearer $SPICY_API_KEY"

GET /api/v1/assets/{id} restituisce un asset con una previewUrl nuova. PATCH cambia il nome, il flag favorite o entrambi (invia solo ciò che vuoi modificare); l'uri resta lo stesso. Con DELETE l'asset scompare subito e il suo uri smette di funzionare nelle nuove richieste. Entrambe le chiamate si possono ripetere senza rischi e non richiedono Idempotency-Key. Un asset sconosciuto e quello di un altro account restituiscono lo stesso 404.

Preferiti, cartelle e modello di origine

Ogni asset ha un flag favorite. Lo imposti con PATCH e {"favorite": true}. In una libreria di team il contrassegno appartiene alla libreria, quindi tutti vedono gli stessi preferiti. Le cartelle (nell'API si chiamano collezioni) raggruppano gli asset senza copiarli, e un asset può stare in più cartelle. Le cartelle si creano e si gestiscono nella pagina Assets dell'app web; l'API non permette né di crearle né di modificarle, ma collectionId su GET /api/v1/assets elenca gli asset di una cartella (404 se la cartella non è nella libreria raggiunta dalla tua chiave).

Ogni asset riporta anche model, il nome pubblico del modello che lo ha generato, e modelName, il suo nome visualizzato. Per i caricamenti e i link entrambi sono stringhe vuote.

Personaggi

Un personaggio è un nome più un insieme ordinato di asset immagine della stessa libreria: fino a 100 immagini e una nota facoltativa di massimo 280 caratteri. Le immagini sono gli asset stessi e non si copia nulla. Se elimini un personaggio, le sue immagini restano; se elimini un'immagine, sparisce da tutti i personaggi. I personaggi si creano e si modificano nella pagina Assets dell'app web o in Studio. Non hanno uno spazio proprio: conta solo il tuo piano della libreria.

L'API legge soltanto i personaggi:

  • GET /api/v1/assets/entities li elenca, dal più recente. Con assetId restano solo i personaggi che contengono quell'asset, limit va da 1 a 100 (50 per impostazione predefinita) e cursor riprende dal nextCursor precedente.
  • GET /api/v1/assets/entities/{id} restituisce un personaggio. Un personaggio sconosciuto, quello di un altro account e quello di una libreria che la tua chiave non raggiunge restituiscono lo stesso 404.

Ogni personaggio ha id, kind (sempre character), name, note, cover (l'immagine di copertina come asset, oppure null finché non ha immagini), assets (le immagini in ordine, ciascuna con il suo uri e una previewUrl privata), count, createdAt e updatedAt. La libreria la decide la chiave, come per gli asset, e la lettura dei personaggi condivide con quella degli asset il limite di 60 richieste al minuto.

Per usare un personaggio, inserisci le sue immagini, in quell'ordine, come riferimenti spicy://a/ nel campo delle immagini di riferimento del modello e richiamale nel prompt come descrive la pagina di quel modello. I personaggi un tempo si chiamavano persona e stavano in una libreria separata; ora fanno parte della libreria di asset. I riferimenti spicy://p/ esistenti continuano a funzionare, solo nella tua libreria personale.

Quando aggiungi a un personaggio un'immagine che hai caricato tu (e non un risultato generato), confermi di averne i diritti e, se mostra una persona reale, di avere il suo consenso.

Piani di archiviazione

L'archiviazione si sblocca automaticamente con le tue ricariche cumulative e può solo aumentare; una modifica alla tabella non elimina mai gli asset che hai già salvato. Per il tuo account fa fede GET /api/v1/assets/usage: il tuo piano, quanto hai archiviato, i limiti, il piano successivo e quanto devi ancora ricaricare per raggiungerlo, e quanti asset hai creato oggi. La tabella mostra i valori predefiniti attuali.

L'archiviazione appartiene al proprietario: le sue ricariche cumulative determinano il piano, e la sua libreria personale insieme a tutte le librerie di team che possiede condividono un'unica quota. usage copre l'intera quota e include un oggetto scope (workspaceId, personal, role, canWrite, canTopUp) che indica quale libreria raggiunge la tua chiave e cosa può fare lì. Quando canTopUp è false (una chiave di team non creata dal proprietario del team), lifetimeTopupUsd e topupToNextTierUsd vengono omessi, e una libreria piena ti invita a contattare il proprietario del team invece di indicare un importo.

PianoRicariche cumulativeArchiviazioneElementiPer file
Freenessunanessuna: non si può salvare nulla di nuovo0non applicabile
Plusda $10500 MB200100 MB
Proda $1001 GB400100 MB
Studioda $2502 GB800100 MB
Maxda $1.00010 GB4.000100 MB

Senza alcuna ricarica non puoi salvare nulla di nuovo, ma gli asset già salvati restano utilizzabili. Il limite di 100 MB per file vale per gli output delle attività e per i link. Un file che arriva da un caricamento resta soggetto ai limiti del caricamento stesso (immagini 10 MiB, audio e video 90 MiB).

Limiti di frequenza ed errori

  • Lettura (elenco, consultazione, usage): 60 richieste al minuto. Salvataggio: 30 richieste al minuto e 200 nuovi asset per giorno UTC. Eliminare un asset non restituisce la quota giornaliera. Quando raggiungi un limite ricevi 429 con l'header Retry-After.
  • Una libreria piena, un'archiviazione non ancora sbloccata o un file oltre il limite per file restituiscono 400 con invalid_request. Il messaggio indica il tuo piano attuale e quanto devi ricaricare per sbloccare il successivo.
  • 403 con data.reason pari a asset_workspace_forbidden quando la chiave non raggiunge la libreria, oppure asset_permission_denied quando il tuo ruolo nel team non consente la modifica. 503 quando l'archiviazione della piattaforma è temporaneamente piena; riprova più tardi.

Un errore su questi endpoint può portare un data.reason leggibile da macchina: asset_library_locked, asset_library_full, asset_file_too_large, asset_daily_limit, asset_storage_full, asset_permission_denied o asset_workspace_forbidden. code, lo stato HTTP e errorCode non cambiano con o senza motivo, e il motivo compare solo sugli endpoint della libreria di asset; il livello compatibile con OpenAI e gli altri livelli di compatibilità mantengono le proprie forme di errore. Tratta un valore sconosciuto come un motivo assente. Vedi Codici di motivo della libreria di asset.

L'app web ha anche un interruttore per il salvataggio automatico nella libreria. Vale solo per le generazioni che avvii nell'app web; quelle avviate tramite API non vengono mai salvate automaticamente, quindi chiama POST /api/v1/assets per quelle che vuoi tenere.

Conservazione dei caricamenti

Un URI di riferimento caricato viene conservato per 1 giorno dalla conferma riuscita, un limite massimo della piattaforma che puoi accorciare o sostituire con distruzione al completamento su Conservazione e distruzione, ed è utilizzabile soltanto fino all'expiresAt restituito dalla conferma. L'URI diventa inutilizzabile alla scadenza, anche per un nuovo tentativo di un'attività che lo referenzia. L'eliminazione prevista dal ciclo di vita dello storage può poi ritardare fino a 24 ore, ma quella finestra di pulizia non prolunga l'utilizzabilità dell'URI. Se ti serve nuovamente lo stesso riferimento, conserva una tua copia e caricalo di nuovo.

In questa pagina