Uma geração bem-sucedida ainda pode virar uma imagem quebrada no seu produto se você salvar a URL temporária de entrega em vez de copiar o arquivo. Links assinados expiram em minutos; a saída da API é mantida por cerca de duas semanas.
Uma API de mídia entrega o mesmo arquivo sob várias identidades: uma referência de entrada, uma chave da saída gerada e uma URL assinada que permite buscar os bytes por alguns minutos. Cada uma tem seu próprio prazo de validade, e nenhuma é um endereço permanente. Este guia mostra as janelas exatas da SpicyAPI e como copiar resultados valiosos para um armazenamento que você controla. Os controles de retenção podem encurtar essas janelas.
Experimente primeiro no Playground
Abra a página de Z-Image de texto para imagem e entre na sua conta:
- Digite uma cena curta em Prompt e escolha um Aspect ratio.
- Pressione Run model. O resultado aparece em Output.
- Baixe a imagem e salve em um lugar que você controla.
- Abra o arquivo salvo novamente mais tarde. Ele abre sem a página nem o link temporário do resultado; é isso que o restante do guia automatiza para produção.
Pontos principais
- Copie na conclusão, em um processo de trabalho. Mantenha downloads grandes fora da requisição de callback.
- Mantenha links assinados privados. Possuir um deles concede acesso temporário ao arquivo.
- Exponha seu próprio ID de recurso e URL de entrega. Um registro de tarefa pode durar mais que sua mídia.
O que expira e quando
| Item | Prazo | O que ocorre no fim |
|---|---|---|
Ticket de upload (uploadUrl) | Cerca de 20 minutos; o PUT e a confirmação precisam terminar dentro desse prazo | Uma resposta de ticket expirado exige um novo ticket; uma confirmação ocupada também pode retornar 409 |
Referência confirmada (spicy://f/fil_...) | 1 dia desde a confirmação, até o expiresAt da resposta de confirmação | URI rejeitada em novas tarefas e novas tentativas |
Link do resultado (output.assets[].url) | Cerca de 20 minutos, nunca além do prazo da própria mídia | O armazenamento responde 403; consulte a tarefa novamente para obter um link novo |
| Imagem, vídeo ou áudio gerado | Cerca de 14 dias desde a conclusão (menos se você configurar assim) | Excluído; nenhum link pode ser emitido (410 de download-url) |
| Texto do prompt e caminhos dos arquivos do resultado na tarefa | 30 dias | input e output são limpos; o registro da tarefa permanece |
| Registro da tarefa (ID, modelo, estado, custo, horários) | Mantido indefinidamente | Ainda pode ser consultado, sem conteúdo |
A mídia de curtas-metragens criada dentro de um projeto Spicy Studio tem sua própria janela mais longa, de cerca de 60 dias; gerações pela API, SDK e Playground usam o limite de 14 dias acima. A retenção das gerações também pode ser reduzida por requisição ou conta, como explica o artigo sobre controles de retenção. Você só pode encurtar esses limites, nunca ampliá-los.
Os 14 dias contam da conclusão, não do envio. Antes de a tarefa terminar, o horário é provisório e é recalculado na conclusão.
“Cerca de” é intencional. A limpeza do armazenamento segue uma regra de ciclo de vida e pode atrasar até um dia após o prazo, mas esse atraso não dá tempo extra a você: assim que a janela fecha, nenhum link de download é emitido.
Um arquivo, quatro representações
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 URLOs SDKs, a CLI e o Playground executam as etapas de upload para você, então raramente é necessário escrevê-las manualmente. Cada representação tem uma função. O ticket autoriza uma gravação direta durante sua validade; a confirmação fixa os bytes aceitos. A URI spicy:// permite que uma tarefa referencie uma entrada confirmada e restrita à conta por um dia. O key de saída identifica um resultado gerado. A url autoriza temporariamente uma leitura. Seu ID de recurso é a única identidade de longa duração que o produto deve expor.
Enviar a referência para a SpicyAPI é grátis; você paga pela tarefa de geração que a usa. Atualizar um link de uma saída existente não cria outra cobrança de geração. Se a saída for perdida e você decidir gerar novamente, essa nova tarefa terá sua própria cobrança do modelo. Reserve separadamente os custos de armazenamento, requisições e transferência do seu bucket permanente conforme o plano do fornecedor de armazenamento.
Entradas: envie e planeje a expiração de 1 dia
Mantenha a imagem, o vídeo ou o áudio de referência original no seu armazenamento. Envie uma entrada local pelo SDK, CLI ou Playground e reutilize sua URI confirmada enquanto for válida. Envie novamente quando precisar após a expiração; a URI temporária não é seu arquivo permanente.
Três maneiras de fornecer uma entrada a um modelo:
- Endereço HTTPS público. Funciona quando o campo do modelo aceita uma URL, desde que ela retorne o próprio arquivo. Um link de drive na nuvem, álbum ou rede social retorna uma página web, e a tarefa falha com
invalid_asset; baixe o arquivo e envie-o em vez disso. - Imagem pequena embutida. JPEG, PNG, WebP ou GIF como URI de dados base64, até 1 MiB cada.
- Upload. Para arquivos locais. Imagens (JPEG, PNG, WebP, GIF) de até 10 MiB; áudio e vídeo WAV, MP3, WebM, MP4 ou MOV de até 90 MiB e 600 segundos. Um modelo pode aceitar menos, então confira o limite do campo na sua página. No TypeScript SDK,
await client.uploadFile("./reference.png")retorna a URIspicy://emfile.uri.
Nunca exponha a chave da SpicyAPI em um navegador; faça seu servidor emitir os tickets de upload. A documentação de mídia explica a integração com navegadores e todos os limites.
Saídas: como é um resultado concluído
Uma tarefa concluída e um arquivo recuperável são estados diferentes:
- Pronto: copie o arquivo para seu armazenamento e registre ID da tarefa, formato e tamanho.
- Pendente: mantenha a mesma tarefa e consulte novamente; não compre outra geração para obter o arquivo.
- Indisponível: a cópia da plataforma se foi, e esperar não vai recuperá-la. Use sua cópia salva; caso contrário, gere novamente, com cobrança normal. Apenas uma falha inicial de armazenamento sem arquivos entregues não é cobrada.
Links temporários dão acesso a quem os possui. Mantenha-os fora de páginas públicas e ferramentas de análise; obtenha um link atual quando o processo de cópia estiver pronto.
O limite da retenção
Um registro de tarefa, uma chave de saída ou um link de download atualizado não preserva os bytes. Copie-os antes do prazo da mídia.
Quando a janela fecha, recordInfo informa contentState: "expired", e input e output são limpos no máximo aos 30 dias. Se você destruiu o conteúdo da tarefa, o estado é purged. O estado de tarefa expired é diferente: significa que a geração excedeu o prazo e não produziu nada. Não mostre ambos com o mesmo rótulo na interface: um pede nova tentativa, enquanto o outro não pode ser recuperado tentando de novo.
Defina o prazo da cópia bem antes do limite. Copie no primeiro evento de sucesso e execute depois uma rotina de reconciliação que liste tarefas bem-sucedidas sem recurso permanente e alerte enquanto ainda restam dias ao resultado. O dia 14 é uma margem de recuperação, não um cronograma. Se uma configuração de conta ou requisição encurtar a retenção, outputsExpireAt pode chegar muito antes dos 14 dias; baseie os alertas nesse campo, nunca em uma constante. Uma configuração de conta também pode agir retroativamente: encurtar o prazo do texto do prompt exclui resultados de tarefas já concluídas antes do outputsExpireAt que receberam. Baixe o que deseja guardar antes de alterar as configurações da conta e espere que a reconciliação relate essas tarefas como perdidas, não como falhas do pipeline.
Um pipeline de callbacks que suporta novas tentativas e vídeos grandes
Em uma aplicação, verifique o evento de conclusão, persista-o e coloque a cópia na fila antes de responder. Faça a cópia ser idempotente para que um evento repetido não crie outro item na galeria. Alerte sobre resultados não copiados antes da expiração aceita.
Este processo autônomo salva os arquivos de uma tarefa existente em um diretório local privado. Instale spicyapi, defina SPICY_API_KEY, TASK_ID e, opcionalmente, ARCHIVE_DIR, e execute-o. Ele não cria nenhuma geração. Guarde o diretório em armazenamento persistente e inclua-o no plano de backup; um disco efêmero de servidor não é um arquivo permanente.
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 entrega por armazenamento de objetos, transmita para um objeto intermediário privado e exponha-o só após a transferência completa. A documentação de mídia e de webhooks explica a atualização de resultados e eventos autenticados. Mantenha a sondagem como caminho de reconciliação para callbacks perdidos.
Seu recurso permanente deve ocultar o caminho da geração
Depois da cópia, o objeto servido pelo seu produto tem sua própria identidade:
asset_01k... database identity
generated/job_.../....mp4 private storage key
https://media.example.com/a/... controlled delivery URLMantenha o bucket privado e entregue pelo seu CDN, por um endpoint de mídia autenticado ou por seus próprios links de curta duração. Não reutilize uma URL assinada da SpicyAPI como URL pública. Isso mantém links temporários e detalhes de armazenamento fora de HTML, logs e exportações, permite alterar regras de acesso ou domínios sem reescrever o histórico de tarefas e torna a exclusão de um recurso do usuário uma operação no seu próprio registro. Guarde a procedência de forma privada: ID da tarefa de origem, chave original do recurso, tipo de conteúdo, tamanho, horário da cópia e chave de destino.
Falhas e ações de recuperação
| Falha | O que você observa | Recuperação correta |
|---|---|---|
| Ticket expirado antes do fim do upload ou arquivo não confirmado usado na tarefa | Upload ou confirmação rejeitados, ou criação de tarefa recusa a referência | Peça um ticket novo e envie novamente; 409/429 por confirmação ocupada significa repetir a mesma confirmação |
URI spicy:// salva usada no dia seguinte | 400 na criação da tarefa ou nova tentativa falha | Envie o original novamente; mantenha os originais no seu armazenamento |
| Callback entregue duas vezes | Dois processos iniciam a mesma cópia | Elimine duplicatas por request_id e por ID da tarefa mais índice do recurso na fila e no destino |
| Endpoint de callback lento | Chegam novas tentativas durante um download | Persista, enfileire e retorne 2xx antes de mover a mídia |
| Link do resultado expirado na fila | Busca na origem retorna 403 | Consulte a tarefa novamente para uma nova url e repita a transmissão |
Recurso mostra pending: true | key e url estão vazios | Espere alguns segundos e consulte novamente |
Recurso mostra unavailable: true ou download-url retorna 410 | Expirou, foi excluído ou não pôde ser armazenado | Use sua cópia privada verificada; se não houver, informe o usuário e gere novamente como uma tarefa nova cobrada |
| Cópia terminou, mas gravação no banco falhou | Objeto privado existe, sem registro de recurso pronto | Verifique o destino existente e repita a gravação dos metadados; nunca exclua uma cópia válida nem gere de novo |
| Banco indica pronto, mas objeto está ausente | Página quebrada ou 404 do CDN | Reconcilie as linhas do banco com o bucket antes de marcar como pronto |
| URL assinada aparece nos logs | Uma capacidade temporária de leitura foi exposta | Oculte parâmetros de consulta e corpos de resposta; os links expiram em cerca de 20 minutos, mas não podem ser revogados |
| Vídeo grande armazenado na memória | Processo reinicia ou fica sem memória | Transmita com concorrência limitada e controle de contrapressão |
Lista para lançamento em produção
- Recupere antes da expiração: alerte sobre cópias ausentes usando o prazo aceito de cada tarefa.
- Deixe a propriedade explícita: entregue recursos privados apenas ao proprietário e recolha os resultados antes de rotacionar as chaves de API.
- Exclua em todos os seus sistemas: inclua bucket, cache de entrega e backups na política de exclusão.
Perguntas frequentes
Posso salvar a URL retornada em output.assets[].url?
Só para a própria cópia. Salve seu próprio registro de recurso e obtenha um link novo quando precisar mover bytes.
Ainda preciso de download-url?
Só para atualizar um link por taskId e key dentro da janela de retenção; recursos prontos já trazem url e expiresAt.
Atualizar um link aumenta o tempo de retenção da mídia?
Não. Um link novo só renova o acesso de leitura; o objeto ainda é excluído em retention.outputsExpireAt.
Posso passar uma URL pública de imagem para o modelo em vez de enviar o arquivo?
Sim, quando o esquema do modelo permite URL nesse campo, desde que o endereço retorne o próprio arquivo e continue acessível durante a tarefa. Use o upload para arquivos locais, entradas maiores ou conteúdo sem hospedagem pública.
Preciso da mesma imagem de referência na semana que vem. O que faço?
Envie novamente a partir da sua cópia quando precisar na semana que vem. Uma referência confirmada funciona apenas por 1 dia, mas pode atender várias tarefas enquanto válida, a menos que seja excluída antes.
Devo copiar o resultado dentro da requisição de callback?
Não. Uma transferência grande dentro da requisição ultrapassa o timeout de callback de 15 segundos e dispara entregas duplicadas; um processo na fila pode tentar novamente com segurança.
E se eu não quiser guardar um resultado?
Não o copie e deixe-o expirar, ou encurte a retenção ou destrua o conteúdo de uma tarefa sob demanda. Esses controles estão em Uploads, retenção e entrega de mídia de IA e na documentação de retenção. Destruir conteúdo nunca altera o que foi cobrado.
Deixe um Agent cuidar da chamada
Use um Agent para recuperar uma saída existente em vez de criá-la novamente.
- CLI:
npx @spicyapi/cli tasks get "$TASK_ID"recupera a tarefa. Veja o guia da CLI. - MCP: peça a
spicyapi_task_geta saída atual e salve o arquivo retornado. Veja o guia MCP. - Skill: instale
npx skills add spicyapi-ai/spicy-skille peça a$spicyapipara arquivar o resultado da tarefa no diretório escolhido. Veja o guia de Skill.







