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 TypeScript | Este SDK |
| Gerar conteúdo pelo terminal, sem escrever código | A 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 texto | O cliente oficial da OpenAI apontado para a SpicyAPI — veja Texto e streaming |
| Usar Python, Go, PHP ou Java | O SDK para Python, o SDK para Go, o SDK para PHP ou o SDK para Java oficiais |
| Usar qualquer outra linguagem | Chamadas 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
openaijá 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
| Termo | O que significa aqui |
|---|---|
| ID do modelo | O 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 |
| Tarefa | Um 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_ |
| Reserva | Quando 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ência | Um 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 |
| Webhook | Um 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 fazer | Método | Custa dinheiro? |
|---|---|---|
| Conferir se a SpicyAPI está acessível (sem chave) | getStatus | Não |
| Ver a lista atual de modelos, os seus preços e os campos de cada modelo | listModels, getModel | Não |
| Saber o preço exato de uma requisição antes de gastar qualquer coisa | quoteTask | Não |
| Iniciar uma geração | createTask | Sim |
| Iniciar uma geração e esperar o resultado numa única chamada | run | Sim |
| Consultar uma tarefa uma vez | getTask | Não |
| Esperar até uma tarefa terminar | waitForTask | Não |
| Tentar de novo uma tarefa que falhou | retryTask | Sim (é uma tarefa nova) |
| Listar as tarefas criadas com a sua chave | listTasks | Não |
| Consultar o seu saldo | getBalance | Não |
| Ver quanto você gastou, por dia e por modelo | getUsage | Não |
| Enviar um arquivo local de imagem, vídeo ou áudio | uploadFile, uploadBytes, uploadBase64 | Não |
| Obter um link de download novo para um resultado | createDownloadUrl | Não |
| Apagar os arquivos e o prompt guardados de uma tarefa concluída | purgeTask | Não (e não há reembolso) |
| Conferir se um webhook veio mesmo da SpicyAPI | verifyWebhook (uma função avulsa) | Não |
| Pesquisar a documentação ou ler o contrato da API sem internet | searchDocumentation, 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 -vO 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| Comando | O que faz |
|---|---|
mkdir spicy-demo | Cria uma pasta vazia para o seu projeto |
cd spicy-demo | Leva o terminal para dentro dessa pasta; os próximos comandos agem nela |
npm init -y | Cria o package.json, o arquivo que lista as configurações e as bibliotecas do seu projeto |
npm pkg set type=module | Avisa o Node.js de que os seus arquivos usam a sintaxe moderna import, exigida por este SDK |
npm install @spicyapi/sdk | Baixa 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
- 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.
- Abra a página Chaves de API do console e clique em Criar chave. Só o nome é obrigatório.
- 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:
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.mjsSpicyAPI is up
Checked at 2026-09-17T08:00:00.000ZEssa 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.
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 --yesModel: <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
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, informaoperational: false.listModels()retorna o catálogo atual de modelos (live catalog) com os preços da sua conta. Use apenas itens em queenabledeavailablesejam ambostrue.includeExamples: trueacrescenta entradas de exemplo que o servidor já validou, que são o ponto de partida mais seguro. O valor que você envia depois é o campomodeldo item: o ID do modelo.- 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, chamegetModel()e leia oinputSchemadele. 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.run()cria a tarefa e depois a consulta até ela terminar. PassarquoteIdeexpectedCostsignifica "só aceite pelo preço que me foi mostrado": se o preço mudou, a chamada falha com40901em vez de cobrar outro valor. OidempotencyKeytorna 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.onAcceptedentrega 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.- 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. Confirasettledecostpara saber o valor final. - O download. Cada resultado vem com uma
urltemporária. Baixe-a sem a sua chave de API e guarde a sua própria cópia: o link dura cerca de 20 minutos (chamegetTask()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:
| Chamada | Repetida automaticamente? |
|---|---|
Leituras: getStatus, listModels, getModel, getTask, listTasks, getBalance, getUsage; e também quoteTask, purgeTask, commitUploadedFile | Sim |
createTask, run (a etapa de criação), retryTask | Só 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, createDownloadUrl | Nã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 retornado | Significado |
|---|---|
operational | true quando o serviço está respondendo. É este o campo a usar |
health | O resultado bruto da verificação de saúde: path, ok, status HTTP, os primeiros 512 caracteres de body e error quando nenhuma resposta chegou |
readiness | O 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 |
checkedAt | Quando 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:
| Campo | Significado |
|---|---|
model | O ID do modelo. Envie exatamente este valor ao criar uma tarefa |
displayName | Um nome legível |
modality, tasks | O que ele produz e que tipo de trabalho faz |
provider, family | Quem fez o modelo e a que linha de produtos ele pertence |
enabled, available | Os dois precisam ser true antes de você usar o modelo |
pricing | Uma 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 |
startingPrice | A faixa mais barata. Para um valor exato, use quoteTask |
quantityField | O campo de entrada que define a quantidade cobrada, como uma duração. Vazio significa uma unidade por chamada |
taskTimeoutSeconds | Quanto tempo uma tarefa pode rodar antes de terminar como expired |
maxOutputDurationSeconds | A saída mais longa que o modelo permite, quando ela tem duração |
inputSchema, examples | Só aparecem quando você os pediu |
updatedAt, version | Mudam 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 retornado | Significado |
|---|---|
quoteId | Passe-o para createTask ou run para travar este preço |
estimatedCost | O valor que será reservado se você criar a tarefa agora |
maxCharge | O máximo que esta requisição pode custar |
currency | Sempre USD |
quantity, unit | A quantidade cobrada, como "5" e per_second |
expiresAt | A 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 retornado | Significado |
|---|---|
taskId | O ID da nova tarefa. Salve-o na hora |
state | queued para uma tarefa nova. Se você repetiu uma chave de idempotência, o estado atual da tarefa original |
estimatedCost | O valor agora reservado, que é também o máximo que esta tarefa pode cobrar |
deadlineAt | Quando 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.code | O que significa | O que fazer |
|---|---|---|
400 | A entrada não bate com o schema do modelo, ou o callBackUrl não é permitido | Compare com getModel().inputSchema e corrija a requisição |
401 | A chave está faltando, malformada ou revogada | Confira SPICY_API_KEY |
40201 | O seu saldo não cobre a reserva | Adicione saldo em Faturamento, no console |
40202 | Um limite de gasto diário, mensal ou acumulado foi atingido | Aumente o limite na página Chaves de API ou espere ele recomeçar |
40301 | Esta chave não tem permissão para usar este modelo | Altere a lista de modelos da chave no console |
409 | Esta chave de idempotência já foi usada para outra requisição | Use uma chave nova para um trabalho novo |
40901 | A cotação expirou, ou o preço ou a requisição mudaram | Peça uma nova cotação e confirme o novo preço |
413 | O corpo da requisição é grande demais | Envie os arquivos de mídia por upload em vez de embuti-los |
429 | Requisições demais | Espere error.retryAfterSeconds e tente de novo |
50301 | O modelo não está aceitando tarefas agora | Tente 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:
SpicyWaitTimeoutErrorquando otimeoutMsse esgota depois que a tarefa foi aceita. A tarefa continua rodando na SpicyAPI. ChamewaitForTask(error.taskId)mais tarde para buscá-la.SpicyTimeoutErrorquando 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
onAcceptedouonUpdateinterrompe a espera; a tarefa em si continua rodando. TypeErrorpara umtimeoutMsouintervalMsinvá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>| Campo | Significado |
|---|---|
taskId, model | Qual tarefa, feita com qual modelo |
state | queued ou running enquanto ela trabalha; succeeded, failed ou expired quando terminou (canceled só aparece em registros antigos) |
output.assets | Os arquivos de resultado, descritos na próxima tabela. Só em succeeded |
output.text | Saída em texto, para modelos que retornam texto |
errorCode, errorMessage | Por que uma tarefa falhou. Tome decisões pelo errorCode; a redação da mensagem pode mudar |
cost | Enquanto settled for false, o valor reservado; quando for true, a cobrança final |
settled | Se o cost já é definitivo |
createdAt, completedAt, deadlineAt | Quando foi criada, quando terminou e quando expiraria |
input | A entrada como foi armazenada. É removida quando termina o prazo de retenção do prompt |
sourceTaskId | Presente numa tarefa criada por retryTask, apontando para a original |
contentState | present; expired (removido pelas regras de retenção); ou purged (você o apagou com purgeTask) |
retention | Quando os arquivos (outputsExpireAt) e o prompt (promptsExpireAt) serão removidos |
Cada item de output.assets:
| Campo | Significado |
|---|---|
url | Um link de download temporário. Baixe-o sem a sua chave de API |
expiresAt | Quando esse link para de funcionar, normalmente em cerca de 20 minutos. Chame getTask de novo para receber um link novo |
mime | O tipo do arquivo, como image/png ou video/mp4 |
width, height, durationSeconds, bytes | Detalhes de tamanho, quando se aplicam |
pending | true enquanto o arquivo ainda está sendo copiado para o armazenamento. Consulte de novo em alguns segundos |
unavailable | true quando o arquivo não existe mais. Esperar não adianta; gere de novo |
key | Um 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>| Campo | Significado |
|---|---|
available | Dinheiro que pode pagar novas tarefas. Pode ficar negativo quando a sua conta usa um limite de crédito aprovado |
held | Dinheiro reservado para tarefas que ainda estão rodando |
total | available mais held |
funding | Detalhamento 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.
| Campo | Significado |
|---|---|
totalCalls | Número de tarefas no intervalo |
totalSpend | Só cobranças finais; dinheiro ainda reservado não entra na conta |
days | Uma entrada por dia com chamadas: day, calls, succeeded, failed, spend |
models | Uma 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 retornado | Significado |
|---|---|
uri | O endereço spicy:// a colocar na entrada de uma tarefa |
expiresAt | Quando o endereço para de funcionar (em um dia) |
fileId, status, bytes, contentType, sha256 | Detalhes do que foi armazenado; status é ready |
width, height, durationSeconds | Dimensõ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>createUploadUrlretornafileId,uploadUrl,method(PUT),headers,maxByteseexpiresAt.- Envie o arquivo com um
PUTparauploadUrl, exatamente com osheaderslistados e sem cabeçalhoAuthorization. O tamanho declarado precisa bater com o do arquivo. commitUploadedFile(fileId)confere o arquivo armazenado e retorna aurispicy://. 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 retornado | Significado |
|---|---|
contentState | purged quando o conteúdo foi apagado. present significa que nada foi removido desta vez; tente de novo mais tarde |
purgedAt | Quando o conteúdo foi apagado pela primeira vez |
contentRemovedBy | user quando foi você quem apagou |
billingRetained | Sempre true: o registro de quanto a tarefa custou é mantido |
mediaDeletionPending | true 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.
| Export | O 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 |
DOCUMENTATION | Esse í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_BYTES | Os 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ção | Contém |
|---|---|
@spicyapi/sdk | Tudo o que está nesta página |
@spicyapi/sdk/webhooks | Só verifyWebhook, computeWebhookSignature e SpicyWebhookVerificationError — uma importação pequena para um receptor de webhooks |
@spicyapi/sdk/openapi | Só 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.
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
2xxsignifica "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.deliveryIdcontinua 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. Leiatask.state, nunca ocodedo envelope. - Em produção, salve primeiro o evento verificado no seu banco de dados, responda
2xxsó 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:
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 é 204Rode 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): VerifiedWebhookPropriedade
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:
- 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. - Uma tarefa falha. A chamada funcionou, mas a geração não. Nada é lançado: a tarefa retornada tem
state: "failed"e umerrorCode. 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. Confirasettledecostpara saber o valor final.
As classes de erro
| Classe | Quando aparece | Campos úteis |
|---|---|---|
SpicyApiError | A SpicyAPI respondeu com um erro, como uma chave recusada ou saldo baixo | code (o código de negócio sobre o qual agir, como 40201), status (status HTTP), message, requestId, retryAfterSeconds, rateLimitLimit, rateLimitRemaining |
SpicyTransportError | Não chegou nenhuma resposta aproveitável: falha de rede, problema de DNS ou uma resposta maior que 4 MiB | message, cause (o erro de origem) |
SpicyTimeoutError | Uma única requisição demorou mais que timeoutMs (ou uploadTimeoutMs). É um tipo de SpicyTransportError | timeoutMs |
SpicyWaitTimeoutError | run ou waitForTask ficou sem tempo. A tarefa continua rodando | taskId, timeoutMs, lastTask |
SpicyUploadError | O armazenamento recusou um arquivo enviado, ou ele era grande demais | status |
SpicyWebhookVerificationError | Um webhook não passou na verificação | reason |
TypeError | A própria chamada era inválida — nenhuma chave definida, uma opção fora do intervalo, um ID vazio. Nada foi enviado | message |
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.
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.code | Significado | Vale tentar de novo? |
|---|---|---|
400 | Requisição inválida, normalmente um campo de entrada que o modelo não aceita | Não. Corrija a requisição |
401 | Chave faltando, malformada, revogada ou expirada | Não. Confira a chave |
40201 | Saldo baixo demais para a reserva | Não. Adicione saldo antes |
40202 | Um limite de gasto foi atingido | Não. Aumente o limite ou espere ele recomeçar no dia ou no mês seguinte |
40301 | Esta chave não pode usar este modelo | Não. Altere a lista de modelos da chave |
404 | Não existe esse modelo ou essa tarefa para esta chave | Não |
409 | Conflito com o estado atual, como uma chave de idempotência reaproveitada para outro trabalho | Depende; veja Erros |
40901 | A cotação expirou ou o preço mudou | Peça uma nova cotação e confirme |
429 | Requisições demais | Sim, depois de retryAfterSeconds. O SDK já repete as leituras por você |
500 | Algo quebrou do lado da SpicyAPI | Sim, com uma pausa |
50301 | O modelo está indisponível agora | Sim, 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.errorCode | O que fazer |
|---|---|
rate_limited, upstream_unavailable, timeout, upstream_failed | Espere e use retryTask uma vez. Se o timeout se repetir, tente uma duração menor ou uma resolução mais baixa com createTask |
generation_failed | Mude o prompt ou a imagem e envie uma tarefa nova com createTask; a mesma entrada costuma falhar do mesmo jeito |
invalid_asset | Envie 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_combination | Não repita sem mudanças. Altere as configurações e depois use createTask |
content_rejected | Nã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
urlde resultado funciona por cerca de 20 minutos. ChamegetTaskde 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
retentionSecondsassim 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
urltemporá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
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.
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.
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
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
Catálogo de modelos e schemas
Como ler o inputSchema de um modelo e escolher entradas válidas
Tarefas assíncronas
Estados da tarefa, polling e o que significa cada campo
Webhooks
Formato das entregas, calendário de novas tentativas e verificação em outras linguagens
Mídia
Uploads, limites de Base64 e download de resultados
Erros
Todos os códigos de erro e quais valem uma nova tentativa
Cobrança
Reservas, liquidação, limites de gasto e o seu saldo
Idempotência
Tente de novo com segurança, sem pagar duas vezes
Ferramenta de linha de comando
Os mesmos recursos pelo terminal, sem escrever código
Servidor MCP
Deixe um assistente de programação com IA usar a SpicyAPI por você
SDK para Python
O mesmo fluxo em Python, sem nenhuma dependência de execução
SDK para Go
O mesmo fluxo em Go, só com a biblioteca padrão
SDK para PHP
O mesmo fluxo em PHP, sem nenhuma dependência de biblioteca
SDK para Java
O mesmo fluxo em Java, com uma taxonomia de erros selada
Integração em produção
Um checklist de lançamento para uma integração que se recupera de falhas — o modelo certo, chaves e saldo protegidos, custos confirmados, acompanhamento durável das tarefas, callbacks confiáveis e os testes a rodar antes de entrar no ar.
SDK para Python
O SDK oficial da SpicyAPI para Python (spicyapi) — instale, cote e faça a sua primeira geração, envie um arquivo local, verifique webhooks e consulte todos os códigos de erro.

