spicyapiDocumentação
SDKs
Conteúdo principal

SDK para TypeScript

Um guia passo a passo do SDK para TypeScript @spicyapi/sdk — instale, obtenha seu primeiro resultado e consulte cada método, opção e erro em linguagem simples. Os SDKs para Python, Go, PHP e Java estão documentados em páginas próprias.

O @spicyapi/sdk é a biblioteca oficial da SpicyAPI para TypeScript e JavaScript. Esta página parte do princípio de que você nunca a usou. Ela leva você de uma pasta vazia até uma imagem salva no disco e depois descreve cada função do pacote: para que serve, o que você passa, o que volta, se custa dinheiro e o que pode dar errado.

O que é o SDK

Um SDK é uma biblioteca de código pronta que você adiciona ao seu próprio programa. Sem ele, o seu programa precisa montar cada requisição HTTP à mão: anexar a chave de API, transformar a resposta de JSON em dados, perguntar de novo e de novo se uma tarefa já terminou, tentar outra vez quando a rede cai, enviar arquivos em três etapas separadas e conferir a assinatura de cada webhook. O SDK faz tudo isso por você, e uma geração inteira cabe numa única chamada:

const task = await client.run({ model: modelId, input: { prompt: "A paper lantern at night" } });

Ele é para você se você escreve, ou está prestes a escrever, um programa em Node.js (JavaScript ou TypeScript) que roda num servidor, num script no seu próprio computador ou num processo em segundo plano. Quem usa TypeScript ganha ainda autocompletar e verificação de tipos em todos os campos.

Esta é a ferramenta certa?

Você quer…Use
Chamar a SpicyAPI a partir do seu próprio código Node.js ou TypeScriptEste SDK
Gerar conteúdo pelo terminal, sem escrever códigoA CLI
Deixar um assistente de IA, como Claude Code ou Cursor, chamar a SpicyAPI por vocêO servidor MCP e o Agent Skill
Usar um modelo de conversa ou de textoO cliente oficial da OpenAI apontado para a SpicyAPI — veja Texto e streaming
Usar Python, Go, PHP ou JavaO SDK para Python, o SDK para Go, o SDK para PHP ou o SDK para Java oficiais
Usar qualquer outra linguagemChamadas HTTP diretas ou um cliente gerado a partir da referência da API

O que ele não faz

  • Não roda no navegador nem em aplicativo de celular. Ele precisa de Node.js, e a sua chave de API nunca pode sair do seu servidor: qualquer pessoa consegue extrair uma chave de uma página da web ou de um aplicativo. Faça o seu site chamar o seu próprio backend, e o backend chamar a SpicyAPI.
  • Não trabalha com modelos de conversa. A geração de texto usa endpoints compatíveis com OpenAI, e o pacote oficial openai já faz isso muito bem. Veja Texto e streaming.
  • Não escolhe o modelo nem preenche os campos por você. Cada modelo aceita os seus próprios campos. O SDK entrega a lista de modelos e o schema de cada um (a lista de campos que ele aceita); quem decide o que enviar é você.
  • Não cancela tarefas. Depois que a SpicyAPI aceita uma tarefa, ela vai até o fim. Encerrar o seu programa só interrompe a sua espera.
  • Não administra a sua conta. Adicionar saldo, criar chaves, limites de gasto e chaves de assinatura de webhook ficam no console.

Sua chave de API fica no seu servidor

Nunca coloque SPICY_API_KEY em JavaScript de navegador, num aplicativo de celular, num repositório público, numa captura de tela ou numa mensagem de chat. Uma chave vazada pode gastar o seu saldo até você desativá-la. O SDK lê a chave de uma variável de ambiente, então ela nunca precisa aparecer no seu código.

Termos usados nesta página

TermoO que significa aqui
ID do modeloO nome exato de um modelo, no formato publisher/model/task. Copie sempre do catálogo atual (listModels()), nunca da memória
Entrada (input)As configurações que você envia com uma tarefa, como prompt ou image_url. Cada modelo tem a sua própria lista de campos permitidos, chamada schema
TarefaUm trabalho de geração. Você a cria, a SpicyAPI trabalha nela em segundo plano e você busca o resultado. Cada tarefa tem um ID que começa com job_
ReservaQuando uma tarefa é aceita, o preço máximo dela é separado do seu saldo. Tarefas que falham ou expiram normalmente liberam toda a reserva. Algumas recusas de conteúdo são cobradas pelo orçamento aceito, somente quando a página do modelo informa explicitamente essa exceção. Confira settled e cost para saber o valor final.
Chave de idempotênciaUm rótulo que você inventa para um trabalho, como order-8814-image-1. Se você enviar a mesma requisição duas vezes com o mesmo rótulo, a SpicyAPI devolve a primeira tarefa em vez de criar e cobrar uma segunda
WebhookUm endereço web seu que a SpicyAPI chama quando uma tarefa termina, para você não precisar ficar perguntando

O que você pode fazer

Todas as funções abaixo são métodos do cliente (client.listModels()), salvo indicação em contrário. "Custa dinheiro" significa que a chamada pode gerar uma cobrança no seu saldo.

O que você quer fazerMétodoCusta dinheiro?
Conferir se a SpicyAPI está acessível (sem chave)getStatusNão
Ver a lista atual de modelos, os seus preços e os campos de cada modelolistModels, getModelNão
Saber o preço exato de uma requisição antes de gastar qualquer coisaquoteTaskNão
Iniciar uma geraçãocreateTaskSim
Iniciar uma geração e esperar o resultado numa única chamadarunSim
Consultar uma tarefa uma vezgetTaskNão
Esperar até uma tarefa terminarwaitForTaskNão
Tentar de novo uma tarefa que falhouretryTaskSim (é uma tarefa nova)
Listar as tarefas criadas com a sua chavelistTasksNão
Consultar o seu saldogetBalanceNão
Ver quanto você gastou, por dia e por modelogetUsageNão
Enviar um arquivo local de imagem, vídeo ou áudiouploadFile, uploadBytes, uploadBase64Não
Obter um link de download novo para um resultadocreateDownloadUrlNão
Apagar os arquivos e o prompt guardados de uma tarefa concluídapurgeTaskNão (e não há reembolso)
Conferir se um webhook veio mesmo da SpicyAPIverifyWebhook (uma função avulsa)Não
Pesquisar a documentação ou ler o contrato da API sem internetsearchDocumentation, readOpenApiContract (funções avulsas)Não

Antes de começar

Você vai precisar de uns dez minutos. Cada passo explica o que o comando faz, para que nada rode às cegas.

Instale o Node.js 22.13 ou mais recente

O Node.js é o programa que roda JavaScript fora do navegador. Instale a versão LTS em nodejs.org; qualquer versão a partir da 22.13 funciona. Depois abra um terminal (o Terminal no macOS, o PowerShell no Windows) e confira:

node -v

O comando mostra a versão instalada, por exemplo v22.13.0 ou v24.8.0. Se o número for menor que 22.13, ou se o comando não for encontrado, instale o Node.js de novo e abra uma janela de terminal nova.

Crie uma pasta de projeto e instale o SDK

Rode um comando de cada vez. Eles funcionam do mesmo jeito no macOS, no Linux e no Windows PowerShell.

mkdir spicy-demo
cd spicy-demo
npm init -y
npm pkg set type=module
npm install @spicyapi/sdk
ComandoO que faz
mkdir spicy-demoCria uma pasta vazia para o seu projeto
cd spicy-demoLeva o terminal para dentro dessa pasta; os próximos comandos agem nela
npm init -yCria o package.json, o arquivo que lista as configurações e as bibliotecas do seu projeto
npm pkg set type=moduleAvisa o Node.js de que os seus arquivos usam a sintaxe moderna import, exigida por este SDK
npm install @spicyapi/sdkBaixa o SDK para uma pasta node_modules e o registra no package.json

O SDK é publicado apenas como módulo ES moderno: import { SpicyClient } from "@spicyapi/sdk" funciona, enquanto o antigo require("@spicyapi/sdk") falha com ERR_PACKAGE_PATH_NOT_EXPORTED. Se você pular o npm pkg set type=module, dê aos seus arquivos a extensão .mjs.

Crie uma chave de API

  1. Entre na sua conta em spicyapi.ai. Ainda não tem conta? Cadastre-se em spicyapi.ai/register; se os cadastros estiverem pausados, essa página oferece uma lista de espera.
  2. Abra a página Chaves de API do console e clique em Criar chave. Só o nome é obrigatório.
  3. Copie a chave na hora. Ela começa com sk-spicy- e aparece uma única vez. Se você a perder, crie outra.

Você pode dar à chave um limite de gasto diário na mesma página; chaves novas não têm nenhum a menos que você adicione um. Para rodar tarefas pagas você também precisa de saldo, que se adiciona em Faturamento.

Coloque a chave numa variável de ambiente

