spicyapiDocumentation
Développer avec l’API
Contenu principal

Médias

Envois directs présignés des médias de référence et liens de téléchargement temporaires pour les résultats.

De nombreux modèles prennent une image, une vidéo ou un extrait audio en entrée, une référence, et un modèle média renvoie un fichier, un résultat. Cette page couvre les deux sens :

  • Entrées de référence : transmettez une URL HTTPS publique, une petite image intégrée directement à la requête ou un fichier envoyé. Pour un envoi, demandez un ticket, utilisez PUT pour envoyer directement le fichier au stockage d’objets, puis validez-le. La validation renvoie une URI spicy:// à placer dans l’entrée de tâche.
  • Téléchargement des résultats : récupérez l’output.assets[].url signée renvoyée avec le résultat. Tous les modèles ne produisent pas de fichier : un point de terminaison textuel répond dans output.text sans aucun assets, donc sans téléchargement ni lien susceptible d’expirer. Voir Forme des sorties.

Les tickets d’envoi et téléchargements de résultats se trouvent sous /api/v1/common/ ; la validation des envois utilise /api/v1/files/{fileId}/commit. L’envoi est gratuit ; vous payez uniquement la tâche qui utilise le fichier.

Médias HTTPS existants

Transmettez directement une URL HTTPS publique d’image, de vidéo ou d’audio dans le champ pris en charge par le modèle, par exemple image_url. Aucun envoi sur la plateforme ni conversion spicy:// n’est nécessaire. L’URL doit rester accessible sans en-tête d’autorisation personnalisé pendant la tâche. Les fichiers locaux plus volumineux suivent la procédure ci-dessous.

L’URL doit renvoyer le fichier lui-même. Un lien de page de partage, qu’il s’agisse d’un drive cloud, d’un album ou d’une publication sociale, renvoie une page web plutôt que l’image, et la tâche échoue avec invalid_asset. Téléchargez alors le fichier et envoyez-le à la place.

Base64 et fichiers locaux

Les petites images peuvent être transmises directement dans les champs d’image déclarés par le schéma, par exemple image_url: "data:image/png;base64,...". Incluez le type MIME et utilisez le Base64 standard. Le Base64 sans préfixe, l’encodage URL-safe, les espaces, le SVG et l’audio ou la vidéo intégrés ne sont pas pris en charge par cette voie.

  • JPEG, PNG, WebP et GIF : au maximum 1 MiB décodé par image, 8192 pixels par côté et 8 388 608 pixels par image.
  • Au maximum 16 images et 16 777 216 pixels cumulés par requête. Le corps JSON entier doit tenir dans 2 MiB, y compris le surplus d’environ un tiers dû au Base64.
  • Les limites de schéma et de média du modèle restent applicables. Les devis valident sans envoyer de fichier ni réserver de fonds. La création stocke les images comme fichiers envoyés liés au compte, conservés un jour comme les autres envois ; l’entrée de tâche contient des URI de fichiers au lieu de reproduire les octets Base64.

Pour les fichiers plus volumineux, le SDK gère le ticket, le PUT et la validation :

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 est un utilitaire d’envoi qui passe par la même API et les limites ci-dessous. La limite de 1 MiB des petites images intégrées ne s’applique pas à cet utilitaire ; chaque modèle doit toutefois prendre en charge le type de média choisi. Les médias HTTPS publics existants n’en ont pas besoin.

1. Envoi direct d’une référence

Pour une référence locale non déjà hébergée à une URL HTTPS publique, l’envoi comporte quatre étapes. Les commandes curl ci-dessous s’exécutent à la suite dans la même fenêtre de terminal, sous macOS ou Linux avec jq installé : chaque étape conserve dans une variable shell ce dont la suivante a besoin. Sous Windows, utilisez le SDK ou l’exemple Python à la fin de cette section.

Demander un ticket

POST /api/v1/common/upload-url

Propriété

Type

L’absence de champ de nom de fichier est volontaire

Les clés d’objets sont toujours générées côté serveur. Laisser l’appelant choisir un chemin permettrait de créer une URL d’envoi vers l’objet d’une autre personne et de l’écraser.

Limites des envois

