spicyapiDocumentação
Desenvolver com a API
Conteúdo principal

Mídia

Uploads diretos com URLs pré-assinadas para mídia de referência e links de curta duração para baixar arquivos gerados.

Muitos modelos recebem uma imagem, um vídeo ou um trecho de áudio como entrada, uma referência, e um modelo de mídia retorna um arquivo, um arquivo gerado. Esta página trata das duas direções:

  • Entradas de referência: envie uma URL HTTPS pública, uma pequena imagem diretamente na solicitação ou um arquivo por upload. Para fazer upload, solicite uma autorização, envie o arquivo por PUT diretamente ao armazenamento de objetos e confirme o envio; a confirmação retorna um URI spicy:// que você coloca na entrada da tarefa.
  • Downloads de arquivos gerados: acesse o output.assets[].url assinado retornado com o resultado. Nem todo modelo produz um arquivo: um endpoint cujo resultado é texto responde em output.text e não tem assets, portanto não há nada para baixar nem link para expirar. Veja Formato da saída.

Autorizações de upload e downloads de arquivos gerados ficam em /api/v1/common/; a confirmação de uploads usa /api/v1/files/{fileId}/commit. O upload é gratuito; você paga apenas pela tarefa que usa o arquivo.

Mídia já disponível em HTTPS

Envie diretamente uma URL HTTPS pública de imagem, vídeo ou áudio no campo aceito pelo modelo, como image_url. Não é necessário fazer upload à plataforma nem converter para spicy://. A URL precisa permanecer acessível sem cabeçalhos de autorização personalizados durante a execução da tarefa. Arquivos locais maiores usam o fluxo de upload abaixo.

A URL precisa retornar o próprio arquivo. Um link para uma página de compartilhamento, como um drive, álbum de fotos ou publicação em rede social, retorna uma página web em vez da imagem, e a tarefa falha com invalid_asset. Nesse caso, baixe o arquivo e envie-o por upload.

Base64 e arquivos locais

Imagens pequenas podem ser enviadas diretamente em campos de imagem declarados no schema, como image_url: "data:image/png;base64,...". Inclua o tipo MIME e use Base64 padrão. Este caminho não aceita Base64 sem o prefixo, codificação URL-safe, espaços em branco, SVG nem áudio ou vídeo inline.

  • JPEG, PNG, WebP e GIF: no máximo 1 MiB após decodificação por imagem, 8192 pixels por lado e 8.388.608 pixels por imagem.
  • No máximo 16 imagens e 16.777.216 pixels somados por solicitação. O corpo JSON inteiro precisa caber em 2 MiB, incluindo o aumento de tamanho de aproximadamente um terço causado pelo Base64.
  • O schema e os limites de mídia específicos do modelo continuam valendo. Orçamentos validam sem fazer upload nem reservar saldo. A criação armazena as imagens como uploads vinculados à conta, que duram um dia, como qualquer outro upload; a entrada da tarefa contém URIs de arquivos, em vez de repetir os bytes Base64.

Para arquivos maiores, o SDK cuida da autorização, do PUT e da confirmação:

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 é um utilitário de upload de arquivos que usa a mesma API de upload e os limites abaixo. O limite de 1 MiB para pequenas imagens inline não se aplica a esse utilitário; cada modelo ainda precisa aceitar o tipo de mídia escolhido. Mídia já disponível em HTTPS público não precisa de utilitário de upload.

1. Upload direto de referências

Para um arquivo local de referência que ainda não esteja hospedado em uma URL HTTPS pública, o upload tem quatro etapas. Os comandos curl abaixo devem ser executados em sequência na mesma janela do terminal, no macOS ou Linux com jq instalado: cada etapa salva em uma variável o que a próxima precisa. No Windows, use o SDK ou o exemplo de Python ao fim desta seção.

Solicitar uma autorização

POST /api/v1/common/upload-url

Propriedade

Tipo

Não há campo de nome de arquivo, e isso é intencional