Uma variável de ambiente é um valor com nome que o terminal entrega a todo programa que ele inicia. O SDK procura uma chamada SPICY_API_KEY, então a chave nunca precisa ser escrita no seu código.

# Define a chave para esta janela de terminal (cole a sua própria chave)
export SPICY_API_KEY="sk-spicy-paste-your-key-here"

# Confira: deve aparecer sk-spicy-
echo "${SPICY_API_KEY:0:9}"

O valor some quando você fecha a janela. Para mantê-lo no seu próprio computador, adicione a linha export ao ~/.zshrc (macOS) ou ao ~/.bashrc (Linux) e abra uma janela nova.

Prefere um arquivo .env? Crie na pasta do projeto um arquivo chamado .env com uma única linha, SPICY_API_KEY=sk-spicy-paste-your-key-here, e inicie o seu programa com node --env-file=.env yourfile.mjs. O SDK não lê arquivos .env sozinho; quem faz isso é essa opção. Adicione .env ao seu arquivo .gitignore antes de rodar git add pela primeira vez, para que a chave nunca vá parar num commit.

Num servidor de verdade, defina SPICY_API_KEY nas configurações de segredos ou de ambiente do seu provedor de hospedagem, ou passe a chave que vem do seu gerenciador de segredos com new SpicyClient({ apiKey }).

Rode o seu primeiro arquivo

Na pasta do projeto, crie com qualquer editor de texto um arquivo chamado check.mjs, cole este conteúdo e salve:

check.mjs
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();
const status = await client.getStatus();

console.log(status.operational ? "SpicyAPI is up" : "SpicyAPI is not reachable right now");
console.log("Checked at", status.checkedAt);

Rode:

node check.mjs
SpicyAPI is up
Checked at 2026-09-17T08:00:00.000Z

Essa verificação não precisa de chave e não custa nada, então ela prova que o Node.js e o SDK estão instalados corretamente.

Usa TypeScript? Salve o mesmo código como check.ts. As versões recentes do Node.js 22 e 24 o executam diretamente com node check.ts. Se a sua reclamar da extensão .ts, rode node --experimental-strip-types check.ts ou npx tsx check.ts.

Seu primeiro resultado

Este programa faz o caminho completo: confere o serviço, escolhe um modelo de imagem que a sua chave pode usar, pede o preço exato e para. Rode de novo com --yes e ele cria a imagem, espera por ela e a salva ao lado do script.

A execução com --yes gasta dinheiro

Sem --yes, o programa só faz leituras: nada é criado e nada é cobrado. Com --yes, ele cria uma tarefa de imagem, que reserva o valor cotado e o cobra se a imagem der certo.

first-result.mjs
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { SpicyApiError, SpicyClient } from "@spicyapi/sdk";

// Lê a sua chave da variável de ambiente SPICY_API_KEY.
const client = new SpicyClient();

async function main() {
  // Passo 1 — A SpicyAPI está acessível? Grátis e não precisa de chave.
  const status = await client.getStatus();
  if (!status.operational) {
    throw new Error("SpicyAPI is not reachable right now. Try again in a few minutes.");
  }

  // Passo 2 — Quais modelos de imagem esta chave pode chamar? Grátis.
  const catalog = await client.listModels({ modality: "image", includeExamples: true });
  const model = catalog.items.find(
    (item) => item.enabled && item.available && item.examples?.length,
  );
  if (!model) throw new Error("No image model is available to this key right now.");
  console.log(`Model: ${model.displayName} (${model.model})`);

  // Passo 3 — Monte a entrada. Parta de um exemplo que o servidor já validou
  // e troque o prompt pelo seu, se o modelo aceitar um.
  const input = { ...model.examples?.[0]?.input };
  if ("prompt" in input) input.prompt = "A paper lantern glowing on a rainy street at night";

  // Passo 4 — Peça o preço exato. Grátis: nada é criado nem reservado.
  const request = { model: model.model, input };
  const quote = await client.quoteTask(request);
  console.log(`Price: $${quote.estimatedCost} USD`);

  if (!process.argv.includes("--yes")) {
    console.log("Nothing was charged. Run again with --yes to generate the image.");
    return;
  }

  // Passo 5 — Crie a tarefa e espere por ela. Este é o passo que custa dinheiro.
  const task = await client.run(
    { ...request, quoteId: quote.quoteId, expectedCost: quote.estimatedCost },
    {
      idempotencyKey: randomUUID(),
      onAccepted: (accepted) => console.log(`Task accepted: ${accepted.taskId}`),
      onUpdate: (update) => console.log(`State: ${update.state}`),
    },
  );

  // Etapa 6 — Uma tarefa que falhou é uma resposta normal. Confira a liquidação.
  if (task.state !== "succeeded") {
    console.log(`The task ended as ${task.state}: ${task.errorCode} ${task.errorMessage ?? ""}`);
    console.log("Check settled and cost for the final charge.");
    return;
  }

  // Passo 7 — Baixe o resultado agora. O link expira em cerca de 20 minutos.
  const asset = task.output?.assets?.find((item) => item.url);
  if (!asset?.url) throw new Error("The task succeeded but its file is unavailable.");
  const download = await fetch(asset.url); // nunca envie a sua chave de API aqui
  if (!download.ok) throw new Error(`Download failed with HTTP ${download.status}`);
  const extension = asset.mime?.split("/")[1] ?? "png";
  await writeFile(`result.${extension}`, Buffer.from(await download.arrayBuffer()));
  console.log(`Saved result.${extension}. Charged: $${task.cost}${task.settled ? "" : " (not final yet)"}`);
}

main().catch((error) => {
  if (error instanceof SpicyApiError) {
    console.error(`SpicyAPI error ${error.code}: ${error.message} (request_id ${error.requestId})`);
  } else {
    console.error(error);
  }
  process.exitCode = 1;
});

Rode duas vezes:

node first-result.mjs
node first-result.mjs --yes
Model: <display name> (<model ID>)
Price: $<amount> USD
Nothing was charged. Run again with --yes to generate the image.

Model: <display name> (<model ID>)
Price: $<amount> USD
Task accepted: job_…
State: running
State: succeeded
Saved result.png. Charged: $<amount>

O que cada passo faz

  1. getStatus() pergunta ao endpoint de saúde da SpicyAPI se o serviço está no ar. Ele nunca lança erro por problema de rede; em vez disso, informa operational: false.
  2. listModels() retorna o catálogo atual de modelos (live catalog) com os preços da sua conta. Use apenas itens em que enabled e available sejam ambos true. includeExamples: true acrescenta entradas de exemplo que o servidor já validou, que são o ponto de partida mais seguro. O valor que você envia depois é o campo model do item: o ID do modelo.
  3. A entrada. Cada modelo aceita campos diferentes; enviar um campo que o modelo não conhece é recusado com o erro 400. Copiar um exemplo validado e trocar só o prompt evita isso. Para ver todos os campos que um modelo aceita, chame getModel() e leia o inputSchema dele.
  4. quoteTask() calcula o preço desta requisição exata. Não cria nada e não reserva nada. Os valores são dólares americanos escritos como texto, como "0.04", para que nenhum arredondamento se infiltre.
  5. run() cria a tarefa e depois a consulta até ela terminar. Passar quoteId e expectedCost significa "só aceite pelo preço que me foi mostrado": se o preço mudou, a chamada falha com 40901 em vez de cobrar outro valor. O idempotencyKey torna segura uma nova tentativa depois de uma falha de rede; num aplicativo de verdade, salve a chave antes de chamar, para que um programa reiniciado possa reutilizá-la. onAccepted entrega o ID da tarefa no instante em que a tarefa passa a existir — salve-o, para poder consultar a tarefa mais tarde mesmo que o seu programa pare.
  6. O estado da tarefa. run() também retorna normalmente quando a tarefa falha. Tarefas que falham ou expiram normalmente liberam toda a reserva. Algumas recusas de conteúdo são cobradas pelo orçamento aceito, somente quando a página do modelo informa explicitamente essa exceção. Confira settled e cost para saber o valor final.
  7. O download. Cada resultado vem com uma url temporária. Baixe-a sem a sua chave de API e guarde a sua própria cópia: o link dura cerca de 20 minutos (chame getTask() de novo para receber um novo), e a SpicyAPI apaga os arquivos gerados depois de no máximo cerca de 14 dias. Veja Retenção e destruição.

Vídeo funciona do mesmo jeito, mas pode levar vários minutos; passe um timeoutMs maior para run() (a espera padrão é de 10 minutos). Nos trechos abaixo, o ID do modelo aparece como MODEL_ID_FROM_CATALOG: é um marcador, não um modelo real. Troque-o pelo valor model de um item do catálogo atual — listModels() no SDK, ou o endpoint autenticado GET /api/v1/models — em que enabled e available sejam ambos true.

Referência da API

Cada item abaixo diz para que serve o método, como chamá-lo, o que ele retorna, se pode custar dinheiro e quais erros esperar. As classes de erro, como SpicyApiError, são explicadas em Como tratar erros. Todos os valores são dólares americanos escritos como texto ("0.04"), e todos os horários são strings ISO 8601 em UTC.