contentTypeExtensionTaille maximaleDurée maximale
image/jpeg.jpg10 MiB—
image/png.png10 MiB—
image/webp.webp10 MiB—
image/gif.gif10 MiB—
audio/wav.wav90 MiB600 secondes
audio/mpeg.mp390 MiB600 secondes
video/webm.webm90 MiB600 secondes
video/mp4.mp490 MiB600 secondes
video/quicktime.mov90 MiB600 secondes
application/octet-stream.safetensors (poids LoRA)2 GiB—

.mov, le format enregistré par défaut sur iPhone, est accepté et converti en MP4 à l’entrée : le modèle reçoit donc toujours du MP4. L’interface web et l’API d’envoi l’acceptent. La CLI, le SDK et le serveur MCP déduisent le type de l’extension et ne le listent pas encore ; fournissez-y explicitement contentType: video/quicktime.

Les poids LoRA sont envoyés en application/octet-stream : un fichier .safetensors de 2 GiB au maximum (l’interface web accepte jusqu’à 90 MiB). Le ticket reste valable deux heures au lieu de 20 minutes, et la validation (commit) vérifie qu’il s’agit d’un fichier safetensors complet. Placez l’URI renvoyée dans loras[].path ; un fichier de poids ne peut être utilisé dans aucun autre champ. Un fichier dont le téléchargement exige une connexion, comme un fichier d’un dépôt Hugging Face privé, peut être importé à partir de son lien : voir Poids d’adaptateur.

Un modèle peut accepter moins : respectez l’x-ui.max_size_mb de son schéma et considérez le maxBytes du ticket comme la limite faisant autorité pour cet envoi.

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)

Vous devriez recevoir une réponse de cette forme, et UPLOAD_URL ainsi que FILE_ID contiennent désormais son uploadUrl et son 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 se situe à environ 20 minutes. Le PUT et la validation doivent être terminés avant ; ensuite, la validation renvoie 409 et exige un nouveau ticket. Demandez le ticket juste avant l’envoi, pas à l’avance.

Envoyer le fichier avec PUT

Envoyez les octets par PUT à uploadUrl. Les clients hors navigateur, tels que curl, Node.js et Python, doivent transmettre chaque en-tête de headers exactement tel quel. Dans le navigateur, traitez Content-Length comme expliqué ci-dessous.

Un en-tête omis invalide la signature

Content-Type et Content-Length sont tous deux signés. Modifier un octet de l’un ou l’autre fait refuser l’envoi par le stockage d’objets avec un 403 venant du stockage, pas de nous, ce qui peut sembler déroutant.

Les navigateurs considèrent Content-Length comme un en-tête de requête interdit : le code ne peut pas le définir avec fetch ou XMLHttpRequest. Envoyez un File ou Blob dont la taille correspond exactement aux bytes du ticket et laissez le navigateur produire Content-Length depuis le corps ; ne définissez vous-même que les autres en-têtes du ticket. La réponse liste toujours Content-Length pour les clients hors navigateur et pour permettre au code navigateur de vérifier la taille avant l’envoi.

N’envoyez pas d’en-tête Authorization pour cette requête. L’URL présignée sert d’autorisation ; un en-tête supplémentaire provoque un refus avec certains stockages.

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
});

Valider l’envoi

Un PUT réussi signifie seulement qu’un objet temporaire a été écrit. Continuez avec le fileId de la première étape :

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

Attendez "status": "ready" et une uri commençant par 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 validation compare la taille stockée et le type de média au ticket, vérifie la signature du média, calcule SHA-256 et copie les octets vers un objet privé que l’ancien ticket ne peut pas écraser. Pour l’audio et la vidéo, elle peut aussi renvoyer durationSeconds sous forme de chaîne décimale, ainsi que width et height numériques. Répéter une validation réussie est idempotent.

RéponseSignificationCorrection
400, code: 40003La taille, le type ou le format du fichier stocké ne correspond pas à votre déclarationDemandez un ticket avec le bon contentType et les bytes exacts, puis renvoyez le fichier
400Le fichier n’est pas encore présent : le PUT n’est pas terminé ou a échouéVérifiez que le PUT a renvoyé 200, puis validez à nouveau
404Aucun fileId de ce type dans ce compteUtilisez le fileId de votre propre ticket
409Le ticket ou fichier envoyé a expiré, ou une validation précédente a refusé ce fichierRecommencez à l’étape 1
409 / 429La vérification du fichier est déjà en cours, ou le vérificateur est occupéAttendez une seconde, puis validez à nouveau