As chaves dos objetos sempre são geradas no servidor. Permitir que quem chama escolha um caminho daria a qualquer pessoa a possibilidade de emitir uma URL de upload para o objeto de outra pessoa e sobrescrevê-lo.

Limites de upload

contentTypeExtensãoTamanho máximoDuração 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—

.mov, formato padrão de gravação da câmera do iPhone, é aceito e convertido para MP4 na entrada; assim, o modelo sempre recebe MP4. O upload web e a API de upload o aceitam; a CLI, o SDK e o servidor MCP inferem o tipo pela extensão e ainda não o listam. Neles, informe contentType: video/quicktime explicitamente.

Os pesos LoRA são enviados como application/octet-stream: um arquivo .safetensors de até 2 GiB (o upload web aceita até 90 MiB). O ticket vale por duas horas, em vez de 20 minutos, e o commit confere se o arquivo é um safetensors completo. Coloque o URI devolvido em loras[].path; um arquivo de pesos não pode ser usado em nenhum outro campo. Um arquivo que exige login para baixar, como um de um repositório privado do Hugging Face, pode ser importado a partir do link: veja Pesos de adaptador.

Um modelo pode aceitar menos: respeite x-ui.max_size_mb do schema e trate o maxBytes da autorização como o limite oficial deste upload.

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)

Você deve ver uma resposta semelhante a esta; UPLOAD_URL e FILE_ID passam a conter seu uploadUrl e 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 fica cerca de 20 minutos adiante. Tanto o PUT quanto a confirmação precisam terminar antes desse horário; depois, a confirmação retorna 409, e você precisa de outra autorização. Solicite-a imediatamente antes do upload, não com antecedência.

Enviar o arquivo por PUT

Envie os bytes por PUT para uploadUrl. Clientes fora do navegador, como curl, Node.js e Python, devem enviar todos os cabeçalhos de headers exatamente como fornecidos. Clientes de navegador precisam tratar Content-Length conforme explicado abaixo.

Omitir um cabeçalho invalida a assinatura

Tanto Content-Type quanto Content-Length são assinados. Alterar um byte de qualquer um faz o armazenamento de objetos recusar o upload com um 403 emitido pela camada de armazenamento, não por nós, o que pode parecer confuso.

Navegadores tratam Content-Length como cabeçalho de solicitação proibido, portanto o código do aplicativo não pode defini-lo por fetch nem por XMLHttpRequest. No navegador, envie um File ou Blob com tamanho exatamente igual ao bytes da autorização e deixe o navegador gerar Content-Length a partir do corpo; defina você apenas os demais cabeçalhos da autorização. A resposta ainda lista Content-Length para clientes fora do navegador e para que o código do navegador confira o tamanho antes do envio.

Não envie um cabeçalho Authorization nesta solicitação. A URL pré-assinada é a credencial; um cabeçalho adicional de autenticação faz algumas implementações de armazenamento recusarem a chamada.

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 o upload

Um PUT bem-sucedido só significa que um objeto temporário foi gravado. Continue usando o fileId da primeira etapa:

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

Espere "status": "ready" e um uri que comece com 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"
}

A confirmação compara o tamanho e o tipo de mídia armazenados com a autorização, verifica a assinatura do formato da mídia, calcula SHA-256 e copia os bytes para um objeto privado que a autorização antiga não pode sobrescrever. Para áudio e vídeo, também pode retornar durationSeconds como string decimal e width e height numéricos. Repetir uma confirmação bem-sucedida é idempotente.

RespostaSignificadoCorreção
400, code: 40003O tamanho, o tipo ou o formato do arquivo armazenado não corresponde ao declaradoSolicite outra autorização com contentType correto e bytes exatos e envie novamente
400O arquivo ainda não está lá: o PUT não terminou ou falhouConfira se o PUT retornou 200 e confirme novamente
404Não existe esse fileId na contaUse o fileId da sua própria autorização
409A autorização ou o arquivo enviado expirou, ou uma confirmação anterior recusou esse arquivoRecomece na etapa 1
409 / 429A verificação desse arquivo já está em andamento ou o verificador está ocupadoEspere um segundo e confirme novamente