Criar um cliente

new SpicyClient(options?: SpicyClientOptions)

A maioria dos programas não passa nada: new SpicyClient() lê a chave de SPICY_API_KEY e conversa com o serviço de produção. Uma opção inválida lança um TypeError na hora, antes de qualquer requisição ser enviada.

Propriedade

Tipo

Novas tentativas automáticas

O SDK só repete uma requisição por conta própria quando isso não tem como cobrar você duas vezes:

ChamadaRepetida automaticamente?
Leituras: getStatus, listModels, getModel, getTask, listTasks, getBalance, getUsage; e também quoteTask, purgeTask, commitUploadedFileSim
createTask, run (a etapa de criação), retryTaskSó quando você passa um idempotencyKey. Sem ele, um erro de rede chega direto a você, porque reenviar poderia criar uma segunda tarefa
createUploadUrl, o envio dos bytes em si, createDownloadUrlNão

Uma nova tentativa acontece quando não chega resposta nenhuma (erro de rede ou tempo esgotado localmente) ou quando a resposta é HTTP 408, 429, 500, 502, 503 ou 504. A pausa é a que o cabeçalho Retry-After do servidor pedir; se ele não pedir nada, 250 ms, depois 500 ms, depois 1 s e assim por diante, cada uma com variação de até 25% e limitada a maxRetryDelayMs. Quando todas as tentativas falham, você recebe o último erro.

Interromper uma chamada com signal

Todos os métodos aceitam a opção signal, um AbortSignal padrão. Use AbortSignal.timeout(5_000) para impor um prazo, ou um AbortController para interromper quando quiser. A chamada então lança o motivo da interrupção. Interromper só encerra a sua requisição ou espera: uma tarefa que a SpicyAPI já aceitou continua rodando e ainda pode ser cobrada.

getStatus()

Confere se a SpicyAPI está acessível. Útil como primeira linha de um script ou como verificação de saúde no seu próprio monitoramento.

getStatus(options?: { signal?: AbortSignal }): Promise<ServiceStatus>
Campo retornadoSignificado
operationaltrue quando o serviço está respondendo. É este o campo a usar
healthO resultado bruto da verificação de saúde: path, ok, status HTTP, os primeiros 512 caracteres de body e error quando nenhuma resposta chegou
readinessO mesmo formato, para uma segunda verificação. No serviço público ela responde 404, o que é normal e não significa que o serviço está fora do ar
checkedAtQuando a verificação rodou

Custa dinheiro: não. Precisa de chave: não. Erros: não lança erro por problemas de rede — você recebe operational: false e o motivo em health.error. Só lança se você o interromper com signal. Há um exemplo em Rode o seu primeiro arquivo.

listModels(options?)

Lista o catálogo atual de modelos, com os preços da sua conta. É daqui que vem todo ID de modelo.

listModels(options?: ListModelsOptions): Promise<ModelList>

Propriedade

Tipo

Retorna { total, items }. Os campos mais úteis de cada item:

CampoSignificado
modelO ID do modelo. Envie exatamente este valor ao criar uma tarefa
displayNameUm nome legível
modality, tasksO que ele produz e que tipo de trabalho faz
provider, familyQuem fez o modelo e a que linha de produtos ele pertence
enabled, availableOs dois precisam ser true antes de você usar o modelo
pricingUma entrada por faixa de preço: price, unit (per_image, per_second, per_request ou per_1k_tokens) e variant (qual faixa, muitas vezes uma resolução). Durante um desconto, regularPrice mostra o preço sem desconto
startingPriceA faixa mais barata. Para um valor exato, use quoteTask
quantityFieldO campo de entrada que define a quantidade cobrada, como uma duração. Vazio significa uma unidade por chamada
taskTimeoutSecondsQuanto tempo uma tarefa pode rodar antes de terminar como expired
maxOutputDurationSecondsA saída mais longa que o modelo permite, quando ela tem duração
inputSchema, examplesSó aparecem quando você os pediu
updatedAt, versionMudam sempre que os detalhes do modelo mudam; use-os para saber quando atualizar um schema que você guardou

Custa dinheiro: não. Precisa de chave: sim. Erros: SpicyApiError (por exemplo 401 para um problema com a chave, 429 para requisições demais), SpicyTransportError.

const { items } = await client.listModels({ modality: "video", task: "image-to-video" });

for (const item of items) {
  if (!item.enabled || !item.available) continue;
  const from = item.startingPrice ? `from $${item.startingPrice.price} ${item.startingPrice.unit}` : "";
  console.log(item.model, "|", item.displayName, "|", from);
}

As consultas ao catálogo usam a capacidade de leitura da conta, independentemente da criação de tarefas. Guarde a lista e atualize-a de vez em quando, ou depois de um 400 ou 50301, em vez de lê-la antes de cada tarefa.

getModel(modelId)

Retorna um único modelo com o schema completo: a lista de campos de entrada que ele aceita, quais são obrigatórios e quais valores são permitidos.

getModel(modelId: string, options?: { signal?: AbortSignal }): Promise<ApiModel>

O resultado tem todos os campos descritos em listModels, sempre com inputSchema e examples. O schema é JSON Schema padrão: properties lista cada campo com o seu type e, muitas vezes, enum (valores permitidos), minimum / maximum e default; required lista os campos que você precisa enviar.

Custa dinheiro: não. Precisa de chave: sim. Erros: SpicyApiError com 404 quando nenhum modelo tem esse ID; TypeError se o ID estiver vazio.

const model = await client.getModel("MODEL_ID_FROM_CATALOG");

console.log("Required fields:", model.inputSchema?.required);
console.log(JSON.stringify(model.inputSchema?.properties, null, 2));
console.log("A request that is known to work:", model.examples?.[0]?.input);

quoteTask(request)

Dá o preço exato de uma requisição sem criar nada. Use para mostrar um preço a uma pessoa ou para recusar trabalhos acima de um orçamento.

quoteTask(request: { model: string; input: object; callBackUrl?: string }, options?: { signal?: AbortSignal }): Promise<TaskQuote>

Passe os mesmos model, input e, se houver, callBackUrl com que você pretende criar a tarefa.

Campo retornadoSignificado
quoteIdPasse-o para createTask ou run para travar este preço
estimatedCostO valor que será reservado se você criar a tarefa agora
maxChargeO máximo que esta requisição pode custar
currencySempre USD
quantity, unitA quantidade cobrada, como "5" e per_second
expiresAtA cotação vale por cinco minutos

Custa dinheiro: não. Precisa de chave: sim. Erros: SpicyApiError, por exemplo 400 quando a entrada não bate com o schema do modelo, 50301 quando o modelo está indisponível, 429.

const request = { model: "MODEL_ID_FROM_CATALOG", input: { prompt: "A paper lantern at night" } };
const quote = await client.quoteTask(request);

console.log(`This will cost $${quote.estimatedCost}. Quote valid until ${quote.expiresAt}.`);

createTask(request, options?)

Inicia uma geração e retorna imediatamente com o ID da tarefa. Ele não espera o resultado: depois use waitForTask ou um webhook, ou então use run para fazer as duas coisas numa chamada só.

createTask(request: CreateTaskInput, options?: CreateTaskOptions): Promise<CreateTaskResult>

A requisição:

Propriedade

Tipo

As opções:

Propriedade

Tipo

Campo retornadoSignificado
taskIdO ID da nova tarefa. Salve-o na hora
statequeued para uma tarefa nova. Se você repetiu uma chave de idempotência, o estado atual da tarefa original
estimatedCostO valor agora reservado, que é também o máximo que esta tarefa pode cobrar
deadlineAtQuando a SpicyAPI deixa de trabalhar na tarefa, se ela não tiver terminado

Custa dinheiro: sim. O estimatedCost é reservado do seu saldo. Se a tarefa der certo, você é cobrado pelo uso real, nunca acima da reserva, e o restante é liberado. Tarefas que falham ou expiram normalmente liberam toda a reserva. Algumas recusas de conteúdo são cobradas pelo orçamento aceito, somente quando a página do modelo informa explicitamente essa exceção. Confira settled e cost para saber o valor final.

Erros:

error.codeO que significaO que fazer
400A entrada não bate com o schema do modelo, ou o callBackUrl não é permitidoCompare com getModel().inputSchema e corrija a requisição
401A chave está faltando, malformada ou revogadaConfira SPICY_API_KEY
40201O seu saldo não cobre a reservaAdicione saldo em Faturamento, no console
40202Um limite de gasto diário, mensal ou acumulado foi atingidoAumente o limite na página Chaves de API ou espere ele recomeçar
40301Esta chave não tem permissão para usar este modeloAltere a lista de modelos da chave no console
409Esta chave de idempotência já foi usada para outra requisiçãoUse uma chave nova para um trabalho novo
40901A cotação expirou, ou o preço ou a requisição mudaramPeça uma nova cotação e confirme o novo preço
413O corpo da requisição é grande demaisEnvie os arquivos de mídia por upload em vez de embuti-los
429Requisições demaisEspere error.retryAfterSeconds e tente de novo
50301O modelo não está aceitando tarefas agoraTente mais tarde ou escolha outro modelo