Un PUT doit être suivi d’une validation

Un fichier non validé ne peut pas servir à créer une tâche. N’utilisez ni le chemin de stockage temporaire du ticket ni son champ de compatibilité key avant une validation réussie.

Placer l’URI spicy:// dans l’entrée

Comme champ de référence, utilisez uniquement data.uri de la réponse de validation, pas uploadUrl, une clé d’objet temporaire ou le fileId seul.

Le champ recevant un fichier dépend du modèle. Dans son inputSchema, il porte un x-ui.widget à upload pour un fichier ou multi-upload pour une liste, où l’URI se place dans un tableau. x-ui.accept liste les types et x-ui.max_size_mb la taille acceptés par le modèle. Voir Catalogue des modèles et schémas.

MODEL_ID_FROM_CATALOG et les autres champs d’entrée ci-dessous sont des valeurs structurelles à remplacer : choisissez le model exact et une entrée valide selon le schéma de la même entrée du catalogue authentifié actuel. La tâche dépend de model lui-même : input ne contient donc aucun champ de point de terminaison. Il accepte uniquement les champs de l’inputSchema du modèle, et un champ supplémentaire renvoie 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 plateforme résout une URI spicy:// validée lorsque la tâche s’exécute. Les fichiers restent ainsi utilisables en file d’attente sans vous imposer de gérer des URL temporaires de téléchargement. Utilisez l’URI de validation sans modification.

Une URI non prête, expirée ou appartenant à un autre compte est refusée à la création avec 400.

Exemple complet

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

Une limite de débit supplémentaire

En plus du quota général de l’API publique, upload-url dispose de sa propre protection de 1 000 requêtes par 10 secondes contre les autorisations d’écriture abusives ou incontrôlées dans le stockage d’objets.

Le coût est ici asymétrique : signer un ticket est un calcul purement local pour nous, mais chaque ticket autorise une écriture qui ne repasse plus par nous. Une personne sélectionnant des fichiers ne peut pas aller si vite, et un script envoyant quelques dizaines de références reste largement dans la limite.

2. Télécharger les résultats

Les résultats prêts incluent déjà output.assets[].url et expiresAt dans le polling et les callbacks v2. Téléchargez directement cette URL sans clé API ; le download-url ci-dessous est facultatif. Les liens durent normalement 20 minutes et ne dépassent jamais la conservation de la tâche, plafonnée à environ 14 jours ou raccourcie par vos réglages.

Pour garder un résultat, enregistrez-le sur votre stockage pendant la validité du lien. Par exemple :

curl -L -o result.png "PASTE_THE_OUTPUT_URL_HERE"

N’ajoutez pas d’en-tête Authorization à cette requête. Si le lien a expiré et que le stockage répond 403, relisez la tâche par recordInfo pour en obtenir un nouveau.

Renouvellement facultatif du lien

POST /api/v1/common/download-url

Propriété

Type

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

Pour plusieurs fichiers, appelez une fois par key. Comme recordInfo, cela fonctionne uniquement avec la clé API qui a créé la tâche.

Une URL signée est une chaîne qui fait elle-même preuve d’autorisation : le stockage d’objets ne vérifie pas de nouveau qui la détient. Par conséquent :

  • Ne la stockez pas en base, ni dans un cache durable servi aux navigateurs. Signez-en une au besoin ; la signature est un calcul local pratiquement gratuit.
  • Téléchargez ce que vous voulez garder. Les fichiers restent chez nous environ 14 jours au maximum, moins si vous raccourcissez la durée ; voir Conservation et destruction.
SituationCe que vous voyezAction
Copie dans notre stockage encore en coursL’élément porte pending: true dans recordInfo ; download-url sans key renvoie 409, avec une key explicite il renvoie 404 jusqu’à la fin de copieAttendez quelques secondes puis relisez
Échec définitif de copieL’élément porte unavailable: true sans keyAttendre ne résout rien ; régénérez
Tâche pas encore réussie404, « This task has no downloadable output »Attendez state: "succeeded"
Conservation expirée ou contenu détruitHTTP 410 avec code: 404, « This output has expired or was deleted »Suppression définitive ; régénérez