Um PUT precisa ser seguido da confirmação

Um arquivo não confirmado não pode ser usado para criar uma tarefa. Não use o caminho temporário de armazenamento da autorização nem o campo de compatibilidade key antes de a confirmação ser concluída com sucesso.

Colocar o URI spicy:// na entrada

Use somente data.uri da resposta de confirmação como campo de referência, nunca uploadUrl, uma chave de objeto temporário ou o fileId isolado.

O campo que recebe um arquivo depende do modelo. No inputSchema, um campo de arquivo tem x-ui.widget definido como upload, para um arquivo, ou multi-upload, para uma lista; nesse caso, coloque o URI dentro de uma lista. x-ui.accept informa os tipos de arquivo e x-ui.max_size_mb, o tamanho aceito pelo modelo. Veja Catálogo de modelos e schemas.

MODEL_ID_FROM_CATALOG e os demais campos de entrada abaixo são marcadores estruturais; selecione o model exato e uma entrada válida pelo schema do mesmo item do catálogo autenticado em tempo real. A tarefa é determinada pelo próprio model; por isso, input não contém um campo de endpoint. Ele aceita somente os campos do inputSchema desse modelo, e um campo a mais retorna 400:

{
  "model": "MODEL_ID_FROM_CATALOG",
  "input": {
    "prompt": "slow dolly in, rain on the window",
    "image_url": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "duration_seconds": 5
  }
}

A plataforma resolve um URI spicy:// confirmado quando a tarefa é executada. Isso mantém os arquivos enviados utilizáveis enquanto uma tarefa aguarda na fila, sem exigir que você gerencie URLs temporárias de download. Use sem alterações o URI retornado pela confirmação.

Um URI que não está pronto, expirou ou pertence a outra conta é recusado na criação com 400.

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

Um limite adicional de solicitações

Além do limite geral da API pública, upload-url tem uma proteção própria de 1.000 solicitações por 10 segundos contra autorizações abusivas ou descontroladas de gravação no armazenamento de objetos.

O custo aqui é assimétrico: assinar uma autorização é apenas cálculo local para nós, mas cada autorização permite uma gravação que não passa mais pelo nosso serviço. Uma pessoa escolhendo arquivos não chega a esse ritmo, e um script que envia algumas dezenas de referências em lote também fica bem abaixo dele.

2. Baixar resultados

Resultados prontos já incluem output.assets[].url e expiresAt nas consultas de status e nos callbacks v2. Baixe diretamente pela URL, sem chave de API; o download-url abaixo é opcional. Os links normalmente duram 20 minutos e nunca ultrapassam o prazo de retenção da tarefa, limitado a cerca de 14 dias e menor se você o reduzir.

Para guardar um resultado, salve-o no seu próprio armazenamento enquanto o link for válido. Por exemplo:

curl -L -o result.png "PASTE_THE_OUTPUT_URL_HERE"

Não adicione um cabeçalho Authorization a essa solicitação. Se o link expirou e o armazenamento responde 403, consulte a tarefa novamente com recordInfo para obter outro.

Renovação opcional de link de download

POST /api/v1/common/download-url

Propriedade

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

Para uma tarefa com vários arquivos gerados, faça uma chamada por key. Assim como recordInfo, isso funciona somente com a chave de API que criou a tarefa.

Uma URL assinada é uma string que comprova sua própria autorização: o armazenamento de objetos não verifica novamente quem a possui. Portanto:

  • Não a armazene no banco de dados nem em um cache duradouro servido a navegadores. Assine uma quando precisar; a assinatura é cálculo local e, na prática, gratuita.
  • Baixe tudo que quiser guardar. Os arquivos gerados ficam no nosso armazenamento por no máximo cerca de 14 dias, e por menos tempo se você reduzir o prazo; veja Retenção e destruição.