Um SpicyTransportError ou SpicyTimeoutError significa que nenhuma resposta chegou, então não dá para saber se a tarefa foi criada. Envie de novo a mesma requisição com a mesma chave de idempotência: ou você a cria, ou recebe de volta a tarefa que já existe. Um TypeError significa que alguma opção era inválida (por exemplo um retentionSeconds negativo) e nada foi enviado.

import { randomUUID } from "node:crypto";

const idempotencyKey = randomUUID(); // num aplicativo de verdade, salve isto com o seu pedido antes
const accepted = await client.createTask(
  { model: "MODEL_ID_FROM_CATALOG", input: { prompt: "A paper lantern at night" } },
  { idempotencyKey },
);

console.log(accepted.taskId, accepted.state, "on hold: $" + accepted.estimatedCost);

run(request, options?)

O jeito mais fácil de gerar alguma coisa: chama createTask, depois waitForTask, e retorna a tarefa concluída.

run(request: CreateTaskInput, options?: RunTaskOptions): Promise<TaskRecord>

request é igual ao de createTask. As opções são todas as que createTask aceita, mais:

Propriedade

Tipo

Retorna a tarefa concluída, com os campos descritos em getTask. Numa tarefa bem-sucedida, ele também espera os arquivos de resultado terminarem de ser copiados, para que output.assets[].url já esteja pronta para download.

Custa dinheiro: sim, exatamente como createTask. Uma tarefa que falhou é retornada, não lançada — confira task.state.

Erros: tudo o que createTask pode lançar, e ainda:

  • SpicyWaitTimeoutError quando o timeoutMs se esgota depois que a tarefa foi aceita. A tarefa continua rodando na SpicyAPI. Chame waitForTask(error.taskId) mais tarde para buscá-la.
  • SpicyTimeoutError quando o tempo acaba antes de a SpicyAPI confirmar a tarefa. Envie a mesma requisição com a mesma chave de idempotência para descobrir o que aconteceu.
  • Qualquer erro lançado dentro do seu próprio onAccepted ou onUpdate interrompe a espera; a tarefa em si continua rodando.
  • TypeError para um timeoutMs ou intervalMs inválido. Esses valores são conferidos antes de qualquer coisa ser criada.

Há um exemplo completo em Seu primeiro resultado.

getTask(taskId)

Lê uma tarefa uma vez: o estado dela, os links do resultado e quanto custou.

getTask(taskId: string, options?: { signal?: AbortSignal }): Promise<TaskRecord>
CampoSignificado
taskId, modelQual tarefa, feita com qual modelo
statequeued ou running enquanto ela trabalha; succeeded, failed ou expired quando terminou (canceled só aparece em registros antigos)
output.assetsOs arquivos de resultado, descritos na próxima tabela. Só em succeeded
output.textSaída em texto, para modelos que retornam texto
errorCode, errorMessagePor que uma tarefa falhou. Tome decisões pelo errorCode; a redação da mensagem pode mudar
costEnquanto settled for false, o valor reservado; quando for true, a cobrança final
settledSe o cost já é definitivo
createdAt, completedAt, deadlineAtQuando foi criada, quando terminou e quando expiraria
inputA entrada como foi armazenada. É removida quando termina o prazo de retenção do prompt
sourceTaskIdPresente numa tarefa criada por retryTask, apontando para a original
contentStatepresent; expired (removido pelas regras de retenção); ou purged (você o apagou com purgeTask)
retentionQuando os arquivos (outputsExpireAt) e o prompt (promptsExpireAt) serão removidos

Cada item de output.assets:

CampoSignificado
urlUm link de download temporário. Baixe-o sem a sua chave de API
expiresAtQuando esse link para de funcionar, normalmente em cerca de 20 minutos. Chame getTask de novo para receber um link novo
mimeO tipo do arquivo, como image/png ou video/mp4
width, height, durationSeconds, bytesDetalhes de tamanho, quando se aplicam
pendingtrue enquanto o arquivo ainda está sendo copiado para o armazenamento. Consulte de novo em alguns segundos
unavailabletrue quando o arquivo não existe mais. Esperar não adianta; gere de novo
keyUm nome de arquivo interno, necessário apenas para createDownloadUrl

Um campo para o qual a SpicyAPI não tem valor fica de fora em vez de vir como null, então trate todos os campos acima como opcionais.

Custa dinheiro: não. Erros: SpicyApiError com 404 quando nenhuma tarefa com esse ID está visível para a sua chave.

const task = await client.getTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3"); // o ID que você salvou

if (task.state === "succeeded") {
  for (const asset of task.output?.assets ?? []) console.log(asset.mime, asset.url);
} else if (task.state === "queued" || task.state === "running") {
  console.log("Still working. On hold: $" + task.cost);
} else {
  console.log(`Finished as ${task.state}: ${task.errorCode}`);
}

waitForTask(taskId, options?)

Consulta uma tarefa repetidamente até ela terminar e então a retorna. Use para uma tarefa que você criou antes, por exemplo depois que o seu programa reiniciou.

waitForTask(taskId: string, options?: WaitForTaskOptions): Promise<TaskRecord>

Opções: timeoutMs (padrão 600000, até 86400000), intervalMs, onUpdate e signal, todas como descritas em run.

Ele retorna quando o state é succeeded, failed, expired ou canceled e, numa tarefa bem-sucedida, quando nenhum arquivo de resultado continua pending. Cada consulta é uma requisição getTask e conta para o limite de requisições da sua conta; é por isso que o intervalo padrão vai ficando mais longo com o tempo.

Custa dinheiro: não. Erros: SpicyWaitTimeoutError quando o timeoutMs se esgota. Ele traz taskId, timeoutMs e lastTask (o último estado observado). A tarefa não é cancelada; basta esperar de novo. Também SpicyApiError (por exemplo 404) e SpicyTransportError.

const task = await client.waitForTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3", {
  timeoutMs: 30 * 60_000,
  onUpdate: (update) => console.log(new Date().toISOString(), update.state),
});
console.log(task.state, task.output?.assets?.[0]?.url);

retryTask(taskId, options?)

Roda de novo uma tarefa que falhou, como uma tarefa totalmente nova, com o mesmo modelo e a mesma entrada. A tarefa original fica exatamente como estava, e a nova aponta de volta para ela por meio de sourceTaskId.

retryTask(taskId: string, options?: { idempotencyKey?: string; signal?: AbortSignal }): Promise<RetryTaskResult>

Funciona com tarefas failed e expired (e com as antigas canceled). Uma tarefa que deu certo ou que ainda está rodando é recusada com 409. O preço, o seu saldo e as permissões da chave são conferidos de novo, e a nova tarefa mantém a configuração de retenção da original.

Como a entrada é reaproveitada sem mudanças, uma nova tentativa só ajuda quando a falha foi passageira. Leia antes o errorCode da tarefa original: rate_limited, upstream_unavailable, timeout e upstream_failed valem uma nova tentativa depois de uma pausa; generation_failed e invalid_asset pedem uma requisição alterada, enviada com createTask; content_rejected, invalid_request e unsupported_combination não devem ser repetidos sem mudanças. Veja Códigos de erro comuns e Erros.

Retorna taskId (a tarefa nova), sourceTaskId (a original), state, estimatedCost e deadlineAt.

Custa dinheiro: sim. A tarefa nova tem a sua própria reserva e é cobrada separadamente se der certo. Passe um idempotencyKey para que rodar o seu código de nova tentativa duas vezes crie uma única nova tentativa.

Erros: 409 (a tarefa não pode ser repetida), além dos erros de saldo, de limite e de disponibilidade listados em createTask. Se a entrada original usava um arquivo spicy:// enviado por upload, esse arquivo só pode ser usado por um dia; envie-o de novo e use createTask.

retryTask não serve para erros de rede

Se o próprio createTask falhou com um erro de rede, você não tem tarefa nenhuma para repetir. Envie o createTask de novo com a mesma chave de idempotência. Use retryTask só numa tarefa cujo state você leu como failed ou expired.

Há um exemplo completo em Repetir uma tarefa que falhou.

listTasks(options?)

Lista as tarefas criadas com esta chave de API, das mais novas para as mais antigas, uma página por vez. Útil para reencontrar trabalhos depois de um reinício ou de um webhook perdido.

listTasks(options?: ListTasksOptions): Promise<TaskList>

Propriedade

Tipo

Retorna items, hasMore e, quando há mais páginas, nextCursor. Cada item tem taskId, model, state, cost, settled, createdAt, deadlineAt e completedAt. Entradas, resultados e links não vêm incluídos; chame getTask para a tarefa de que você precisar.