Une tâche qui n’est pas la vôtre ou une key étrangère à la tâche renvoie dans les deux cas 404. Nous ne distinguons pas « n’existe pas » et « appartient à quelqu’un d’autre », car cette différence permettrait d’énumérer les IDs de tâches d’autrui.

Bibliothèque d'assets

Certains fichiers servent encore et encore : un portrait de personnage, une référence de style, une photo produit, un échantillon de voix. La bibliothèque d'assets les conserve jusqu'à ce que vous les supprimiez et donne à chacun une référence stable, spicy://a/<id>. Placez cette référence dans n'importe quel champ média de n'importe quelle entrée de tâche, comme pour un envoi spicy://f/, mais sans l'expiration au bout d'un jour.

  • Privée à votre espace de travail. Une référence spicy://a/ ne fonctionne que dans les requêtes faites avec des clés du même espace de travail. Une clé personnelle atteint votre bibliothèque personnelle ; une clé créée dans un espace d’équipe atteint la bibliothèque partagée de cette équipe, et les deux ne communiquent jamais (voir ci-dessous).
  • Conservée jusqu’à suppression. Les résultats générés sont nettoyés au bout d’environ 14 jours et les envois au bout de 1 jour ; les assets enregistrés restent jusqu’à ce que vous les supprimiez. Une bibliothèque personnelle disparaît avec la fermeture de votre compte, une bibliothèque d’équipe avec son équipe.
  • Ce n'est pas un hébergeur de fichiers. L'API vous donne la référence spicy://a/<id> et une previewUrl signée pour vous seul, valable 20 minutes au plus. Il n'existe aucun lien public ou permanent, et une référence ne peut pas en devenir un. Conservez uri, jamais previewUrl.

Bibliothèques personnelle et d’équipe

La clé décide de la bibliothèque. Une clé créée dans votre espace personnel atteint votre bibliothèque personnelle. Une clé créée dans un espace d’équipe, par n’importe quel membre, atteint la bibliothèque partagée de cette équipe. Une clé d’équipe n’atteint jamais la bibliothèque personnelle de quiconque, une clé personnelle n’atteint jamais une bibliothèque d’équipe, et une référence spicy://a/ de l’autre bibliothèque est refusée comme si elle n’existait pas.

Ce qu’une clé d’équipe peut modifier suit le rôle actuel du membre qui l’a créée :

RôleEnregistrerRenommer ou supprimerLister, lire et utiliser
owner, adminOuiN’importe quel assetOui
developerOuiSeulement ceux qu’il a enregistrésOui
billing, readonlyNonNonOui

Si ce membre a quitté l’équipe, la clé reçoit 403 avec data.reason valant asset_workspace_forbidden. Les assets n’indiquent pas qui les a enregistrés ; vous voyez seulement si vous pouvez en modifier un.

Enregistrer un fichier

POST /api/v1/assets copie un fichier dans votre bibliothèque. L'en-tête Idempotency-Key est obligatoire. Choisissez l'une des trois sources :

sourceChampsRemarques
task_outputtaskId, et key ou indexL'une de vos propres tâches réussies. index part de 0, dans l'ordre de output.assets dans recordInfo.
uploadfileIdUn envoi validé : fil_… ou spicy://f/fil_….
urlurlUne adresse http ou https publique sur le port 80 ou 443, sans nom d'utilisateur ni mot de passe. Le fichier est téléchargé une fois et son type est lu dans le fichier lui-même ; seuls les images, vidéos et fichiers audio sont acceptés.

name est facultatif (80 caractères au plus ; Saved asset par défaut). Enregistrer de nouveau la même source renvoie l'asset existant sans occuper d'espace supplémentaire. La taille, les dimensions et la durée sont mesurées par nos soins.

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

L'utiliser dans une tâche

Placez data.uri dans tout champ fichier que le inputSchema du modèle marque avec x-ui.widget valant upload ou 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
  }
}

Une référence supprimée, ou située dans une bibliothèque que cette clé n’atteint pas (celle d’un autre compte ou de l’autre espace de travail), est refusée à la création de la tâche, exactement comme un identifiant inconnu.

Lister, renommer et supprimer

