spicyapiDocumentación
Desarrollar con la API
Contenido principal

Medios

Subidas directas prefirmadas para medios de referencia y enlaces temporales de descarga de resultados.

Muchos modelos reciben una imagen, un video o un clip de sonido como entrada (una referencia), y un modelo de medios devuelve un archivo (un resultado). Esta página explica ambas direcciones:

  • Entradas de referencia: proporciona una URL HTTPS pública, una imagen pequeña incluida directamente en la solicitud o un archivo que subas. Para subirlo, solicita una autorización de subida, envía el archivo con PUT directamente al almacenamiento de objetos y confirma la subida; la confirmación devuelve un URI spicy:// que debes incluir en la entrada de la tarea.
  • Descargas de resultados: descarga la output.assets[].url firmada devuelta con el resultado. No todos los modelos producen archivos: un endpoint que devuelve texto responde en output.text y no tiene assets, por lo que no hay nada que descargar ni un enlace que pueda vencer. Consulta Forma de la salida.

Las autorizaciones de subida y las descargas de resultados están en /api/v1/common/; las confirmaciones de subida utilizan /api/v1/files/{fileId}/commit. Subir archivos es gratis; solo pagas por la tarea que los utiliza.

Medios HTTPS existentes

Proporciona directamente una URL HTTPS pública de imagen, video o audio en el campo admitido por el modelo (por ejemplo, image_url). No necesitas subirla a la plataforma ni convertirla a spicy://. La URL debe permanecer accesible sin encabezados de autorización personalizados mientras se ejecuta la tarea. Los archivos locales más grandes utilizan el proceso de subida siguiente.

La URL debe devolver el archivo en sí. Un enlace de una página compartida —de una unidad en la nube, un álbum o una publicación social— devuelve una página web en lugar de la imagen, y la tarea falla con invalid_asset. En ese caso, descarga el archivo y súbelo.

Base64 y archivos locales

Las imágenes pequeñas pueden enviarse directamente en los campos de imagen declarados en el esquema, por ejemplo image_url: "data:image/png;base64,...". Incluye el tipo MIME y utiliza Base64 estándar. Esta opción no admite Base64 sin el prefijo, codificación URL-safe, espacios en blanco, SVG ni audio o video en línea.

  • JPEG, PNG, WebP y GIF: como máximo 1 MiB decodificado por imagen, 8192 píxeles por lado y 8,388,608 píxeles por imagen.
  • Como máximo 16 imágenes y 16,777,216 píxeles combinados por solicitud. El cuerpo JSON completo debe caber en 2 MiB, incluido el aumento de tamaño de aproximadamente un tercio que añade Base64.
  • También se aplican el esquema y los límites de medios específicos del modelo. Las cotizaciones validan sin subir archivos ni reservar fondos. Al crear una tarea, las imágenes se almacenan como subidas vinculadas a la cuenta y duran un día, como cualquier otra subida; la entrada de la tarea contiene URI de archivos en lugar de repetir los bytes Base64.

Para archivos más grandes, el SDK gestiona la autorización, el PUT y la confirmación:

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 es una función de subida de archivos que utiliza la misma API de subida y los límites indicados más abajo. El límite de 1 MiB de las imágenes pequeñas en línea no se aplica a esta función; aun así, cada modelo debe admitir el tipo de medio elegido. Un medio HTTPS público existente no requiere ninguna función de subida.

1. Subida directa de referencias

Subir un archivo local de referencia que todavía no esté alojado en una URL HTTPS pública requiere cuatro pasos. Los comandos curl siguientes deben ejecutarse uno después de otro en la misma ventana de terminal, en macOS o Linux y con jq instalado: cada paso guarda en una variable de shell lo que necesita el siguiente. En Windows, utiliza el SDK o el ejemplo de Python al final de esta sección.

Solicitar una autorización

POST /api/v1/common/upload-url

Propiedad

Tipo

No hay un campo de nombre de archivo, y es intencional

Las claves de objetos siempre se generan en el servidor. Permitir que quien llama elija una ruta haría posible crear una URL de subida que apunte al objeto de otra persona y sobrescribirlo.