Custa dinheiro: não. Erros: SpicyApiError (por exemplo 400 para um intervalo de datas inválido), SpicyTransportError.

let cursor;
do {
  const page = await client.listTasks({
    from: "2026-09-01", // mantenha as mesmas datas em todas as páginas
    to: "2026-09-08",
    state: "failed",
    limit: 100,
    cursor,
  });
  for (const item of page.items) console.log(item.taskId, item.model, item.createdAt);
  cursor = page.hasMore ? page.nextCursor : undefined;
} while (cursor);

getBalance()

Mostra quanto dinheiro a sua conta tem.

getBalance(options?: { signal?: AbortSignal }): Promise<Balance>
CampoSignificado
availableDinheiro que pode pagar novas tarefas. Pode ficar negativo quando a sua conta usa um limite de crédito aprovado
heldDinheiro reservado para tarefas que ainda estão rodando
totalavailable mais held
fundingDetalhamento opcional: saldo pré-pago, verbas promocionais (com os modelos em que valem e a validade) e um eventual limite de crédito. Respostas mais antigas podem não trazê-lo

Custa dinheiro: não. Você não precisa consultar o saldo antes de cada tarefa: a SpicyAPI o confere quando você cria uma e responde 40201 se ele não for suficiente. Veja Cobrança para todos os campos de funding.

const balance = await client.getBalance();
console.log(`Available: $${balance.available} (on hold: $${balance.held})`);

getUsage(options?)

Informa quanto esta chave de API gastou, agrupado por dia e por modelo.

getUsage(options?: { from?: string; to?: string; signal?: AbortSignal }): Promise<UsageReport>

from e to são datas em UTC (YYYY-MM-DD); from entra no intervalo e to não. Deixe os dois de fora para ver os últimos sete dias, incluindo hoje. O intervalo pode cobrir no máximo 92 dias.

CampoSignificado
totalCallsNúmero de tarefas no intervalo
totalSpendSó cobranças finais; dinheiro ainda reservado não entra na conta
daysUma entrada por dia com chamadas: day, calls, succeeded, failed, spend
modelsUma entrada por modelo: model, calls, succeeded, failed, spend

Custa dinheiro: não. Este relatório é mais pesado que as outras leituras, por isso tem um limite extra de 30 requisições por minuto; use-o para relatórios, não dentro de um laço. Erros: SpicyApiError (429 acima desse limite), SpicyTransportError.

const usage = await client.getUsage({ from: "2026-09-01", to: "2026-09-08" });
console.log(`${usage.totalCalls} tasks, $${usage.totalSpend} spent`);
for (const row of usage.models) console.log(row.model, row.calls, "$" + row.spend);

Enviar arquivos

Quando um modelo precisa de uma imagem, de um vídeo ou de um som como entrada, você pode colocar um link HTTPS público para o arquivo diretamente no campo de entrada. Para um arquivo que está no seu computador, envie-o primeiro. Você recebe de volta um endereço spicy://; coloque esse endereço no campo de entrada. O upload é grátis.

  • Imagens (JPEG, PNG, WebP, GIF): até 10 MiB.
  • Vídeo (MP4, WebM) e áudio (MP3, WAV): até 90 MiB e 600 segundos de duração.
  • Um modelo pode definir um limite menor no seu schema.
  • Um arquivo enviado pode ser usado por um dia, e só pela sua conta.

uploadFile(path, options?)

Envia um arquivo do disco.

uploadFile(path: string, options?: { contentType?: string; signal?: AbortSignal }): Promise<UploadedFile>

O tipo do arquivo é detectado pela extensão: .jpg, .jpeg, .png, .webp, .gif, .mp4, .webm, .mp3 ou .wav. Para qualquer outro nome, passe contentType: image/jpeg, image/png, image/webp, image/gif, video/mp4, video/webm, audio/mpeg ou audio/wav.

Campo retornadoSignificado
uriO endereço spicy:// a colocar na entrada de uma tarefa
expiresAtQuando o endereço para de funcionar (em um dia)
fileId, status, bytes, contentType, sha256Detalhes do que foi armazenado; status é ready
width, height, durationSecondsDimensões ou duração medidas, quando se aplicam

Custa dinheiro: não. Erros: TypeError quando o tipo não pode ser detectado — a mensagem lista todas as extensões reconhecidas, contentType is required unless the file extension is one of: gif, jpeg, jpg, png, webp, mp4, webm, mp3, wav; um erro ENOENT do Node.js quando o caminho está errado; SpicyUploadError (com status) quando o armazenamento recusou os bytes ou o arquivo é maior que o permitido (413); SpicyApiError quando a SpicyAPI recusou o upload; SpicyTimeoutError quando o envio demorou mais que uploadTimeoutMs.

const file = await client.uploadFile("./photo.jpg");
console.log(file.uri, "can be used until", file.expiresAt);

uploadBytes(data, options)

Envia bytes que você já tem na memória, como um arquivo que um usuário mandou para o seu servidor. Um Buffer do Node.js serve.

uploadBytes(data: Uint8Array, options: { contentType: string; signal?: AbortSignal }): Promise<UploadedFile>

contentType é obrigatório. O resultado e os erros são os mesmos de uploadFile.

const response = await fetch("https://example.com/reference.png");
const bytes = new Uint8Array(await response.arrayBuffer());
const file = await client.uploadBytes(bytes, { contentType: "image/png" });

uploadBase64(value, options?)

Envia um arquivo que você tem como texto Base64: ou um data URI, como data:image/png;base64,iVBOR…, ou Base64 puro acompanhado de contentType.

uploadBase64(value: string, options?: { contentType?: string; signal?: AbortSignal }): Promise<UploadedFile>

A codificação e o tamanho são conferidos primeiro na sua máquina; dados inválidos ou grandes demais lançam um TypeError sem enviar nada. O resultado é o mesmo de uploadFile.

createUploadUrl e commitUploadedFile

As três funções acima são construídas a partir destas duas chamadas de nível mais baixo. Você só precisa delas para enviar os bytes de outro lugar que não o seu processo Node.js.

createUploadUrl(contentType: string, bytes: number, options?: { signal?: AbortSignal }): Promise<UploadTicket>
commitUploadedFile(fileId: string, options?: { signal?: AbortSignal }): Promise<UploadedFile>
  1. createUploadUrl retorna fileId, uploadUrl, method (PUT), headers, maxBytes e expiresAt.
  2. Envie o arquivo com um PUT para uploadUrl, exatamente com os headers listados e sem cabeçalho Authorization. O tamanho declarado precisa bater com o do arquivo.
  3. commitUploadedFile(fileId) confere o arquivo armazenado e retorna a uri spicy://. Um arquivo enviado mas nunca confirmado não pode ser usado.

O fluxo completo, inclusive uploads feitos pelo navegador, está descrito em Mídia.

createDownloadUrl(taskId, key?)

Cria um link de download temporário novo para um arquivo de resultado. Você raramente vai precisar dele: getTask, waitForTask, run e os webhooks já trazem uma url pronta para cada resultado.

createDownloadUrl(taskId: string, key?: string, options?: { signal?: AbortSignal }): Promise<DownloadTicket>

Passe a key do asset para escolher um arquivo, ou deixe de fora para pegar o primeiro. Retorna key, url e expiresAt (cerca de 20 minutos).

Custa dinheiro: não. Erros: SpicyApiError com 409 enquanto o arquivo ainda está sendo copiado (espere alguns segundos), ou 404 quando a tarefa não deu certo ou a chave não pertence a ela.

purgeTask(taskId)

Apaga de forma permanente a mídia gerada, o resultado armazenado (os dados do resultado guardados), o prompt e os demais textos de entrada de uma tarefa concluída, antes do que o prazo de retenção faria. Use quando um resultado não pode ficar guardado.

purgeTask(taskId: string, options?: { signal?: AbortSignal }): Promise<PurgeTaskResult>
Campo retornadoSignificado
contentStatepurged quando o conteúdo foi apagado. present significa que nada foi removido desta vez; tente de novo mais tarde
purgedAtQuando o conteúdo foi apagado pela primeira vez
contentRemovedByuser quando foi você quem apagou
billingRetainedSempre true: o registro de quanto a tarefa custou é mantido
mediaDeletionPendingtrue enquanto os arquivos armazenados ainda estão sendo removidos em segundo plano, normalmente por menos de um minuto

Custa dinheiro: não — e não é um reembolso. O lançamento contábil, o valor cobrado, o modelo, o estado, os horários e o ID da requisição continuam registrados. A exclusão não pode ser desfeita. Chamar de novo na mesma tarefa é seguro. Erros: SpicyApiError com 400 quando a tarefa ainda está queued ou running, 404 quando ela não existe. Uma tarefa aceita não pode ser cancelada e não existe API de cancelamento, então espere por ela com waitForTask e depois faça o purge. Veja Retenção e destruição.

const result = await client.purgeTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3");
console.log(result.contentState, "billing kept:", result.billingRetained);