GET /api/v1/assets renvoie vos assets sous items, avec un nextCursor absent de la dernière page. Filtrez avec kind (image, video ou audio), source (séparée par des virgules : task_output,upload,url), q (recherche dans le nom, sans distinction de casse), favorite (true pour les seuls favoris, false pour tout le reste) et collectionId (un dossier, voir plus bas). Triez avec sort : recent (par défaut, du plus récent au plus ancien), oldest, name (de A à Z, sans distinction de casse) ou size (du plus volumineux au plus petit). limit va de 1 à 100 et vaut 100 par défaut. Pour la page suivante, passez le nextCursor précédent comme cursor ; un curseur ne fonctionne qu’avec le sort dont il provient, et un curseur issu d’un autre tri est refusé avec 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} renvoie un asset avec une previewUrl neuve. PATCH modifie le nom, l’indicateur favorite ou les deux (n’envoyez que ce qui change) ; l’uri reste identique. Avec DELETE, l’asset disparaît aussitôt et son uri ne fonctionne plus dans les nouvelles requêtes. Les deux appels se répètent sans risque et n’exigent pas d’Idempotency-Key. Un asset inconnu et l’asset d’un autre compte renvoient le même 404.

Favoris, dossiers et modèle générateur

Chaque asset porte un indicateur favorite. Activez-le avec PATCH et {"favorite": true}. Dans une bibliothèque d’équipe, la marque appartient à la bibliothèque : tout le monde voit les mêmes favoris. Les dossiers (appelés collections dans l’API) regroupent des assets sans les copier, et un même asset peut figurer dans plusieurs dossiers. Vous créez et gérez les dossiers depuis la page Assets de l’application web ; l’API ne permet ni de les créer ni de les modifier, mais collectionId sur GET /api/v1/assets liste les assets d’un dossier (404 si ce dossier n’est pas dans la bibliothèque que votre clé atteint).

Chaque asset porte aussi model, le nom public du modèle qui l’a généré, et modelName, son nom d’affichage. Pour les imports et les liens, les deux sont des chaînes vides.

Personnages

Un personnage est un nom accompagné d’un ensemble ordonné d’assets image de la même bibliothèque : jusqu’à 100 images et une note facultative de 280 caractères au plus. Les images sont les assets eux-mêmes, rien n’est copié. Supprimer un personnage conserve ses images, et supprimer une image la retire de tous les personnages. Vous créez et modifiez les personnages depuis la page Assets de l’application web ou dans Studio. Ils n’ont pas de stockage propre : seul votre forfait de bibliothèque compte.

L’API ne fait que lire les personnages :

  • GET /api/v1/assets/entities les liste, du plus récent au plus ancien. assetId ne garde que les personnages qui contiennent cet asset, limit va de 1 à 100 (50 par défaut), et cursor reprend au nextCursor précédent.
  • GET /api/v1/assets/entities/{id} renvoie un personnage. Un personnage inconnu, celui d’un autre compte et celui d’une bibliothèque que votre clé n’atteint pas renvoient le même 404.

Chaque personnage comporte id, kind (toujours character), name, note, cover (l’image de couverture sous forme d’asset, ou null tant qu’il n’a pas d’image), assets (les images dans l’ordre, chacune avec son uri et une previewUrl privée), count, createdAt et updatedAt. C’est la clé qui détermine la bibliothèque, comme pour les assets, et la lecture des personnages partage la limite de 60 requêtes par minute avec la lecture des assets.

Pour utiliser un personnage, placez ses images, dans cet ordre, sous forme de références spicy://a/ dans le champ d’images de référence du modèle, et désignez-les dans le prompt comme le décrit la page de ce modèle. Les personnages s’appelaient auparavant des personas et vivaient dans une bibliothèque distincte ; ils font désormais partie de la bibliothèque d’assets. Les références spicy://p/ existantes continuent de fonctionner, dans votre bibliothèque personnelle uniquement.

Quand vous ajoutez à un personnage une image que vous avez importée vous-même (et non un résultat généré), vous confirmez détenir les droits sur cette image et, si elle montre une personne réelle, avoir son consentement.

Forfaits de stockage