Límites de subida

contentTypeExtensiónTamaño máximoDuración máxima
image/jpeg.jpg10 MiB—
image/png.png10 MiB—
image/webp.webp10 MiB—
image/gif.gif10 MiB—
audio/wav.wav90 MiB600 segundos
audio/mpeg.mp390 MiB600 segundos
video/webm.webm90 MiB600 segundos
video/mp4.mp490 MiB600 segundos
video/quicktime.mov90 MiB600 segundos
application/octet-stream.safetensors (pesos LoRA)2 GiB—

Se acepta .mov, el formato predeterminado de la cámara de un iPhone, y se convierte a MP4 al ingresar, por lo que el modelo siempre recibe MP4. Lo admiten tanto el cargador web como la API de subida; la CLI, el SDK y el servidor MCP deducen el tipo a partir de la extensión y todavía no lo incluyen, así que pasa explícitamente contentType: video/quicktime en esas herramientas.

Los pesos LoRA se suben como application/octet-stream: un archivo .safetensors de hasta 2 GiB (el cargador web admite hasta 90 MiB). El ticket es válido durante dos horas en lugar de 20 minutos, y el commit comprueba que el archivo sea un safetensors completo. Pon el URI devuelto en loras[].path; un archivo de pesos no puede usarse en ningún otro campo. Un archivo que exige iniciar sesión para descargarse, como uno de un repositorio privado de Hugging Face, puede importarse desde su enlace: consulta Pesos de adaptador.

Un modelo puede aceptar menos: respeta el x-ui.max_size_mb de su esquema y considera el maxBytes de la autorización como el máximo válido para esta subida.

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)

Deberías ver una respuesta como esta; UPLOAD_URL y FILE_ID ahora contienen sus valores de uploadUrl y 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 está aproximadamente a 20 minutos. Tanto el PUT como la confirmación deben terminar antes; después, la confirmación devuelve 409 y necesitas una autorización nueva. Solicítala justo antes de subir, no con antelación.

Enviar el archivo con PUT

Envía los bytes mediante PUT a uploadUrl. Los clientes ajenos al navegador, como curl, Node.js y Python, deben enviar todos los encabezados de headers exactamente como se proporcionan. Los clientes del navegador deben gestionar Content-Length como se explica a continuación.

Si omites un encabezado, la firma deja de coincidir

Se firman tanto Content-Type como Content-Length. Cambiar un byte de cualquiera hace que el almacenamiento de objetos rechace la subida con un 403 procedente del almacenamiento, no de nosotros, lo que puede resultar confuso.

Los navegadores consideran Content-Length un encabezado de solicitud prohibido, por lo que el código de la aplicación no puede establecerlo mediante fetch ni XMLHttpRequest. En un navegador, envía un File o Blob cuyo tamaño coincida exactamente con los bytes de la autorización y deja que el navegador genere Content-Length a partir del cuerpo; establece por tu cuenta solo los demás encabezados de la autorización. La respuesta sigue incluyendo Content-Length para los clientes ajenos al navegador y para que el código del navegador pueda verificar el tamaño antes de enviarlo.

No envíes un encabezado Authorization en esta solicitud. La URL prefirmada es la credencial; algunos sistemas de almacenamiento rechazan la solicitud si añades otro encabezado de autenticación.

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

Confirmar la subida

Un PUT correcto solo significa que se escribió un objeto temporal. Continúa con el fileId del primer paso:

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

Deberías recibir "status": "ready" y un uri que comience 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 confirmación compara el tamaño y el tipo del medio almacenado con la autorización, comprueba la firma del formato, calcula SHA-256 y copia los bytes a un objeto privado que la autorización antigua no puede sobrescribir. Para audio y video, también puede devolver durationSeconds como cadena decimal, además de width y height numéricos. Repetir una confirmación correcta es idempotente.