Funções de webhook

verifyWebhook e computeWebhookSignature são funções avulsas, não métodos do cliente. Elas são descritas com um exemplo funcional em Webhooks.

Utilitários offline e tipos

Estes funcionam sem conexão de rede e sem chave.

ExportO que oferece
searchDocumentation(query, limit)Pesquisa um pequeno índice embutido deste site de documentação. Retorna até limit entradas (de 1 a 25, padrão 10), cada uma com slug, title, summary, keywords e url, em que url tem o formato https://docs.spicyapi.ai/docs/<slug>. Sem correspondência, retorna um array vazio; uma query vazia retorna as primeiras entradas
DOCUMENTATIONEsse índice embutido inteiro como array, uma entrada por página da documentação
readOpenApiContract()O contrato OpenAPI 3.1 que acompanha esta versão do SDK, como texto YAML
DEFAULT_API_BASE_URL, DEFAULT_SERVICE_BASE_URL, DEFAULT_TIMEOUT_MS, DEFAULT_UPLOAD_TIMEOUT_MS, DEFAULT_WAIT_TIMEOUT_MS, DEFAULT_WAIT_INTERVAL_MS, MAX_API_RESPONSE_BYTESOs valores padrão descritos nesta página. Respostas da API maiores que MAX_API_RESPONSE_BYTES (4 MiB) são recusadas

Tipos TypeScript para todas as requisições e respostas também são exportados, por exemplo import type { TaskRecord, ApiModel, Balance } from "@spicyapi/sdk".

Caminho de importaçãoContém
@spicyapi/sdkTudo o que está nesta página
@spicyapi/sdk/webhooksSó verifyWebhook, computeWebhookSignature e SpicyWebhookVerificationError — uma importação pequena para um receptor de webhooks
@spicyapi/sdk/openapiSó os tipos OpenAPI brutos paths, components e operations

Webhooks

Um webhook faz a SpicyAPI avisar você quando uma tarefa termina, em vez de o seu programa perguntar a cada poucos segundos. Você dá a cada tarefa um callBackUrl, um endereço no seu próprio servidor, e a SpicyAPI envia para lá o registro da tarefa concluída.

Por que você precisa verificar todo webhook

O endereço do seu webhook é uma URL pública: qualquer pessoa que o adivinhe pode mandar para ele uma mensagem falsa dizendo "a tarefa deu certo, aqui está o arquivo". Se o seu código acreditar nessa mensagem, ele pode entregar o arquivo errado a um cliente ou marcar um pedido como concluído.

Toda entrega verdadeira vem assinada com uma chave de assinatura secreta que só você e a SpicyAPI conhecem. verifyWebhook recalcula essa assinatura a partir dos bytes exatos que você recebeu e compara as duas. Ele também confere o horário da entrega, para que um atacante não consiga capturar uma mensagem legítima e reenviá-la para você horas depois. Só aja sobre uma mensagem depois que ela passar nessa verificação.

Configure

Obtenha a sua chave de assinatura

Uma chave de assinatura é criada automaticamente quando você se cadastra, mas o valor dela nunca é mostrado, então você precisa criar uma que possa copiar. No console, abra Webhooks e clique em Gerar (ou em Trocar, se o cartão disser que já há uma chave configurada). A chave nova aparece uma única vez: copie-a e guarde-a no seu servidor como SPICY_WEBHOOK_SECRET. Se você a perder, troque-a de novo.

A troca não tem período de transição: a chave antiga para de assinar na hora, e até as entregas que ainda estão sendo repetidas passam a ser assinadas com a nova. Atualize todos os receptores em funcionamento logo depois da troca. Os detalhes estão na página Webhooks.

Rode um receptor num endereço HTTPS público

A SpicyAPI só consegue chamar um endereço acessível pela internet que comece com https://. http:// simples, localhost e endereços de rede privada são recusados. Durante o desenvolvimento, exponha o seu servidor local por um serviço de túnel que dê a ele um endereço HTTPS público, ou use polling até fazer o deploy. As regras completas para o endereço de callback estão na página Webhooks.

Envie callBackUrl com as suas tarefas

Acrescente callBackUrl: "https://your-app.example.com/hooks/spicy" à requisição que você passa para createTask. Veja Receber resultados por webhook.

Um receptor mínimo

Este receptor usa só o módulo http que já vem com o Node.js, então não há nada mais para instalar. Salve-o como receiver.mjs e inicie com node receiver.mjs.

receiver.mjs
import { createServer } from "node:http";
import { SpicyWebhookVerificationError, verifyWebhook } from "@spicyapi/sdk/webhooks";

const secret = process.env.SPICY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set SPICY_WEBHOOK_SECRET first");

// IDs de entrega que você já processou. Em produção, use uma tabela no banco de dados,
// para que a lista sobreviva a um reinício.
const handled = new Set();

const server = createServer(async (request, response) => {
  if (request.method !== "POST" || request.url !== "/hooks/spicy") {
    response.writeHead(404).end();
    return;
  }
  if (request.headers["x-webhook-payload-version"] !== "2") {
    response.writeHead(400).end();
    return;
  }

  // 1. Leia o corpo como bytes brutos. Não o interprete antes de verificar.
  const chunks = [];
  for await (const chunk of request) chunks.push(chunk);
  const rawBody = Buffer.concat(chunks);

  // 2. Confira a assinatura e o horário.
  let event;
  try {
    event = verifyWebhook({
      rawBody,
      secret,
      timestamp: String(request.headers["x-webhook-timestamp"] ?? ""),
      signature: String(request.headers["x-webhook-signature"] ?? ""),
      payloadVersion: 2,
    });
  } catch (error) {
    const reason = error instanceof SpicyWebhookVerificationError ? error.reason : String(error);
    console.warn("Rejected a webhook:", reason);
    response.writeHead(401).end();
    return;
  }

  // 3. Responda rápido. A SpicyAPI espera no máximo 15 segundos e depois tenta de novo.
  response.writeHead(204).end();

  // 4. A mesma entrega pode chegar mais de uma vez. Processe cada uma só uma vez.
  if (!event.deliveryId || handled.has(event.deliveryId)) return;
  handled.add(event.deliveryId);

  if (!("data" in event.payload)) return; // corpos da versão 2 trazem a tarefa em data
  const task = event.payload.data; // os mesmos campos que getTask() retorna
  console.log("Task", task.taskId, "is", task.state);
  if (task.state === "succeeded") {
    console.log("Result:", task.output?.assets?.[0]?.url);
  }
});

server.listen(8080, () => console.log("Listening on http://localhost:8080/hooks/spicy"));

O que importa nele:

  • Bytes brutos. A assinatura cobre os bytes exatos que a SpicyAPI enviou. Se um framework interpretar o JSON primeiro e você o transformar de volta em texto, os bytes mudam e toda verificação falha. No Express, use express.raw({ type: "application/json" }) nesta rota.
  • Responda rápido. Qualquer resposta 2xx significa "recebido". Sem resposta em 15 segundos, ou com qualquer outro status, a SpicyAPI tenta de novo várias vezes ao longo das horas seguintes. Faça o trabalho demorado depois de responder.
  • Duplicatas acontecem. Novas tentativas e instabilidades de rede podem entregar a mesma mensagem duas vezes. O event.deliveryId continua o mesmo entre as tentativas, então guarde os IDs que você já processou.
  • O sucesso está no state. Uma entrega de uma tarefa que falhou continua sendo uma entrega válida. Leia task.state, nunca o code do envelope.
  • Em produção, salve primeiro o evento verificado no seu banco de dados, responda 2xx só depois que o salvamento der certo e ignore IDs de tarefa que a sua aplicação não criou. O guia Webhooks tem uma versão apoiada em banco de dados.

Teste o seu receptor antes de entrar no ar

computeWebhookSignature gera uma assinatura de verdade, então você pode mandar para si mesmo uma entrega de teste assinada. Com o receiver.mjs rodando num terminal e o mesmo SPICY_WEBHOOK_SECRET definido em outro, rode:

send-test.mjs
import { computeWebhookSignature } from "@spicyapi/sdk";

const secret = process.env.SPICY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set SPICY_WEBHOOK_SECRET first");

const body = JSON.stringify({
  code: 200,
  msg: "success",
  request_id: "whd_local_test_1",
  data: {
    taskId: "job_local_test",
    state: "succeeded",
    cost: "0.01",
    settled: true,
    output: { assets: [{ url: "https://example.com/result.png" }] },
  },
});
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = computeWebhookSignature("job_local_test", timestamp, body, secret);

const response = await fetch("http://localhost:8080/hooks/spicy", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Webhook-Timestamp": timestamp,
    "X-Webhook-Signature": signature,
    "X-Webhook-Payload-Version": "2",
  },
  body,
});
console.log("Your receiver answered", response.status); // o esperado é 204

Rode duas vezes: o receptor mostra a tarefa uma única vez, porque a segunda entrega tem o mesmo request_id. Mude um caractere de body depois de assinar e o receptor responde 401.