SituaçãoO que apareceO que fazer
Arquivo ainda sendo copiado para nosso armazenamentoO item mostra pending: true em recordInfo; download-url sem key retorna 409; com key explícita, retorna 404 até a cópia terminarEspere alguns segundos e consulte novamente
A cópia falhou definitivamenteO item mostra unavailable: true e não tem keyEsperar não resolve; gere novamente
A tarefa ainda não foi concluída com sucesso404, “This task has no downloadable output”Aguarde state: "succeeded"
O prazo de retenção terminou ou você destruiu o conteúdoHTTP 410 com code: 404, “This output has expired or was deleted”O arquivo foi perdido definitivamente; gere novamente

Tanto uma tarefa que não é sua quanto uma key que não pertence à tarefa retornam 404. Não diferenciamos “não existe” de “pertence a outra pessoa”, pois essa diferença pode ser usada para enumerar IDs de tarefas de outras pessoas.

Biblioteca de ativos

Alguns arquivos você usa o tempo todo: um retrato de personagem, uma referência de estilo, uma foto de produto, uma amostra de voz. A biblioteca de ativos guarda esses arquivos até você excluí-los e dá a cada um uma referência estável, spicy://a/<id>. Coloque essa referência em qualquer campo de mídia de qualquer entrada de tarefa, como você faz com um upload spicy://f/, só que sem a expiração de um dia.

  • Privada ao seu workspace. Uma referência spicy://a/ funciona apenas em requisições feitas com chaves do mesmo workspace. Uma chave pessoal chega à sua biblioteca pessoal; uma chave criada em um workspace de equipe chega à biblioteca compartilhada dessa equipe, e as duas nunca se conectam (veja abaixo).
  • Guardada até ser excluída. Os resultados gerados são limpos após cerca de 14 dias e os uploads após 1 dia; os ativos salvos ficam até você excluí-los. Uma biblioteca pessoal some quando você encerra a conta, e uma biblioteca de equipe some junto com a equipe.
  • Não é hospedagem de arquivos. A API entrega a referência spicy://a/<id> e uma previewUrl assinada só para você, válida por no máximo 20 minutos. Não existe link público nem permanente, e uma referência não pode virar um. Guarde uri, nunca previewUrl.

Bibliotecas pessoal e de equipe

A chave decide a biblioteca. Uma chave criada no seu workspace pessoal chega à sua biblioteca pessoal. Uma chave criada em um workspace de equipe, por qualquer membro, chega à biblioteca compartilhada dessa equipe. Uma chave de equipe nunca chega à biblioteca pessoal de ninguém, uma chave pessoal nunca chega a uma biblioteca de equipe, e uma referência spicy://a/ da outra biblioteca é rejeitada como se não existisse.

O que uma chave de equipe pode alterar segue o papel atual do membro que a criou:

PapelSalvarRenomear ou excluirListar, ler e usar
owner, adminSimQualquer ativoSim
developerSimSó os que salvouSim
billing, readonlyNãoNãoSim

Se esse membro saiu da equipe, a chave recebe 403 com data.reason igual a asset_workspace_forbidden. Os ativos não mostram quem os salvou; você só vê se pode alterar um.

Salvar um arquivo

POST /api/v1/assets copia um arquivo para a sua biblioteca. O cabeçalho Idempotency-Key é obrigatório. Escolha uma das três origens:

sourceCamposObservações
task_outputtaskId, e key ou indexUma tarefa sua concluída com sucesso. index começa em 0, na ordem de output.assets em recordInfo.
uploadfileIdUm upload confirmado: fil_… ou spicy://f/fil_….
urlurlUm endereço http ou https público na porta 80 ou 443, sem usuário e senha. O arquivo é baixado uma vez e o tipo é lido do próprio arquivo; só são aceitos imagens, vídeo e áudio.

name é opcional (até 80 caracteres; padrão Saved asset). Salvar a mesma origem de novo devolve o ativo que você já tem, sem ocupar mais espaço. Tamanho, dimensões e duração são medidos por nós.

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

Usar em uma tarefa