RespuestaSignificadoSolución
400, code: 40003El tamaño, tipo o formato del archivo almacenado no coincide con lo declaradoSolicita otra autorización con el contentType correcto y los bytes exactos y vuelve a subir
400El archivo todavía no está: el PUT no terminó o fallóComprueba que el PUT devolvió 200 y vuelve a confirmar
404No existe ese fileId en esta cuentaUtiliza el fileId de tu propia autorización
409Venció la autorización o el archivo subido, o una confirmación anterior rechazó el archivoEmpieza otra vez desde el paso 1
409 / 429Ya se está verificando el archivo o el verificador está ocupadoEspera un segundo y vuelve a confirmar

Después de PUT debes confirmar la subida

Un archivo sin confirmar no puede utilizarse para crear una tarea. No uses la ruta temporal de almacenamiento de la autorización ni el campo de compatibilidad key hasta que la confirmación termine correctamente.

Incluir el URI spicy:// en la entrada

Utiliza únicamente data.uri de la respuesta de confirmación —no uploadUrl, una clave de objeto temporal ni el fileId solo— como campo de referencia.

El campo que recibe un archivo depende del modelo. En su inputSchema, un campo de archivo tiene x-ui.widget con el valor upload para un archivo o multi-upload para una lista, donde debes incluir el URI dentro de un array. x-ui.accept enumera los tipos de archivo y x-ui.max_size_mb el tamaño que acepta el modelo. Consulta Catálogo de modelos y esquemas.

MODEL_ID_FROM_CATALOG y los demás campos de entrada siguientes son marcadores de posición estructurales; selecciona el model exacto y una entrada válida con el esquema de la misma entrada del catálogo autenticado en vivo. La tarea se determina por el propio model, así que input no contiene un campo de endpoint: solo acepta los campos enumerados en el inputSchema de ese modelo, y un campo adicional devuelve 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 plataforma resuelve un URI spicy:// confirmado al ejecutar la tarea. Así, los archivos subidos siguen siendo utilizables mientras la tarea está en cola sin que debas gestionar URL de descarga temporales. Utiliza sin cambios el URI devuelto por la confirmación.

Un URI que no esté listo, haya vencido o pertenezca a otra cuenta se rechaza al crear la tarea con 400.

Ejemplo 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 límite adicional de solicitudes

Además del límite general de la API pública, upload-url tiene su propia protección de 1,000 solicitudes cada 10 segundos contra autorizaciones de escritura en almacenamiento de objetos abusivas o descontroladas.

El costo es asimétrico: firmar una autorización solo requiere cálculo local para nosotros, pero cada una autoriza una escritura que ya no pasa por nosotros. Una persona que selecciona archivos no alcanza ese ritmo, y un script que sube unas decenas de referencias sigue estando ampliamente dentro del límite.

2. Descargar resultados

Los resultados listos ya incluyen output.assets[].url y expiresAt en las consultas de estado y los callbacks v2. Descarga esa URL directamente sin una clave de API; el download-url siguiente es opcional. Los enlaces normalmente duran 20 minutos y nunca superan el plazo de conservación de la tarea, limitado a unos 14 días o menos si lo configuras así.

Para conservar un resultado, guárdalo en tu propio almacenamiento mientras el enlace sea válido. Por ejemplo:

curl -L -o result.png "PASTE_THE_OUTPUT_URL_HERE"

No añadas un encabezado Authorization a esta solicitud. Si el enlace venció y el almacenamiento devuelve 403, vuelve a consultar la tarea con recordInfo para obtener uno nuevo.

Renovar el enlace de descarga de forma opcional

POST /api/v1/common/download-url

Propiedad

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

Si una tarea tiene varios archivos, realiza una llamada por cada key. Como recordInfo, solo funciona con la clave de API que creó la tarea.

Una URL firmada es una cadena que acredita por sí misma el acceso: el almacenamiento de objetos no vuelve a preguntar quién la posee. Por tanto:

  • No la guardes en tu base de datos ni en una caché de larga duración que sirvas a los navegadores. Solicita una firma cuando la necesites: firmar es cálculo local y su costo es prácticamente nulo.
  • Descarga lo que quieras conservar. Los archivos duran como máximo unos 14 días en nuestro almacenamiento y menos si acortas el período; consulta Conservación y destrucción.