verifyWebhook(options)

verifyWebhook(options: VerifyWebhookOptions): VerifiedWebhook

Propriedade

Tipo

Retorna payload (o corpo interpretado; na versão 2 a tarefa fica em payload.data), payloadVersion, taskId, deliveryId (o request_id da versão 2, idêntico entre as tentativas) e timestamp. A função é síncrona: não precisa de await.

Quando a verificação falha, ela lança SpicyWebhookVerificationError, cujo reason é um destes: invalid_signature, stale_timestamp, invalid_timestamp, invalid_json, invalid_payload, body_too_large ou invalid_secret. Responda 401 e não aja sobre a mensagem.

computeWebhookSignature(taskId, timestamp, rawBody, secret) retorna a assinatura em Base64 de um corpo. Você só precisa dela para testes como o de cima. O algoritmo de assinatura completo, para outras linguagens, está em Webhooks.

Dica para TypeScript: payload é tipado como qualquer um dos dois formatos. Restrinja o tipo com "data" in event.payload, como o receptor faz, ou passe o tipo esperado: verifyWebhook<components["schemas"]["TaskRecordEnvelope"]>(…) com import type { components } from "@spicyapi/sdk".

Como tratar erros

As coisas dão errado de dois jeitos diferentes, e cada um pede um tratamento diferente:

  1. Uma chamada falha. O SDK lança um erro: a sua chave estava errada, o seu saldo era baixo demais, a rede caiu. Capture com try / catch.
  2. Uma tarefa falha. A chamada funcionou, mas a geração não. Nada é lançado: a tarefa retornada tem state: "failed" e um errorCode. Tarefas que falham ou expiram normalmente liberam toda a reserva. Algumas recusas de conteúdo são cobradas pelo orçamento aceito, somente quando a página do modelo informa explicitamente essa exceção. Confira settled e cost para saber o valor final.

As classes de erro

ClasseQuando apareceCampos úteis
SpicyApiErrorA SpicyAPI respondeu com um erro, como uma chave recusada ou saldo baixocode (o código de negócio sobre o qual agir, como 40201), status (status HTTP), message, requestId, retryAfterSeconds, rateLimitLimit, rateLimitRemaining
SpicyTransportErrorNão chegou nenhuma resposta aproveitável: falha de rede, problema de DNS ou uma resposta maior que 4 MiBmessage, cause (o erro de origem)
SpicyTimeoutErrorUma única requisição demorou mais que timeoutMs (ou uploadTimeoutMs). É um tipo de SpicyTransportErrortimeoutMs
SpicyWaitTimeoutErrorrun ou waitForTask ficou sem tempo. A tarefa continua rodandotaskId, timeoutMs, lastTask
SpicyUploadErrorO armazenamento recusou um arquivo enviado, ou ele era grande demaisstatus
SpicyWebhookVerificationErrorUm webhook não passou na verificaçãoreason
TypeErrorA própria chamada era inválida — nenhuma chave definida, uma opção fora do intervalo, um ID vazio. Nada foi enviadomessage

Decida o que fazer pelo code, nunca pela message: as mensagens são para pessoas, a redação delas pode mudar e elas seguem a configuração de idioma de erros da sua conta. Informe o requestId sempre que falar com o suporte; com ele, encontramos a requisição exata.

handle-errors.mjs
import { randomUUID } from "node:crypto";
import {
  SpicyApiError,
  SpicyClient,
  SpicyTransportError,
  SpicyWaitTimeoutError,
} from "@spicyapi/sdk";

const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // um ID real vindo de listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

const idempotencyKey = randomUUID(); // guarde: você precisa dela para tentar de novo

try {
  const task = await client.run(
    { model: MODEL_ID, input: { prompt: "A paper lantern on a rainy street" } },
    { idempotencyKey, onAccepted: ({ taskId }) => console.log("Task ID:", taskId) },
  );
  if (task.state === "succeeded") console.log("Done:", task.output?.assets?.[0]?.url);
  else console.log(`Finished as ${task.state} (${task.errorCode}). Cost: ${task.cost}; settled: ${task.settled}.`);
} catch (error) {
  if (error instanceof SpicyWaitTimeoutError) {
    // A sua espera acabou, a tarefa não. Consulte-a mais tarde com o mesmo ID.
    console.log(`Still running: ${error.taskId}. Call waitForTask() again later.`);
  } else if (error instanceof SpicyApiError) {
    console.log(`SpicyAPI said no: ${error.code} (HTTP ${error.status}) ${error.message}`);
    console.log(`Quote this if you contact support: ${error.requestId}`);
    if (error.code === 40201) console.log("Your balance is too low. Top up in the console.");
    if (error.code === 40202) console.log("A spend cap was reached. Raise it on the API keys page.");
    if (error.code === 429) console.log(`Too many requests. Wait ${error.retryAfterSeconds ?? 1} s.`);
  } else if (error instanceof SpicyTransportError) {
    // Falha de rede ou tempo esgotado localmente: a tarefa pode ou não existir.
    // Rode a mesma requisição de novo com a MESMA idempotencyKey para descobrir com segurança.
    console.log(`Network problem (${error.message}). Retry with key ${idempotencyKey}.`);
  } else {
    throw error; // um TypeError aqui é um erro na própria chamada, como uma chave faltando
  }
}

Códigos de erro comuns

error.codeSignificadoVale tentar de novo?
400Requisição inválida, normalmente um campo de entrada que o modelo não aceitaNão. Corrija a requisição
401Chave faltando, malformada, revogada ou expiradaNão. Confira a chave
40201Saldo baixo demais para a reservaNão. Adicione saldo antes
40202Um limite de gasto foi atingidoNão. Aumente o limite ou espere ele recomeçar no dia ou no mês seguinte
40301Esta chave não pode usar este modeloNão. Altere a lista de modelos da chave
404Não existe esse modelo ou essa tarefa para esta chaveNão
409Conflito com o estado atual, como uma chave de idempotência reaproveitada para outro trabalhoDepende; veja Erros
40901A cotação expirou ou o preço mudouPeça uma nova cotação e confirme
429Requisições demaisSim, depois de retryAfterSeconds. O SDK já repete as leituras por você
500Algo quebrou do lado da SpicyAPISim, com uma pausa
50301O modelo está indisponível agoraSim, mais tarde — ou escolha outro modelo

Quando uma tarefa falha, task.errorCode é um destes: invalid_request, unsupported_combination, content_rejected, rate_limited, upstream_unavailable, generation_failed, timeout, invalid_asset ou upstream_failed. retryTask envia de novo a entrada original sem mudanças, então escolha o próximo passo pelo código:

task.errorCodeO que fazer
rate_limited, upstream_unavailable, timeout, upstream_failedEspere e use retryTask uma vez. Se o timeout se repetir, tente uma duração menor ou uma resolução mais baixa com createTask
generation_failedMude o prompt ou a imagem e envie uma tarefa nova com createTask; a mesma entrada costuma falhar do mesmo jeito
invalid_assetEnvie o arquivo de novo e crie uma tarefa nova com createTask usando a nova uri; uma nova tentativa reaproveitaria o arquivo antigo
invalid_request, unsupported_combinationNão repita sem mudanças. Altere as configurações e depois use createTask
content_rejectedNão repita sem mudanças. O modelo escolhido recusou a requisição segundo a política dele; mude o prompt ou escolha outro modelo

A tabela completa está em Erros.

Guarde os seus resultados

  • Uma url de resultado funciona por cerca de 20 minutos. Chame getTask de novo para receber uma nova.
  • Os arquivos gerados são apagados depois de no máximo cerca de 14 dias, ou antes, se a sua conta ou o retentionSeconds assim definirem. Os arquivos de entrada enviados duram um dia, e os prompts guardados, 30 dias.
  • Baixe para o seu próprio armazenamento o que você quer guardar assim que a tarefa der certo. Não salve a url temporária em si no seu banco de dados. Detalhes: Retenção e destruição.

Receitas comuns

As receitas usam MODEL_ID_FROM_CATALOG como marcador. Antes de rodar uma delas, troque-o pelo valor model de um item adequado do catálogo atual (listModels() ou GET /api/v1/models) em que enabled e available sejam ambos true. Cada receita para com uma mensagem clara se você esquecer.

De imagem para vídeo: envie primeiro uma imagem local

image-to-video.mjs
import { randomUUID } from "node:crypto";
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();

// Troque por um ID de modelo image-to-video do catálogo atual (GET /api/v1/models):
// await client.listModels({ modality: "video", task: "image-to-video" })
const MODEL_ID = "MODEL_ID_FROM_CATALOG";
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

// 1. Envie a imagem local. Grátis. O endereço spicy:// funciona por um dia.
const file = await client.uploadFile("./photo.jpg");
console.log("Uploaded:", file.uri);

// 2. Mostre o schema do modelo: qual campo recebe a imagem e quais campos são obrigatórios.
const { inputSchema } = await client.getModel(MODEL_ID);
console.log(JSON.stringify(inputSchema, null, 2));