Coloque data.uri em qualquer campo de arquivo que o inputSchema do modelo marque com x-ui.widget igual a 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
  }
}

Uma referência excluída, ou que esteja em uma biblioteca que esta chave não alcança (a de outra conta ou a do outro workspace), é rejeitada na criação da tarefa, exatamente como um ID desconhecido.

Listar, renomear e excluir

GET /api/v1/assets devolve seus ativos em items, mais um nextCursor que não aparece na última página. Filtre com kind (image, video ou audio), source (separadas por vírgula: task_output,upload,url), q (busca no nome, sem diferenciar maiúsculas de minúsculas), favorite (true só para favoritos, false para o resto) e collectionId (uma pasta, veja abaixo). Ordene com sort: recent (padrão, do mais novo para o mais antigo), oldest, name (de A a Z, sem diferenciar maiúsculas de minúsculas) ou size (do maior para o menor). limit vai de 1 a 100 e o padrão é 100. Para a página seguinte, passe o nextCursor anterior como cursor; um cursor só funciona com o sort de onde veio, e um cursor de outra ordenação é rejeitado com 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} devolve um ativo com uma previewUrl nova. PATCH altera o nome, o indicador favorite ou os dois (envie só o que quer mudar); a uri continua a mesma. Com DELETE, o ativo some na hora e a uri deixa de funcionar em novas requisições. As duas chamadas podem ser repetidas com segurança e não precisam de Idempotency-Key. Um ativo desconhecido e o ativo de outra conta devolvem o mesmo 404.

Favoritos, pastas e o modelo que gerou

Todo ativo tem um indicador favorite. Defina-o com PATCH e {"favorite": true}. Em uma biblioteca de equipe, a marca pertence à biblioteca, então todos veem os mesmos favoritos. As pastas (na API, chamadas de coleções) agrupam ativos sem copiá-los, e um ativo pode estar em várias pastas. Você cria e gerencia as pastas na página Assets do aplicativo web; a API não permite criá-las nem editá-las, mas collectionId em GET /api/v1/assets lista os ativos de uma pasta (404 quando a pasta não está na biblioteca que sua chave alcança).

Todo ativo também traz model, o nome público do modelo que o gerou, e modelName, o nome de exibição dele. Em uploads e links, os dois são strings vazias.

Personagens

Um personagem é um nome mais um conjunto ordenado de ativos de imagem da mesma biblioteca: até 100 imagens e uma nota opcional de até 280 caracteres. As imagens são os próprios ativos e nada é copiado. Excluir um personagem mantém as imagens, e excluir uma imagem a remove de todos os personagens. Você cria e edita personagens na página Assets do aplicativo web ou no Studio. Eles não usam armazenamento próprio: só conta o seu plano da biblioteca.

A API apenas lê personagens:

  • GET /api/v1/assets/entities lista os personagens, do mais novo para o mais antigo. assetId mantém só os personagens que contêm esse ativo, limit vai de 1 a 100 (padrão 50) e cursor continua a partir do nextCursor anterior.
  • GET /api/v1/assets/entities/{id} devolve um personagem. Um personagem desconhecido, o de outra conta e o de uma biblioteca que sua chave não alcança devolvem o mesmo 404.

Cada personagem traz id, kind (sempre character), name, note, cover (a imagem de capa como ativo, ou null enquanto não houver imagens), assets (as imagens em ordem, cada uma com sua uri e uma previewUrl privada), count, createdAt e updatedAt. A chave decide a biblioteca, como nos ativos, e a leitura de personagens divide com a de ativos o limite de 60 requisições por minuto.

Para usar um personagem, coloque as imagens dele, nessa ordem, como referências spicy://a/ no campo de imagens de referência do modelo e cite-as no prompt do jeito que a página desse modelo descreve. Os personagens se chamavam personas e ficavam em uma biblioteca separada; agora fazem parte da biblioteca de ativos. As referências spicy://p/ existentes continuam funcionando, só na sua biblioteca pessoal.