SituaciónQué vesQué hacer
Todavía se copia a nuestro almacenamientoEl archivo muestra pending: true en recordInfo; download-url sin key devuelve 409 (con un key explícito devuelve 404 hasta que termine la copia)Espera unos segundos y vuelve a consultar
La copia falló definitivamenteEl archivo muestra unavailable: true y no tiene keyEsperar no ayudará; vuelve a generar
La tarea todavía no terminó correctamente404, «This task has no downloadable output»Espera a state: "succeeded"
Terminó su período de conservación o lo destruisteHTTP 410 con code: 404, «This output has expired or was deleted»Se perdió definitivamente; vuelve a generar

Tanto una tarea que no sea tuya como un key que no le pertenezca devuelven 404. No distinguimos «no existe» de «pertenece a otra persona», ya que esa diferencia permitiría enumerar ID de tareas ajenas.

Biblioteca de recursos

Hay archivos que usas una y otra vez: un retrato de personaje, una referencia de estilo, una foto de producto, una muestra de voz. La biblioteca de recursos los guarda hasta que los elimines y da a cada uno una referencia estable, spicy://a/<id>. Pon esa referencia en cualquier campo multimedia de cualquier entrada de tarea, igual que con una subida spicy://f/, pero sin la caducidad de un día.

  • Privada a tu espacio de trabajo. Una referencia spicy://a/ solo funciona en solicitudes hechas con claves del mismo espacio de trabajo. Una clave personal llega a tu biblioteca personal; una clave creada en un espacio de equipo llega a la biblioteca compartida de ese equipo, y ambas nunca se conectan (consulta más abajo).
  • Se conserva hasta que se elimine. Los resultados generados se limpian a los 14 días aproximadamente y las subidas al cabo de 1 día; los recursos guardados permanecen hasta que los elimines. Una biblioteca personal desaparece al cerrar tu cuenta y una biblioteca de equipo, junto con su equipo.
  • No es un servicio de alojamiento de archivos. La API te da la referencia spicy://a/<id> y una previewUrl firmada solo para ti, válida como máximo 20 minutos. No existe ningún enlace público ni permanente, y una referencia no puede convertirse en uno. Guarda uri, nunca previewUrl.

Bibliotecas personal y de equipo

La clave decide la biblioteca. Una clave creada en tu espacio personal llega a tu biblioteca personal. Una clave creada en un espacio de equipo, por cualquier miembro, llega a la biblioteca compartida de ese equipo. Una clave de equipo nunca llega a la biblioteca personal de nadie, una clave personal nunca llega a una biblioteca de equipo, y una referencia spicy://a/ de la otra biblioteca se rechaza como si no existiera.

Lo que una clave de equipo puede cambiar depende del rol actual del miembro que la creó:

RolGuardarRenombrar o eliminarListar, leer y usar
owner, adminSíCualquier recursoSí
developerSíSolo los que guardóSí
billing, readonlyNoNoSí

Si ese miembro ha salido del equipo, la clave recibe 403 con data.reason igual a asset_workspace_forbidden. Los recursos no indican quién los guardó; solo ves si puedes modificar uno.

Guardar un archivo

POST /api/v1/assets copia un archivo a tu biblioteca. La cabecera Idempotency-Key es obligatoria. Elige una de las tres fuentes:

sourceCamposNotas
task_outputtaskId, y key o indexUna de tus propias tareas completadas con éxito. index empieza en 0, en el orden de output.assets en recordInfo.
uploadfileIdUna subida confirmada: fil_… o spicy://f/fil_….
urlurlUna dirección http o https pública en el puerto 80 o 443, sin usuario ni contraseña. El archivo se descarga una vez y su tipo se lee del propio archivo; solo se aceptan imágenes, vídeo y audio.

name es opcional (hasta 80 caracteres; por defecto Saved asset). Si guardas de nuevo la misma fuente, se devuelve el recurso que ya tienes y no se ocupa más espacio. El tamaño, las dimensiones y la duración los medimos nosotros.

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 en una tarea

Pon data.uri en cualquier campo de archivo que el inputSchema del modelo marque con x-ui.widget igual 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
  }
}