Le stockage se débloque automatiquement avec vos recharges cumulées et ne peut que progresser ; une modification du tableau ne supprime jamais les assets déjà enregistrés. La référence pour votre compte est GET /api/v1/assets/usage : votre forfait, l'espace utilisé, les limites, le forfait suivant et le montant restant à recharger pour l'atteindre, ainsi que le nombre d'assets créés aujourd'hui. Le tableau montre les valeurs par défaut actuelles.

Le stockage appartient au propriétaire : ses recharges cumulées déterminent le forfait, et sa bibliothèque personnelle ainsi que toutes les bibliothèques d’équipe qu’il possède partagent un seul quota. usage couvre ce quota entier et comporte un objet scope (workspaceId, personal, role, canWrite, canTopUp) qui indique quelle bibliothèque votre clé atteint et ce qu’elle peut y faire. Lorsque canTopUp vaut false (clé d’équipe non créée par le propriétaire de l’équipe), lifetimeTopupUsd et topupToNextTierUsd sont omis, et une bibliothèque pleine vous invite à contacter le propriétaire de l’équipe au lieu d’indiquer un montant.

ForfaitRecharges cumuléesStockageNombrePar fichier
Freeaucuneaucun : rien de nouveau ne peut être enregistré0sans objet
Plus10 $ ou plus500 Mo200100 Mo
Pro100 $ ou plus1 Go400100 Mo
Studio250 $ ou plus2 Go800100 Mo
Max1 000 $ ou plus10 Go4 000100 Mo

Sans recharge, vous ne pouvez rien enregistrer de nouveau, mais les assets déjà enregistrés restent utilisables. La limite de 100 Mo par fichier s'applique aux sorties de tâches et aux liens. Un fichier qui arrive par envoi reste soumis aux limites de l'envoi lui-même (images 10 Mio, audio et vidéo 90 Mio).

Limites de débit et erreurs

  • Lecture (liste, consultation, usage) : 60 requêtes par minute. Enregistrement : 30 requêtes par minute et 200 nouveaux assets par jour UTC. Supprimer un asset ne restitue pas le quota du jour. Lorsqu'une limite est atteinte, vous recevez 429 avec l'en-tête Retry-After.
  • Une bibliothèque pleine, un stockage pas encore débloqué ou un fichier au-delà de la limite par fichier renvoient 400 avec invalid_request. Le message indique votre forfait actuel et le montant à recharger pour débloquer le suivant.
  • 403 avec data.reason valant asset_workspace_forbidden lorsque la clé n’atteint pas la bibliothèque, ou asset_permission_denied lorsque votre rôle dans l’équipe n’autorise pas la modification. 503 lorsque le stockage de la plateforme est temporairement plein ; réessayez plus tard.

Un échec sur ces points d’accès peut porter un data.reason lisible par machine : asset_library_locked, asset_library_full, asset_file_too_large, asset_daily_limit, asset_storage_full, asset_permission_denied, asset_workspace_forbidden ou asset_entity_name_taken. code, le statut HTTP et errorCode ne changent pas, avec ou sans cette raison, et elle n’apparaît que sur les points d’accès de la bibliothèque d’assets ; la couche compatible OpenAI et les autres couches de compatibilité gardent leurs propres formes d’erreur. Traitez une valeur inconnue comme une raison absente. Voir Codes de raison de la bibliothèque d’assets.

L'application web propose aussi un interrupteur d'enregistrement automatique dans la bibliothèque. Il ne concerne que les générations lancées depuis l'application web ; celles lancées par l'API ne sont jamais enregistrées automatiquement, appelez donc POST /api/v1/assets pour celles que vous voulez garder.

Conservation des fichiers envoyés

Une URI de référence envoyée est conservée 1 jour à partir d’une validation réussie. Ce plafond de plateforme peut être raccourci ou remplacé par une destruction à la fin de la tâche sur Conservation et destruction. L’URI est utilisable uniquement jusqu’à l’expiresAt renvoyé par la validation. Elle devient inutilisable à expiration, y compris pour une relance d’une tâche qui la référence. La suppression par cycle de vie du stockage d’objets peut ensuite prendre jusqu’à 24 heures de plus, mais ce délai de nettoyage ne prolonge pas la disponibilité de l’URI. Si vous avez besoin de la même référence, conservez votre copie et renvoyez-la.

Sur cette page