Una generación correcta puede acabar mostrando una imagen rota en tu producto si guardas la URL temporal de entrega en lugar de copiar el archivo. Los enlaces firmados caducan en minutos; la salida de la API se conserva unas dos semanas.
Una API de medios entrega el mismo archivo con varias identidades: una referencia de entrada, una clave del resultado generado y una URL firmada que permite obtener los bytes durante unos minutos. Cada una tiene su propio plazo de caducidad y ninguna es una dirección permanente. Esta guía explica los plazos exactos de SpicyAPI y cómo copiar los resultados valiosos al almacenamiento que tú controlas. Los controles de retención pueden acortar estos plazos.
Pruébalo primero en el Playground
Abre la página de Z-Image de texto a imagen e inicia sesión:
- Escribe una escena breve en Prompt y elige un Aspect ratio.
- Pulsa Run model. El resultado aparece en Output.
- Descarga la imagen y guárdala en un lugar que tú controles.
- Vuelve a abrir el archivo guardado más tarde. Se abre sin la página ni su enlace temporal al resultado; eso es lo que el resto de la guía automatiza para producción.
Ideas clave
- Copia al finalizar, en un proceso de trabajo. Mantén las descargas grandes fuera de la solicitud del callback.
- Mantén privados los enlaces firmados. Quien posea uno obtiene acceso temporal al archivo.
- Expón tu propio ID de recurso y URL de entrega. Un registro de tarea puede sobrevivir a sus medios.
Qué caduca y cuándo
| Elemento | Duración | Qué ocurre al terminar |
|---|---|---|
Ticket de carga (uploadUrl) | Unos 20 minutos; tanto PUT como la confirmación deben terminar en ese plazo | Una respuesta de ticket caducado exige uno nuevo; una confirmación saturada también puede devolver 409 |
Referencia confirmada (spicy://f/fil_...) | 1 día desde la confirmación, hasta el expiresAt de la respuesta de confirmación | El URI se rechaza en tareas nuevas y reintentos |
Enlace al resultado (output.assets[].url) | Unos 20 minutos, sin superar nunca el plazo del propio medio | El almacenamiento devuelve 403; consulta de nuevo la tarea para obtener un enlace nuevo |
| Imagen, vídeo o audio generado | Unos 14 días desde la finalización (menos si lo configuras así) | Se elimina; ya no se puede emitir un enlace (410 de download-url) |
| Texto del prompt y rutas de los archivos del resultado en la tarea | 30 días | Se vacían input y output; el registro de tarea permanece |
| Registro de tarea (ID, modelo, estado, coste, marcas de tiempo) | Se conserva indefinidamente | Sigue siendo consultable, sin contenido |
Los medios de cortometrajes creados dentro de un proyecto de Spicy Studio tienen su propio plazo más largo, de unos 60 días; las generaciones de API, SDK y Playground usan el límite de 14 días anterior. También se puede acortar la retención por solicitud o por cuenta, como explica el artículo sobre controles de retención. Solo puedes reducir estos límites, nunca ampliarlos.
Los 14 días cuentan desde la finalización, no desde el envío. Antes de que la tarea termine, la marca de tiempo es provisional y se recalcula al finalizar.
«Unos» es intencionado. La limpieza del almacenamiento funciona mediante una regla de ciclo de vida y puede retrasarse hasta un día respecto al plazo, pero ese retraso no te da tiempo adicional: cuando se cierra la ventana, ya no se emite ningún enlace de descarga.
Un archivo, cuatro representaciones
local reference image
│
├─ POST /api/v1/common/upload-url → upload ticket (about 20 min)
├─ PUT to the returned uploadUrl → temporary object
├─ POST /api/v1/files/{fileId}/commit → spicy://f/fil_... (usable 1 day)
├─ POST /api/v1/jobs/createTask → task
└─ terminal callback or recordInfo
└─ output.assets[] → key + url + expiresAt (about 20 min)
└─ stream bytes to your storage → your asset ID and delivery URLLos SDK, la CLI y el Playground realizan por ti los pasos de carga, por lo que rara vez tendrás que escribirlos a mano. Cada representación tiene una función. El ticket autoriza una escritura directa durante su vigencia; la confirmación fija los bytes aceptados. El URI spicy:// permite que una tarea haga referencia durante un día a una entrada confirmada y limitada a la cuenta. El key de salida identifica un resultado generado. La url autoriza temporalmente una lectura. Tu propio ID de recurso es la única identidad duradera que debería exponer tu producto.
Subir la referencia a SpicyAPI es gratis; pagas por la tarea de generación que la usa. Actualizar un enlace a una salida existente no genera otro cargo por generación. Si pierdes la salida y decides volver a generarla, esa tarea nueva tendrá su propio cargo de modelo. Presupuesta por separado el almacenamiento, las solicitudes y la transferencia de tu contenedor duradero según el plan de tu proveedor de almacenamiento.
Entradas: carga y prevé la caducidad de 1 día
Conserva la imagen, el vídeo o el audio de referencia original en tu propio almacenamiento. Sube una entrada local mediante SDK, CLI o Playground y reutiliza su URI confirmado mientras siga siendo válido. Vuelve a subirla cuando la necesites tras caducar; el URI temporal no es tu archivo permanente.
Tres formas de proporcionar una entrada a un modelo:
- Dirección HTTPS pública. Funciona si el campo del modelo acepta una URL, siempre que devuelva el propio archivo. Un enlace a una unidad en la nube, un álbum o una red social devuelve una página web y la tarea falla con
invalid_asset; descarga el archivo y súbelo en su lugar. - Imagen pequeña integrada. JPEG, PNG, WebP o GIF como URI de datos base64, hasta 1 MiB cada una.
- Carga. Para archivos locales. Imágenes (JPEG, PNG, WebP, GIF) de hasta 10 MiB; audio y vídeo WAV, MP3, WebM, MP4 o MOV de hasta 90 MiB y 600 segundos. Un modelo puede aceptar menos: consulta el límite del campo en su página. En el TypeScript SDK,
await client.uploadFile("./reference.png")devuelve el URIspicy://comofile.uri.
Nunca expongas la clave de SpicyAPI en un navegador; haz que tu servidor emita los tickets de carga. La documentación de medios explica la integración con el navegador y todos los límites.
Salidas: cómo es un resultado terminado
Una tarea finalizada y un archivo recuperable son estados distintos:
- Listo: copia el archivo a tu almacenamiento y registra su ID de tarea, formato y tamaño.
- Pendiente: conserva la misma tarea y consulta de nuevo; no compres otra generación para obtener el archivo.
- No disponible: la copia de la plataforma ya no existe y esperar no la recuperará. Usa tu propia copia guardada; de lo contrario, genera de nuevo, con la facturación normal. Solo queda sin cargo un fallo inicial de almacenamiento sin archivos entregados.
Los enlaces temporales dan acceso a quien los posee. No los incluyas en páginas públicas ni en analítica; obtén un enlace actual cuando el proceso de copia esté preparado.
El límite de retención
Un registro de tarea, una clave de salida o un enlace de descarga actualizado no conservan los bytes. Cópialos antes de que venza el plazo del medio.
Cuando se cierra la ventana, recordInfo informa contentState: "expired", y input y output se vacían como máximo a los 30 días. Si tú destruiste el contenido de la tarea, el estado es purged. El estado de tarea expired significa algo distinto: la generación agotó su tiempo y no produjo nada. No muestres ambos con la misma etiqueta en la interfaz: uno requiere un reintento y el otro no se puede recuperar reintentándolo.
Fija el plazo de copia bastante antes del límite. Copia en el primer evento de éxito y ejecuta después un proceso de conciliación que enumere las tareas completadas sin recurso duradero y que avise mientras al resultado aún le queden días. El día 14 es un margen de recuperación, no un calendario. Si una configuración de cuenta o solicitud acorta la retención, outputsExpireAt puede llegar mucho antes de los 14 días: genera las alertas desde ese campo y nunca desde una constante. Un ajuste de cuenta también puede afectar al pasado: acortar el plazo del texto del prompt elimina los resultados de tareas ya terminadas antes del outputsExpireAt que recibieron. Descarga lo que quieras conservar antes de cambiar la configuración de cuenta y espera que la conciliación marque esas tareas como perdidas, no como errores de la canalización.
Una canalización de callbacks que soporta reintentos y vídeos grandes
En una aplicación, verifica el evento de finalización, guárdalo y encola la copia antes de responder. Haz que la copia sea idempotente para que un evento repetido no cree otro elemento de galería. Avisa de los resultados sin copiar antes de su caducidad aceptada.
Este proceso autónomo guarda los archivos de una tarea existente en un directorio local privado. Instala spicyapi, configura SPICY_API_KEY, TASK_ID y, opcionalmente, ARCHIVE_DIR, y ejecútalo. No crea ninguna generación. Almacena ese directorio en un soporte persistente e inclúyelo en tu plan de copias de seguridad; un disco efímero de servidor no es un archivo duradero.
import hashlib
import json
import os
from pathlib import Path
import tempfile
import urllib.request
from spicyapi import SpicyClient, output_assets
client = SpicyClient()
task_id = os.environ["TASK_ID"]
root = Path(os.environ.get("ARCHIVE_DIR", "media-archive"))
root.mkdir(parents=True, exist_ok=True, mode=0o700)
task = client.get_task(task_id)
if task["state"] != "succeeded":
raise RuntimeError("Resume the existing task before archiving it.")
for index, asset in enumerate(output_assets(task)):
if asset.get("pending") or asset.get("unavailable") or not asset.get("url"):
raise RuntimeError("Asset is not ready; retain the task ID.")
request = urllib.request.Request(asset["url"], headers={"User-Agent": "SpicyAPI-Blog-Example/1.0"})
temporary = None
try:
with urllib.request.urlopen(request, timeout=300) as response:
mime = response.headers.get_content_type()
suffix = {"image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp",
"video/mp4": ".mp4", "video/webm": ".webm", "audio/mpeg": ".mp3", "audio/wav": ".wav"}.get(mime)
if response.status != 200 or suffix is None:
raise RuntimeError("Unexpected response format; do not guess an extension.")
digest, size = hashlib.sha256(), 0
with tempfile.NamedTemporaryFile(dir=root, delete=False) as output:
temporary = Path(output.name)
while chunk := response.read(1024 * 1024):
output.write(chunk)
digest.update(chunk)
size += len(chunk)
if not size or (asset.get("bytes") and size != int(asset["bytes"])):
raise RuntimeError("Empty or incomplete transfer.")
filename = root / (digest.hexdigest() + suffix)
temporary.replace(filename)
record = {"taskId": task_id, "index": index, "file": filename.name,
"mime": mime, "bytes": size, "sha256": digest.hexdigest()}
(root / (digest.hexdigest() + ".json")).write_text(json.dumps(record, indent=2))
print(filename)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)Para servir desde almacenamiento de objetos, transmite a un objeto privado provisional y expónlo solo cuando la transferencia esté completa. La documentación de medios y de webhooks explica la actualización de resultados y los eventos autenticados. Mantén el sondeo como vía de conciliación para callbacks perdidos.
Tu recurso duradero debe ocultar la ruta de generación
Una vez copiado, el objeto que sirve tu producto tiene su propia identidad:
asset_01k... database identity
generated/job_.../....mp4 private storage key
https://media.example.com/a/... controlled delivery URLMantén privado el contenedor y entrega mediante tu propio CDN, un endpoint de medios autenticado o tus propios enlaces de corta duración. No reutilices una URL firmada de SpicyAPI como URL pública. Así no aparecen enlaces temporales ni detalles de almacenamiento en HTML, registros o exportaciones, puedes cambiar reglas de acceso o dominios sin reescribir el historial de tareas y puedes eliminar un recurso de usuario actuando sobre tu propio registro. Guarda la procedencia de forma privada: ID de tarea de origen, clave original del recurso, tipo de contenido, tamaño, hora de copia y clave de destino.
Fallos y acciones de recuperación
| Fallo | Qué observas | Recuperación correcta |
|---|---|---|
| Ticket de carga caducado antes de terminar o archivo sin confirmar utilizado en una tarea | Se rechaza la carga o confirmación, o la creación de tarea rechaza la referencia | Solicita un ticket nuevo y sube de nuevo; un 409/429 por saturación de la confirmación implica reintentar la misma confirmación |
URI spicy:// guardado utilizado al día siguiente | 400 al crear la tarea o un reintento falla | Sube de nuevo el original; consérvalo en tu propio almacenamiento |
| Callback entregado dos veces | Dos procesos inician la misma copia | Elimina duplicados mediante request_id, y mediante ID de tarea más índice del recurso en la cola y el destino |
| Endpoint del callback lento | Llegan reintentos durante una descarga | Guarda, encola y devuelve 2xx antes de mover los medios |
| Enlace al resultado caducado en la cola | La lectura del origen devuelve 403 | Consulta de nuevo la tarea para obtener una url nueva y reintenta la transmisión |
El recurso muestra pending: true | key y url están vacíos | Espera unos segundos y consulta de nuevo |
El recurso muestra unavailable: true o download-url devuelve 410 | Caducado, eliminado o no se pudo almacenar | Usa tu copia privada verificada; si no existe, informa al usuario y genera de nuevo como tarea nueva facturada |
| La copia terminó, pero falló la escritura en la base de datos | Existe el objeto privado, pero no un registro de recurso listo | Verifica el destino existente y reintenta escribir los metadatos; nunca elimines una copia válida ni regeneres |
| La base de datos indica listo, pero falta el objeto | Página rota o 404 del CDN | Concilia los registros con el contenedor antes de marcar como listo |
| Una URL firmada aparece en los registros | Se expone una capacidad temporal de lectura | Oculta las cadenas de consulta y los cuerpos de respuesta; los enlaces caducan en unos 20 minutos, pero no se pueden revocar |
| Vídeo grande almacenado en memoria | El proceso se reinicia o agota la memoria | Transmite con concurrencia limitada y control de presión de retorno |
Lista para el lanzamiento en producción
- Recupera antes de caducar: avisa de copias ausentes usando el plazo aceptado de cada tarea.
- Deja clara la propiedad: sirve recursos privados solo a su propietario y recoge los resultados antes de rotar las claves de API.
- Elimina en todos tus sistemas: incluye contenedor, caché de entrega y copias de seguridad en la política de eliminación.
Preguntas frecuentes
¿Puedo guardar la URL devuelta en output.assets[].url?
Solo para la propia copia. Guarda tu propio registro de recurso y obtén un enlace nuevo cuando necesites mover bytes.
¿Sigo necesitando download-url?
Solo para actualizar un enlace mediante taskId y key durante la ventana de retención; los recursos listos ya incluyen url y expiresAt.
¿Actualizar un enlace amplía la retención del medio?
No. Un enlace nuevo solo renueva el acceso de lectura; el objeto se elimina igualmente en retention.outputsExpireAt.
¿Puedo pasar una URL pública de imagen al modelo en lugar de subirla?
Sí, si el esquema del modelo permite una URL en ese campo, siempre que la dirección devuelva el propio archivo y siga accesible durante la tarea. Usa la carga para archivos locales, entradas más grandes o contenido sin alojamiento público.
Necesito la misma imagen de referencia la semana que viene. ¿Qué hago?
Súbela de nuevo desde tu propia copia cuando la necesites la semana que viene. Una referencia confirmada solo funciona durante 1 día, pero puede servir para varias tareas mientras sea válida, salvo que se elimine antes.
¿Debo copiar el resultado dentro de la solicitud del callback?
No. Una transferencia grande dentro de la solicitud supera el tiempo de espera del callback de 15 segundos y provoca entregas duplicadas; un proceso en cola puede reintentar de forma segura.
¿Y si no quiero conservar un resultado?
No lo copies y deja que caduque, o reduce la retención o destruye el contenido de una tarea a petición. Estos controles se explican en Cargas, retención y entrega de medios de IA y en la documentación de retención. Destruir contenido nunca cambia lo que se te ha cobrado.
Deja que un Agent se encargue de la llamada
Usa un Agent para recuperar una salida existente en lugar de crearla otra vez.
- CLI:
npx @spicyapi/cli tasks get "$TASK_ID"recupera la tarea. Consulta la guía de CLI. - MCP: solicita la salida actual mediante
spicyapi_task_gety guarda el archivo devuelto. Consulta la guía de MCP. - Skill: instala
npx skills add spicyapi-ai/spicy-skilly pide a$spicyapique archive el resultado de esa tarea en el directorio elegido. Consulta la guía de Skill.