Una referencia eliminada, o ubicada en una biblioteca a la que esta clave no llega (la de otra cuenta o la del otro espacio de trabajo), se rechaza al crear la tarea, exactamente igual que un ID desconocido.

Listar, renombrar y eliminar

GET /api/v1/assets devuelve tus recursos en items, más un nextCursor que no aparece en la última página. Filtra con kind (image, video o audio), source (separadas por comas: task_output,upload,url), q (búsqueda en el nombre, sin distinguir mayúsculas), favorite (true solo para favoritos, false para todo lo demás) y collectionId (una carpeta, más abajo). Ordena con sort: recent (por defecto, del más reciente al más antiguo), oldest, name (de la A a la Z, sin distinguir mayúsculas) o size (del más grande al más pequeño). limit va de 1 a 100 y por defecto es 100. Para la página siguiente, pasa el nextCursor anterior como cursor; un cursor solo sirve con el sort del que procede, y uno de otro orden se rechaza 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} devuelve un recurso con una previewUrl nueva. PATCH cambia el nombre, el indicador favorite o ambos (envía solo lo que quieras cambiar); la uri no cambia. Con DELETE, el recurso desaparece al instante y su uri deja de funcionar en solicitudes nuevas. Ambas llamadas se pueden repetir sin riesgo y no necesitan Idempotency-Key. Un recurso desconocido y el de otra cuenta devuelven el mismo 404.

Favoritos, carpetas y modelo generador

Cada recurso tiene un indicador favorite. Actívalo con PATCH y {"favorite": true}. En una biblioteca de equipo la marca pertenece a la biblioteca, así que todos ven los mismos favoritos. Las carpetas (en la API se llaman colecciones) agrupan recursos sin copiarlos, y un recurso puede estar en varias carpetas. Las carpetas se crean y gestionan en la página Assets de la aplicación web; la API no permite crearlas ni editarlas, pero collectionId en GET /api/v1/assets lista los recursos de una carpeta (404 si esa carpeta no está en la biblioteca a la que llega tu clave).

Cada recurso incluye además model, el nombre público del modelo que lo generó, y modelName, su nombre para mostrar. En las subidas y los enlaces ambos son cadenas vacías.

Personajes

Un personaje es un nombre más un conjunto ordenado de recursos de imagen de la misma biblioteca: hasta 100 imágenes y una nota opcional de hasta 280 caracteres. Las imágenes son los propios recursos y no se copia nada. Si eliminas un personaje, sus imágenes se conservan, y si eliminas una imagen, desaparece de todos los personajes. Los personajes se crean y editan en la página Assets de la aplicación web o en Studio. No necesitan almacenamiento propio: solo cuenta tu plan de biblioteca.

La API solo lee personajes:

  • GET /api/v1/assets/entities los lista, del más reciente al más antiguo. assetId deja solo los personajes que contienen ese recurso, limit va de 1 a 100 (50 por defecto) y cursor continúa desde el nextCursor anterior.
  • GET /api/v1/assets/entities/{id} devuelve un personaje. Un personaje desconocido, el de otra cuenta y el de una biblioteca a la que tu clave no llega devuelven el mismo 404.

Cada personaje trae id, kind (siempre character), name, note, cover (la imagen de portada como recurso, o null mientras no tenga imágenes), assets (las imágenes en orden, cada una con su uri y una previewUrl privada), count, createdAt y updatedAt. La clave decide la biblioteca igual que con los recursos, y la lectura de personajes comparte con la de recursos el límite de 60 solicitudes por minuto.

Para usar un personaje, pon sus imágenes, en ese orden, como referencias spicy://a/ en el campo de imágenes de referencia del modelo y menciónalas en el prompt como indica la página de ese modelo. Los personajes se llamaban antes personas y vivían en una biblioteca aparte; ahora forman parte de la biblioteca de recursos. Las referencias spicy://p/ existentes siguen funcionando, solo en tu biblioteca personal.

Cuando añades a un personaje una imagen que subiste tú (y no un resultado generado), confirmas que tienes los derechos sobre ella y, si aparece una persona real, que cuentas con su consentimiento.