Ao adicionar a um personagem uma imagem que você mesmo enviou (e não um resultado gerado), você confirma que tem os direitos sobre ela e, se aparecer uma pessoa real, que tem o consentimento dessa pessoa.

Planos de armazenamento

O armazenamento é liberado automaticamente com as suas recargas acumuladas e só aumenta; uma mudança na tabela nunca exclui ativos que você já salvou. A referência para a sua conta é GET /api/v1/assets/usage: seu plano, o que você armazenou, os limites, o próximo plano e quanto falta recarregar para chegar a ele, e quantos ativos você criou hoje. A tabela mostra os valores padrão atuais.

O armazenamento pertence ao proprietário: as recargas acumuladas dele definem o plano, e a biblioteca pessoal dele e todas as bibliotecas de equipe que ele possui dividem uma única cota. usage cobre toda essa cota e traz um objeto scope (workspaceId, personal, role, canWrite, canTopUp) que diz qual biblioteca a sua chave alcança e o que ela pode fazer ali. Quando canTopUp é false (uma chave de equipe não criada pelo proprietário da equipe), lifetimeTopupUsd e topupToNextTierUsd são omitidos, e uma biblioteca cheia pede que você fale com o proprietário da equipe em vez de informar um valor.

PlanoRecargas acumuladasArmazenamentoItensPor arquivo
Freenenhumanenhum: nada novo pode ser salvo0não se aplica
Plusa partir de $10500 MB200100 MB
Proa partir de $1001 GB400100 MB
Studioa partir de $2502 GB800100 MB
Maxa partir de $1.00010 GB4.000100 MB

Sem nenhuma recarga você não consegue salvar nada novo, mas os ativos que já salvou continuam utilizáveis. O limite de 100 MB por arquivo vale para saídas de tarefas e links. Um arquivo que chega por upload continua sujeito aos limites do próprio upload (imagens 10 MiB, áudio e vídeo 90 MiB).

Limites de taxa e erros

  • Leitura (listar, consultar, usage): 60 requisições por minuto. Salvamento: 30 requisições por minuto e 200 ativos novos por dia UTC. Excluir um ativo não devolve a cota do dia. Ao atingir um limite, você recebe 429 com o cabeçalho Retry-After.
  • Biblioteca cheia, armazenamento ainda não liberado ou arquivo acima do limite por arquivo retornam 400 com invalid_request. A mensagem informa seu plano atual e quanto você precisa recarregar para liberar o próximo.
  • 403 com data.reason igual a asset_workspace_forbidden quando a chave não alcança a biblioteca, ou asset_permission_denied quando o seu papel na equipe não permite a alteração. 503 quando o armazenamento da plataforma está temporariamente cheio; tente de novo mais tarde.

Uma falha nesses endpoints pode trazer um data.reason legível 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 ou asset_entity_name_taken. code, o status HTTP e errorCode não mudam com ou sem ele, e o motivo aparece só nos endpoints da biblioteca de ativos; a camada compatível com OpenAI e as demais camadas de compatibilidade mantêm seus próprios formatos de erro. Trate um valor desconhecido como motivo ausente. Veja Códigos de motivo da biblioteca de ativos.

O aplicativo web também tem um interruptor de salvamento automático na biblioteca. Ele vale apenas para gerações iniciadas no aplicativo web; as iniciadas pela API nunca são salvas automaticamente, então chame POST /api/v1/assets para as que você quiser guardar.

Retenção de uploads

Um URI de referência enviado por upload é retido por 1 dia após uma confirmação bem-sucedida. Esse é um limite da plataforma que você pode reduzir ou substituir pela destruição ao concluir, em Retenção e destruição; o URI só pode ser usado até o expiresAt retornado pela confirmação. Ele se torna inutilizável ao expirar, inclusive em uma nova tentativa de uma tarefa que o referenciou. A exclusão pelo ciclo de vida do armazenamento de objetos pode levar até mais 24 horas, mas esse intervalo de limpeza não estende a validade do URI. Se precisar da mesma referência novamente, guarde uma cópia própria e envie-a de novo.

Nesta página