// 3. Crie o vídeo e espere por ele. Vídeo costuma levar vários minutos.
const task = await client.run(
  {
    model: MODEL_ID,
    input: {
      prompt: "Slow push-in, light rain, neon reflections on wet stone",
      image_url: file.uri, // use o nome de campo que o schema acima mostra
    },
  },
  {
    idempotencyKey: randomUUID(),
    timeoutMs: 30 * 60_000,
    onAccepted: ({ taskId }) => console.log("Task ID (save it):", taskId),
  },
);

console.log(task.state, task.output?.assets?.[0]?.url ?? task.errorCode);

O schema mostrado é quem manda nos nomes de campo e nos campos obrigatórios; um modelo de vídeo pode exigir também uma duração ou uma resolução, e o preço muitas vezes depende delas. Se a sua imagem já está online num endereço HTTPS público, pule o upload e coloque esse endereço diretamente no campo de imagem. Rode antes quoteTask com a mesma requisição se quiser ver o preço.

Rodar um lote com concorrência limitada

Iniciar centenas de tarefas de uma vez pode esbarrar nos seus limites de gasto e no seu limite de requisições. Este script roda uma lista de prompts com no máximo duas tarefas em andamento ao mesmo tempo.

batch.mjs
import { SpicyApiError, SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();

const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // um ID real vindo de listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

// Mude o nome do lote sempre que mudar os prompts.
const BATCH_NAME = "lanterns-2026-09-17";
const prompts = [
  "A paper lantern on a rainy street",
  "A paper lantern floating on a lake",
  "A paper lantern in a snowy forest",
  "A paper lantern above a night market",
];
const CONCURRENCY = 2; // quantas tarefas podem rodar ao mesmo tempo

const results = prompts.map((prompt) => ({ prompt, outcome: "not started", detail: "" }));
let nextIndex = 0;

async function worker() {
  while (nextIndex < prompts.length) {
    const index = nextIndex++;
    try {
      const task = await client.run(
        { model: MODEL_ID, input: { prompt: prompts[index] } },
        {
          // Mesmo nome de lote + mesma posição = mesma chave. Se o script travar e você
          // rodá-lo de novo dentro de 24 horas, recebe de volta as tarefas que já existem
          // em vez de pagar por elas duas vezes.
          idempotencyKey: `${BATCH_NAME}-${index}`,
          timeoutMs: 20 * 60_000,
        },
      );
      results[index].outcome = task.state;
      results[index].detail = task.output?.assets?.[0]?.url ?? task.errorCode ?? "";
    } catch (error) {
      results[index].outcome = "error";
      results[index].detail = error instanceof Error ? error.message : String(error);
      // Sem saldo (40201) ou acima de um limite de gasto (40202): pare de iniciar tarefas novas.
      if (error instanceof SpicyApiError && (error.code === 40201 || error.code === 40202)) {
        nextIndex = prompts.length;
      }
    }
  }
}

await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
console.table(results);
  • Por que as chaves são montadas a partir do nome do lote. Uma chave de idempotência amarrada a "este lote, item 3" faz com que rodar de novo depois de uma falha reaproveite as tarefas já criadas. Uma chave aleatória a cada execução pagaria pela mesma imagem outra vez.
  • Por que o nome do lote precisa mudar junto com os prompts. Reaproveitar uma chave para uma requisição diferente é recusado com 409.
  • Como escolher CONCURRENCY. Cada tarefa em andamento mantém parte do seu saldo reservada, e cada consulta de estado conta para o limite de requisições da sua conta. Comece com pouco e aumente depois que o lote rodar sem problemas.

Receber resultados por webhook em vez de polling

Num aplicativo web ou num processo em segundo plano, ficar esperando dentro de uma requisição é desperdício. Crie a tarefa com um callBackUrl, guarde o ID da tarefa e deixe o receptor cuidar do resultado quando ele chegar.

webhook-create.mjs
import { randomUUID } from "node:crypto";
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // um ID real vindo de listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");

// Num aplicativo de verdade, guarde esta chave junto com o pedido do cliente antes de chamar.
const idempotencyKey = randomUUID();

const accepted = await client.createTask(
  {
    model: MODEL_ID,
    input: { prompt: "A paper lantern on a rainy street" },
    callBackUrl: "https://your-app.example.com/hooks/spicy",
  },
  { idempotencyKey },
);

console.log(`Accepted ${accepted.taskId}; $${accepted.estimatedCost} is on hold.`);
// Salve accepted.taskId agora. Quando a tarefa terminar, a SpicyAPI chama o seu receptor.

Tenha uma rede de segurança para entregas que nunca chegam, por exemplo porque o seu servidor ficou fora do ar durante todas as tentativas: de vez em quando, chame getTask para as tarefas salvas que já passaram bastante do deadlineAt sem webhook, ou percorra as páginas de listTasks. O histórico de entregas de webhooks, com um botão para reenviar manualmente, fica na página Webhooks do console.

Repetir uma tarefa que falhou

retry.mjs
import { SpicyClient } from "@spicyapi/sdk";

const client = new SpicyClient();

const taskId = process.argv[2]; // rode assim: node retry.mjs job_...
if (!taskId) throw new Error("Usage: node retry.mjs <taskId>");

// Uma nova tentativa reenvia a entrada original sem mudanças, então só vale para falhas passageiras.
// Para qualquer outro código, altere a requisição e use createTask.
const WORTH_ONE_RETRY = new Set(["rate_limited", "upstream_unavailable", "timeout", "upstream_failed"]);

const source = await client.getTask(taskId);

if (
  (source.state !== "failed" && source.state !== "expired") ||
  !WORTH_ONE_RETRY.has(source.errorCode ?? "")
) {
  console.log(`Not retrying: the task is ${source.state} (${source.errorCode ?? "no error code"}).`);
} else {
  // Uma chave fixa por tentativa: rodar este script duas vezes cria uma nova tentativa, não duas.
  const retry = await client.retryTask(source.taskId, {
    idempotencyKey: `retry-${source.taskId}-attempt-1`,
  });
  console.log(`New task ${retry.taskId}; $${retry.estimatedCost} is on hold.`);

  const finished = await client.waitForTask(retry.taskId, { timeoutMs: 20 * 60_000 });
  console.log("The retry finished as", finished.state);
}

Ele só repete falhas que podem ter outro desfecho na próxima vez — um serviço ocupado ou temporariamente indisponível, um tempo esgotado ou um erro desconhecido — e só uma vez, com uma chave de idempotência fixa. Para invalid_asset, envie o arquivo de novo e crie uma tarefa nova, porque a nova tentativa reaproveitaria o upload expirado. Para generation_failed, mude o prompt ou a imagem e crie uma tarefa nova. Para content_rejected, invalid_request e unsupported_combination, altere a requisição em vez de repeti-la. Se essa única nova tentativa também falhar, pare e examine o erro; para um timeout repetido, tente uma duração menor ou uma resolução mais baixa com createTask. Veja Erros.

Solução de problemas e perguntas frequentes

Código-fonte

O pacote tem licença MIT e é desenvolvido abertamente em github.com/spicyapi-ai/spicy-sdk. É lá que você lê o código por trás de qualquer método desta página, relata um comportamento diferente do descrito aqui ou propõe uma mudança.

Próximos passos

Nesta página

O que é o SDKEsta é a ferramenta certa?O que ele não fazTermos usados nesta páginaO que você pode fazerAntes de começarInstale o Node.js 22.13 ou mais recenteCrie uma pasta de projeto e instale o SDKCrie uma chave de APIColoque a chave numa variável de ambienteRode o seu primeiro arquivoSeu primeiro resultadoO que cada passo fazReferência da APICriar um clienteNovas tentativas automáticasInterromper uma chamada com signalgetStatus()listModels(options?)getModel(modelId)quoteTask(request)createTask(request, options?)run(request, options?)getTask(taskId)waitForTask(taskId, options?)retryTask(taskId, options?)listTasks(options?)getBalance()getUsage(options?)Enviar arquivosuploadFile(path, options?)uploadBytes(data, options)uploadBase64(value, options?)createUploadUrl e commitUploadedFilecreateDownloadUrl(taskId, key?)purgeTask(taskId)Funções de webhookUtilitários offline e tiposWebhooksPor que você precisa verificar todo webhookConfigureObtenha a sua chave de assinaturaRode um receptor num endereço HTTPS públicoEnvie callBackUrl com as suas tarefasUm receptor mínimoTeste o seu receptor antes de entrar no arverifyWebhook(options)Como tratar errosAs classes de erroCódigos de erro comunsGuarde os seus resultadosReceitas comunsDe imagem para vídeo: envie primeiro uma imagem localRodar um lote com concorrência limitadaReceber resultados por webhook em vez de pollingRepetir uma tarefa que falhouSolução de problemas e perguntas frequentesCódigo-fontePróximos passos