Planes de almacenamiento

El almacenamiento se desbloquea automáticamente con tus recargas acumuladas y solo puede subir; un cambio en la tabla nunca elimina los recursos que ya guardaste. La fuente de verdad para tu cuenta es GET /api/v1/assets/usage: tu plan, lo que tienes almacenado, los límites, el siguiente plan y cuánto más debes recargar para alcanzarlo, y cuántos recursos has creado hoy. La tabla muestra los valores por defecto actuales.

El almacenamiento pertenece al propietario: sus recargas acumuladas determinan el plan, y su biblioteca personal junto con todas las bibliotecas de equipo que posee comparten una sola cuota. usage cubre toda esa cuota e incluye un objeto scope (workspaceId, personal, role, canWrite, canTopUp) que indica a qué biblioteca llega tu clave y qué puede hacer allí. Cuando canTopUp es false (una clave de equipo no creada por el propietario del equipo), se omiten lifetimeTopupUsd y topupToNextTierUsd, y una biblioteca llena te pide contactar con el propietario del equipo en lugar de indicar un importe.

PlanRecargas acumuladasAlmacenamientoElementosPor archivo
Freeningunaninguno: no se puede guardar nada nuevo0no aplica
Plusdesde $10500 MB200100 MB
Prodesde $1001 GB400100 MB
Studiodesde $2502 GB800100 MB
Maxdesde $100010 GB4000100 MB

Sin ninguna recarga no puedes guardar nada nuevo, pero los recursos ya guardados siguen siendo utilizables. El límite de 100 MB por archivo se aplica a las salidas de tareas y a los enlaces. Un archivo que entra por subida sigue sujeto a los límites de la propia subida (imágenes 10 MiB, audio y vídeo 90 MiB).

Límites de frecuencia y errores

  • Lectura (listar, consultar, usage): 60 solicitudes por minuto. Guardado: 30 solicitudes por minuto y 200 recursos nuevos por día UTC. Eliminar un recurso no devuelve la cuota diaria. Al alcanzar un límite recibes 429 con la cabecera Retry-After.
  • Una biblioteca llena, un almacenamiento aún sin desbloquear o un archivo por encima del límite por archivo devuelven 400 con invalid_request. El mensaje indica tu plan actual y cuánto debes recargar para desbloquear el siguiente.
  • 403 con data.reason igual a asset_workspace_forbidden cuando la clave no llega a la biblioteca, o asset_permission_denied cuando tu rol en el equipo no permite el cambio. 503 cuando el almacenamiento de la plataforma está temporalmente lleno; vuelve a intentarlo más tarde.

Un fallo en estos endpoints puede llevar un data.reason legible por máquina: asset_library_locked, asset_library_full, asset_file_too_large, asset_daily_limit, asset_storage_full, asset_permission_denied, asset_workspace_forbidden o asset_entity_name_taken. code, el estado HTTP y errorCode no cambian con o sin él, y el motivo aparece solo en los endpoints de la biblioteca de recursos; la capa compatible con OpenAI y las demás capas de compatibilidad conservan sus propias formas de error. Trata un valor desconocido como un motivo ausente. Consulta Códigos de motivo de la biblioteca de recursos.

La aplicación web también tiene un interruptor de guardado automático en la biblioteca. Solo afecta a las generaciones que inicias en la aplicación web; las iniciadas por la API nunca se guardan automáticamente, así que llama a POST /api/v1/assets para las que quieras conservar.

Conservación de subidas

Un URI de referencia subido se conserva durante 1 día desde una confirmación correcta —un máximo de la plataforma que puedes acortar o sustituir por la destrucción al terminar en Conservación y destrucción— y solo puede utilizarse hasta el expiresAt devuelto por esa confirmación. Al vencer, el URI deja de funcionar, incluso para un reintento de una tarea que lo utilizó. La eliminación automática del almacenamiento puede demorarse hasta 24 horas más, pero ese plazo de limpieza no prolonga la vida útil del URI. Si necesitas otra vez la misma referencia, conserva tu propia copia y vuelve a subirla.

En esta página