Ir al contenido principal

Tu API de IA devolvió una clave de recurso. Ahora conserva los medios de forma duradera

Los resultados se conservan unos 14 días y las referencias 1 día. Consulta qué caduca y cómo copiar los archivos a tu almacenamiento desde un callback.

Autor
SpicyAPI Engineering
Publicado
Lectura
13 min de lectura
Actualizado
Una mano desliza una tarjeta fotográfica luminosa desde una base temporal con un reloj de arena hasta una bahía de disco junto a un monitor de edición

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:

  1. Escribe una escena breve en Prompt y elige un Aspect ratio.
  2. Pulsa Run model. El resultado aparece en Output.
  3. Descarga la imagen y guárdala en un lugar que tú controles.
  4. 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

ElementoDuraciónQué ocurre al terminar
Ticket de carga (uploadUrl)Unos 20 minutos; tanto PUT como la confirmación deben terminar en ese plazoUna 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ónEl URI se rechaza en tareas nuevas y reintentos
Enlace al resultado (output.assets[].url)Unos 20 minutos, sin superar nunca el plazo del propio medioEl almacenamiento devuelve 403; consulta de nuevo la tarea para obtener un enlace nuevo
Imagen, vídeo o audio generadoUnos 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 tarea30 díasSe vacían input y output; el registro de tarea permanece
Registro de tarea (ID, modelo, estado, coste, marcas de tiempo)Se conserva indefinidamenteSigue 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

text
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 URL

Los 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 URI spicy:// como file.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.

python
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:

text
asset_01k...                     database identity
generated/job_.../....mp4        private storage key
https://media.example.com/a/...  controlled delivery URL

Manté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

FalloQué observasRecuperación correcta
Ticket de carga caducado antes de terminar o archivo sin confirmar utilizado en una tareaSe rechaza la carga o confirmación, o la creación de tarea rechaza la referenciaSolicita 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 siguiente400 al crear la tarea o un reintento fallaSube de nuevo el original; consérvalo en tu propio almacenamiento
Callback entregado dos vecesDos procesos inician la misma copiaElimina duplicados mediante request_id, y mediante ID de tarea más índice del recurso en la cola y el destino
Endpoint del callback lentoLlegan reintentos durante una descargaGuarda, encola y devuelve 2xx antes de mover los medios
Enlace al resultado caducado en la colaLa lectura del origen devuelve 403Consulta de nuevo la tarea para obtener una url nueva y reintenta la transmisión
El recurso muestra pending: truekey y url están vacíosEspera unos segundos y consulta de nuevo
El recurso muestra unavailable: true o download-url devuelve 410Caducado, eliminado o no se pudo almacenarUsa 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 datosExiste el objeto privado, pero no un registro de recurso listoVerifica 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 objetoPágina rota o 404 del CDNConcilia los registros con el contenedor antes de marcar como listo
Una URL firmada aparece en los registrosSe expone una capacidad temporal de lecturaOculta 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 memoriaEl proceso se reinicia o agota la memoriaTransmite 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_get y guarda el archivo devuelto. Consulta la guía de MCP.
  • Skill: instala npx skills add spicyapi-ai/spicy-skill y pide a $spicyapi que archive el resultado de esa tarea en el directorio elegido. Consulta la guía de Skill.

También en English · 日本語 · 한국어 · Deutsch · Français · Português (Brasil) · Русский · Italiano · Polski