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

Tarefas assíncronas

Os contratos de createTask e recordInfo, o significado dos seis estados e como escolher entre callbacks e consultas de status.

A geração é assíncrona: o envio e a obtenção do resultado são duas chamadas separadas.

Em termos simples:

  1. Você envia o trabalho e recebe imediatamente um taskId. Nada terminou ainda; o preço do trabalho é separado do seu saldo — uma reserva.
  2. Depois, você volta e pergunta “esse taskId já terminou?” — isso é polling, ou consulta periódica; consultar a cada poucos segundos é suficiente. Outra opção é fornecer um endereço web para receber o resultado quando estiver pronto — isso é um callback, ou webhook.
  3. Quando state é succeeded, o link do arquivo está em data.output.assets[0].url. Abra ou baixe o arquivo em cerca de 20 minutos; se o link expirar, consulte novamente para obter outro. Uma tarefa bem-sucedida nunca é cobrada acima da reserva; tarefas que falham ou expiram normalmente liberam a reserva, com exceções informadas na página do modelo para determinadas recusas de conteúdo cobradas. Confira settled e cost para saber a cobrança final.

Por que não usar uma única chamada? Imagens levam segundos; vídeos costumam levar minutos. Um endpoint síncrono manteria seu processo preso a uma conexão, e qualquer interrupção deixaria você sem resultado e sem uma forma segura de tentar novamente — não seria possível saber se a tentativa anterior já estava em andamento. Separar as chamadas faz do ID da tarefa a referência: reconecte-se e continue consultando o mesmo ID.

1. Crie uma tarefa

POST /api/v1/jobs/createTask

Esta chamada envolve gastos

Uma tarefa aceita reserva imediatamente estimatedCost do seu saldo e não pode ser cancelada. Para consultar o preço sem criar nada, solicite uma estimativa. Os limites de gastos da chave restringem o quanto ela pode gastar; veja Faturamento.

Cabeçalhos

CabeçalhoObrigatórioObservações
AuthorizationSimBearer sk-spicy-…
Content-TypeSimapplication/json
Idempotency-KeyNãoAltamente recomendado: permite repetir com segurança após um erro de rede. Veja Idempotência
X-Spicy-RetentionNãoMantenha as saídas e o prompt desta tarefa por menos tempo, em segundos. Veja Retenção e destruição

Corpo

Propriedade

Tipo

callBackUrl deve ser um endereço HTTPS público que nossos servidores consigam acessar pela internet. URLs http://, porta explícita diferente de 443 ou 80, URLs com usuário ou senha, IPs privados ou de loopback e nomes de host que não resolvem são rejeitados, pois o corpo do callback contém seu prompt e links assinados do resultado. Por isso, http://localhost não funciona durante o desenvolvimento local: consulte o status ou exponha sua máquina por um túnel que forneça um endereço HTTPS público. Valores inválidos retornam HTTP 400 com o código de negócio code: 400, msg: "Invalid callback URL" e um request_id. O erro não repete sua URL, nome de host, endereço IP nem detalhes de DNS. Verificamos o destino novamente ao conectar; veja Webhooks.

Escreva callBackUrl, com B maiúsculo

Apenas input é validado campo a campo. Um campo desconhecido no nível superior, como callback_url ou webhookUrl, é atualmente ignorado: a tarefa é aceita, mas nenhum callback é enviado. Se você espera um callback e ele não chega, verifique primeiro a grafia desse campo.

Resposta

HTTP/1.1 202 Accepted
Location: /api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3
Retry-After: 2

O status de transporte é 202 Accepted. O corpo continua usando o envelope padrão, portanto o code de negócio de sucesso permanece 200. Location informa o endereço a consultar, e Retry-After: 2 sugere uma espera de dois segundos antes da primeira consulta.

{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "state": "queued",
    "estimatedCost": "0.008",
    "deadlineAt": "2026-08-28T09:42:04Z"
  },
  "request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}
CampoObservações
taskIdID da tarefa, com prefixo job_. Todas as operações seguintes dependem dele; salve-o
statequeued para uma nova tarefa; o estado atual da tarefa original em um reenvio idempotente com a mesma chave
estimatedCostValor reservado no aceite, por exemplo "0.008" — dólares americanos como string — e o máximo que esta tarefa pode cobrar. O saldo não utilizado é liberado; nenhuma quantia acima da reserva é cobrada posteriormente
deadlineAtPrazo máximo em que o servidor continua trabalhando nesta tarefa. Se ela não terminar até lá, passa para expired e não há cobrança. Use esse prazo para decidir por quanto tempo esperar

Campos monetários são strings, mas não contêm símbolo de moeda

estimatedCost, cost e os três campos de saldo são strings decimais simples, como "0.008": sem $ nem separadores de milhares. parseFloat / float() funcionam diretamente com esses valores.

São strings, não números JSON, porque na maioria das linguagens um número JSON vira float64, e 0.008 não tem uma representação binária exata. Depois de passar por um float, um preço já não pode ser tratado como o valor exato cobrado — use um tipo decimal, como decimal.Decimal ou BigDecimal, se o valor entrar nos seus registros contábeis.

A moeda é sempre USD. Veja Faturamento.

Quando a criação falha

Estes são os erros mais comuns nas primeiras integrações. Nenhum valor é reservado ou cobrado quando createTask retorna um erro.

HTTPcodeSignificado mais comumO que fazer
400400Um campo de input não existe no schema do modelo, um valor está fora do intervalo ou callBackUrl é inválidaCompare com o inputSchema do modelo; msg identifica o problema
40040004Todos os campos são válidos, mas essa combinação não pode ser atendida agoraAltere a combinação, por exemplo a resolução ou a duração
401401A chave de API está ausente, incorreta ou desativadaConfira o cabeçalho Authorization
40240201Não há saldo suficiente para reservar estimatedCostAdicione saldo no console e reenvie com a mesma Idempotency-Key
40240202Foi atingido um limite de gastos da chave, equipe ou contaAumente o limite no console ou aguarde a renovação do limite diário ou mensal
40340301Esta chave não tem permissão para chamar o modeloUse outra chave ou altere as restrições de modelos da chave
409409 / 40901Conflito de idempotência ou uma estimativa que já não correspondeVeja Idempotência e Estimativas
413413O corpo da solicitação é grande demais, normalmente devido a mídia embutidaFaça upload do arquivo e envie a URI spicy://; veja Mídia
429429Muitas solicitações em pouco tempoAguarde o período de Retry-After e reenvie com a mesma chave
50350301O modelo está temporariamente indisponívelTente mais tarde com a mesma chave

Lista completa: Erros.

Antes de começar: escolha um modelo e uma entrada válida

Os exemplos abaixo precisam de um ID de modelo real e de um input aceito por esse modelo. O catálogo fornece ambos: includeExamples=1 adiciona entradas que já passaram pelo schema de cada modelo. O comando exige jq, uma pequena ferramenta de JSON para a linha de comando; troque image por video ou audio para outros tipos de modelo.

curl -s "https://api.spicyapi.ai/api/v1/models?modality=image&includeExamples=1" \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  | jq -c '.data.items[] | select(.enabled and .available) | {model, input: .examples[0].input}'

Cada linha impressa é um corpo de solicitação pronto, com dois campos: model e input. Escolha uma. Se uma linha mostrar "input": null, o modelo não tem um exemplo armazenado: consulte o inputSchema em GET /api/v1/models/{model} e monte a entrada. Veja Catálogo de modelos e schemas.

Exemplos

MODEL_ID_FROM_CATALOG abaixo é apenas um marcador: substitua o corpo completo da solicitação pela linha escolhida no catálogo ao vivo. Não envie o marcador literalmente.

curl
# Paste one line printed by the command above between the single quotes.
TASK='{"model":"MODEL_ID_FROM_CATALOG","input":{"prompt":"a folded paper lantern, hard side light"}}'
# One new key per new task. After a network error, rerun only the curl command
# so the same key is sent again and no second task is created.
IDEMPOTENCY_KEY="$(uuidgen)"

curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  --data "$TASK"

Você deve receber o corpo 202 mostrado acima, com code: 200 e um taskId. Guarde o taskId: ele permite obter o resultado. Para receber um callback, adicione "callBackUrl":"https://…" com seu endereço HTTPS ao JSON.

JavaScript
const res = await fetch('https://api.spicyapi.ai/api/v1/jobs/createTask', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SPICY_API_KEY}`,
    'Content-Type': 'application/json',
    // One key per logical request; reuse it across retries
    'Idempotency-Key': 'order-8814-render-1',
  },
  body: JSON.stringify({
    model: 'MODEL_ID_FROM_CATALOG',
    input: { prompt: 'a folded paper lantern, hard side light' },
    // Your own public HTTPS endpoint; delete this line to poll instead
    callBackUrl: 'https://your-app.example.com/hooks/spicy',
  }),
});

const body = await res.json();
if (res.status !== 202 || body.code !== 200) throw new Error(`${res.status} ${body.code} ${body.msg}`);
console.log(body.data.taskId, body.data.deadlineAt);
Python
import os, requests

res = requests.post(
    "https://api.spicyapi.ai/api/v1/jobs/createTask",
    headers={
        "Authorization": f"Bearer {os.environ['SPICY_API_KEY']}",
        "Content-Type": "application/json",
        # One key per logical request; reuse it across retries
        "Idempotency-Key": "order-8814-render-1",
    },
    json={
        "model": "MODEL_ID_FROM_CATALOG",
        "input": {"prompt": "a folded paper lantern, hard side light"},
        # Your own public HTTPS endpoint; delete this line to poll instead
        "callBackUrl": "https://your-app.example.com/hooks/spicy",
    },
)

body = res.json()
if res.status_code != 202 or body["code"] != 200:
    raise RuntimeError(f'{res.status_code} {body["code"]} {body["msg"]}')
print(body["data"]["taskId"], body["data"]["deadlineAt"])

Uma resposta 202 nem sempre indica queued

A primeira solicitação aceita cria uma tarefa queued. Se a mesma chave de API reenviar a mesma solicitação com a mesma Idempotency-Key, esse 202 retorna o estado atual da tarefa original. Ela já pode estar running ou em um estado terminal, mas não é criada uma segunda tarefa nem uma segunda reserva.

2. Estados

queued
running
succeededfailedexpired
Uma tarefa fica em um único estado por vez. Só succeeded é cobrado; qualquer outro estado final devolve integralmente o valor reservado.

Uma tarefa aceita é irreversível: nem queued nem running oferecem uma ação de cancelamento. A API continua podendo ler registros históricos canceled, mas nenhuma nova ação de usuário ou operador cria esse estado.

EstadoSignificadoTerminalCobrança
queuedCriada, com saldo reservado, aguardando processamentoNãoReservado, ainda não liquidado
runningO modelo está gerando; aguardando o resultadoNãoReservado, ainda não liquidado
succeededGeração concluída com sucessoSimLiquidada pelo uso real, limitada ao valor reservado
failedA geração falhouSimNormalmente, a reserva é liberada integralmente; certos resultados content_rejected explicitamente informados na página do modelo são cobrados pela estimativa aceita. Confira settled e cost
canceledApenas compatibilidade histórica; não há uma ação de cancelamento disponívelSimReserva liberada integralmente
expiredUltrapassou o tempo máximo de execução do modeloSimReserva liberada integralmente

Confira a liquidação, não apenas o estado da tarefa

Tarefas que falham ou expiram normalmente liberam a reserva integral. Algumas recusas de conteúdo são cobradas pela estimativa aceita apenas quando a página do modelo informa explicitamente essa exceção. Confira settled e cost para saber a cobrança final. Uma recusa cobrada continua tendo state: "failed" e errorCode: "content_rejected". Um processo de recuperação conclui automaticamente qualquer liquidação ou liberação pendente.

Considere terminal todo estado em que state não pertence a {queued, running}, em vez de enumerar os valores terminais. Isso trata com segurança os registros históricos canceled e impede que um novo estado terminal provoque consultas intermináveis no futuro.

2.5 Dispense o loop de consultas com wait

Acrescente ?wait=<seconds> a createTask e manteremos a conexão aberta até a tarefa chegar a um estado terminal, para que a primeira chamada dispense um loop de consultas.

curl
curl -X POST "https://api.spicyapi.ai/api/v1/jobs/createTask?wait=30" \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"MODEL_ID_FROM_CATALOG","input":{"prompt":"a lighthouse at dusk"}}'
ResultadoO que você recebe
Estado terminal dentro do tempo disponível200 com o registro da tarefa — o mesmo formato retornado por recordInfo
O tempo disponível acabaO 202 normal, idêntico ao de uma solicitação sem wait. Continue consultando a partir daí
Você se desconectaNada muda na tarefa. Ela continua executando e é cobrada normalmente

Um estado terminal não significa necessariamente sucesso

200 aqui indica que a tarefa terminou, não que foi bem-sucedida. Confira data.state — failed e expired chegam com o mesmo código de status, exatamente como em recordInfo.

O teto é de 60 segundos. Valores maiores são limitados ao teto, em vez de rejeitados, e qualquer valor que não seja um inteiro positivo é ignorado, não rejeitado — um erro de digitação em um parâmetro de otimização não deve impedir uma geração que funcionaria sem ele.

Esse teto existe por causa do timeout de 100 segundos na borda da API. Depois desse limite, quem encerra a conexão é a CDN, não nós, e você receberia um 524 sem ID de tarefa nem estado que permita retomá-la — embora a tarefa exista e o saldo já esteja reservado. Os 60 segundos deixam margem para o processamento da própria solicitação.

Use para imagens, não para vídeo

Modelos de vídeo costumam levar minutos. wait simplesmente atingirá o tempo limite, você terá o custo de latência da conexão mantida aberta e ainda precisará consultar o status. Vale a pena quando o modelo costuma terminar em segundos — como a maioria dos endpoints de imagem e edição — e é desperdício nos demais casos. Por decisão de projeto, não há como torná-lo padrão.

Para integrações em produção, webhooks continuam sendo a melhor opção: nenhuma conexão fica aberta, e você recebe os resultados mesmo de tarefas que demoram mais que qualquer tempo de espera escolhido.

3. Consulte uma tarefa

GET /api/v1/jobs/recordInfo?taskId=job_…

Há um parâmetro de consulta obrigatório: taskId. Uma tarefa só pode ser lida com a mesma chave de API que a criou. Consultar com outra chave — mesmo outra da sua conta ou a substituta após uma rotação — retorna 404, exatamente como uma tarefa inexistente, para que diferenças nos erros não permitam enumerar IDs de tarefas de outras pessoas. A mesma regra vale para download-url e retry. Antes de rotacionar uma chave, termine de obter os resultados das tarefas criadas com a antiga.

curl
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3" \
  -H "Authorization: Bearer $SPICY_API_KEY"

A consulta é gratuita. Substitua o ID do exemplo pelo seu taskId.

Campos da resposta

CampoTipoObservações
taskIdstringID da tarefa
sourceTaskIdstringApenas em tarefas criadas por retry: a tarefa de origem da nova tentativa
modelstringIdentificador do modelo
statestringVeja a tabela acima
inputobjectParâmetros validados; imagens embutidas viram URIs de arquivos da plataforma. Omitido após o fim da retenção das entradas
outputobjectDescrição dos arquivos gerados. Só tem significado quando a tarefa está succeeded
errorCodestringPresente quando state é failed ou expired — uma tarefa expirada tem timeout. Um dos nove valores; veja Códigos de falha de tarefas. Use este campo para decidir o fluxo, nunca errorMessage
errorMessagestringMotivo da falha legível por pessoas, em inglês, a menos que outro idioma seja selecionado. Presente apenas em caso de falha
coststringValor liquidado ou reserva, se ainda não houve liquidação; a cobrança final não pode exceder essa reserva
settledbooleanIndica se a cobrança é definitiva. Enquanto for false, cost ainda pode mudar
createdAtstringRFC 3339
deadlineAtstringPrazo de execução registrado no aceite; eventos de webhook antigos podem omiti-lo
completedAtstringRFC 3339. Apenas estados terminais
contentStatestringpresent, expired — removido pela política de retenção — ou purged — você o destruiu. Veja Retenção e destruição
contentRemovedBystringuser ou system; omitido enquanto o conteúdo ainda está presente
purgedAtstringRFC 3339. Apenas após a destruição do conteúdo por você
retentionobjectoutputsExpireAt — quando as saídas são excluídas —, promptsExpireAt — quando o prompt é apagado — e source — qual configuração determinou o prazo

Quatro prazos distintos

  • Seu próprio tempo de espera é definido por você. Parar de esperar não cancela a tarefa remota: guarde o taskId e consulte a mesma tarefa depois para retomar o acompanhamento.
  • deadlineAt é o prazo de execução do servidor. Um reenvio idempotente não o estende; uma nova tentativa explícita cria outra tarefa com seu próprio prazo. Confira o estado terminal e settled, em vez de deduzir um reembolso com base em um cronômetro local.
  • output.assets[].expiresAt encerra a validade de um link temporário do resultado, normalmente após 20 minutos. Uma nova consulta atualiza o link sem criar outra tarefa.
  • A mídia gerada é mantida por no máximo cerca de 14 dias após a conclusão; as entradas enviadas à plataforma, por no máximo um dia. Esses são os limites da plataforma: você pode definir períodos menores em Retenção e destruição ou destruir o conteúdo de uma tarefa sob demanda. Atualizar um link não estende a retenção da mídia que ele aponta.

Se você usa o SDK JavaScript, client.run({ model, input }, { idempotencyKey, onAccepted, timeoutMs }) envia uma vez e consulta com intervalos crescentes de 2 até no máximo 10 segundos; timeoutMs é apenas o tempo de espera local, portanto salve o ID recebido em onAccepted. Veja SDK. Prefira webhooks para trabalhos em segundo plano. Após verificar um callback com resultado completo, use o resultado diretamente, sem outra consulta à tarefa.

Use a URL do resultado diretamente

Arquivos prontos incluem url e expiresAt, além de key e metadados da mídia. Baixe diretamente por output.assets[].url, sem credenciais de API — por exemplo, curl -L -o result.png "<url>". Consulte novamente quando a URL expirar; /common/download-url continua sendo opcional para integrações existentes. As URLs normalmente duram 20 minutos e não estendem a retenção de 14 dias dos resultados.

succeeded
{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "model": "MODEL_ID_FROM_CATALOG",
    "state": "succeeded",
    "input": { "prompt": "a folded paper lantern, hard side light" },
    "output": {
      "assets": [
        {
          "key": "tasks/2026/08/28/job_01k3m8x9q2z4v7n5p6r8s0t1w3/9f3c1d0a7b4e2f68c5a1d3e9b0472fa1.png",
          "url": "https://example.r2.cloudflarestorage.com/results/result.png?X-Amz-Signature=SIGNATURE_FROM_RESPONSE",
          "expiresAt": "2026-08-28T09:32:11Z",
          "mime": "image/png",
          "width": 1024,
          "height": 1024,
          "bytes": 1483920
        }
      ]
    },
    "cost": "0.008",
    "settled": true,
    "createdAt": "2026-08-28T09:12:04Z",
    "deadlineAt": "2026-08-28T09:42:04Z",
    "completedAt": "2026-08-28T09:12:11Z",
    "contentState": "present"
  },
  "request_id": "req_01k3m8xa1b2c3d4e5f6g7h8j9k0m"
}
failed
{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "model": "MODEL_ID_FROM_CATALOG",
    "state": "failed",
    "errorCode": "upstream_failed",
    "errorMessage": "Generation failed; the charge has been refunded",
    "cost": "0",
    "settled": true,
    "createdAt": "2026-08-28T09:12:04Z",
    "deadlineAt": "2026-08-28T09:42:04Z",
    "completedAt": "2026-08-28T09:12:39Z",
    "contentState": "present"
  },
  "request_id": "req_01k3m8xb2c3d4e5f6g7h8j9k0m1n"
}

Uma consulta bem-sucedida não significa uma tarefa bem-sucedida

Ambas as respostas contêm code: 200 — a consulta funcionou. O resultado da tarefa fica em data.state. O código de negócio descreve a consulta; state descreve a tarefa. Não confunda os dois.

O corpo de um webhook usa essa mesma estrutura: o mesmo envelope code: 200 e os mesmos campos de data. O parser desta seção também interpreta um callback sem alterações — dois canais, um parser.

O formato de output

output possui duas partes independentes. Os arquivos gerados — imagens, vídeo e áudio — chegam em output.assets[]. Resultados de texto chegam em output.text, como uma string simples.

A maioria dos modelos preenche apenas uma das duas partes, mas um modelo pode preencher ambas. Qual delas você recebe é uma propriedade do endpoint, não de sua modality: um endpoint speech-to-text fica agrupado em audio no catálogo porque essa é sua entrada, e responde com texto.

`output.assets` nem sempre está presente

Um endpoint que retorna texto não tem a chave assets — nem mesmo como array vazio. Um código que abre o resultado com task.output.assets[0].url lança uma exceção nessas tarefas, em vez de retornar um valor que você possa verificar. Leia output.text primeiro quando estiver presente e trate assets como opcional:

const { text, assets = [] } = task.output ?? {};
if (text) console.log(text);
for (const asset of assets) console.log(asset.url);

Não há arquivo para baixar nem link para expirar em um resultado de texto, mas o texto continua fazendo parte da saída da tarefa: ele é removido no mesmo prazo de qualquer outro resultado, e contentState passa para expired junto com ele. Copie o que precisar guardar. Um resultado que chega em várias partes — os segmentos de uma transcrição, por exemplo — é reunido em uma única string, com quebras de linha entre as partes.

O restante desta seção descreve assets[].

CampoTipoObservações
keystringIdentificador de compatibilidade para atualização opcional do link de download. Vazio enquanto pending
urlstringURL GET assinada direta. Omitida quando o arquivo está pendente ou indisponível; não envie uma chave de API
expiresAtstringExpiração desta URL em RFC 3339; consulte novamente para atualizar o link durante o período de retenção
mimestringTipo de conteúdo, como image/png ou video/mp4
width · heightintegerDimensões em pixels. Ausentes em áudio e texto
durationSecondsnumberDuração em segundos. Apenas vídeo e áudio
bytesintegerTamanho em bytes
pendingbooleanVeja abaixo
unavailablebooleanVeja abaixo
layerobjectApenas em resultados de decomposição em camadas: zIndex — inteiro —, boundingBox — [x1, y1, x2, y2], opcional — e name — opcional. Veja abaixo

Campos que não temos são omitidos — não recebem null nem 0. Interprete-os como opcionais.

Dois indicadores precisam ser tratados:

  • pending: true — o arquivo ainda está sendo copiado para nosso armazenamento, e key e url estão vazios por enquanto. Consulte novamente após alguns segundos; chamar download-url sem uma key nesse momento retorna 409.
  • unavailable: true — o arquivo expirou ou ficou permanentemente indisponível após uma falha de transferência. Esse arquivo não será recuperado; esperar não resolve, então gere novamente. Se nenhum arquivo de uma tarefa bem-sucedida puder ser armazenado, essa tarefa não será cobrada.

Resultados de decomposição em camadas acrescentam o campo layer. O primeiro arquivo é a imagem base — a origem com os elementos separados removidos, zIndex 0 —; todos os demais são camadas PNG transparentes. Empilhe-as em ordem crescente de zIndex para reconstruir a imagem. boundingBox é [x1, y1, x2, y2] em pixels da imagem base: redimensione a camada para preencher essa caixa e ela volta à posição original. Modelos que não informam posições omitem esse campo, e as camadas continuam funcionando como recortes independentes. name é o rótulo do próprio modelo e só aparece quando ele o fornece. A cobrança conta todas as imagens retornadas, incluindo a base.

4. Callbacks primeiro; consultas como alternativa

Polling significa que seu código consulta recordInfo repetidamente até a tarefa terminar. Um callback significa que enviamos a tarefa concluída ao seu servidor uma vez. Ambos fornecem os mesmos dados.

CallbackPolling
LatênciaNo momento em que a tarefa terminaSeu intervalo entre consultas
Solicitações1 por tarefa, feita por nósN por tarefa, feitas por você
Consome seu limite de solicitaçõesNãoSim — veja Limites de solicitações
Exige um endpoint HTTPS públicoSimNão

Use callbacks em produção. Uma tarefa de vídeo de três minutos consultada a cada segundo gera 180 solicitações, quase sem informação nova. O limite geral atual de 1.000 solicitações por 10 segundos da API aberta não restringe a geração paga normal, mas consultas muito frequentes criam tráfego desnecessário, e um loop descontrolado esgota sua própria capacidade de leitura até que recordInfo comece a retornar 429.

Consultar o status faz sentido em duas situações: desenvolvimento local e serviços sem um endpoint público de entrada.

Aumente os intervalos entre consultas

recordInfo usa a cota de leitura e createTask a de escrita, portanto as consultas não podem consumir a capacidade de envio. Ainda assim, se forem excessivas, o próprio recordInfo começará a retornar 429: você deixa de enxergar suas tarefas até a cota se recompor.

Uma estratégia que funciona:

  • Comece com 2–3 segundos. Intervalos menores são desperdício; até os modelos de imagem mais rápidos levam alguns segundos.
  • Multiplique por 1,5, com um teto de 10–15 segundos.
  • Pare no prazo da própria tarefa, não após um número fixo de minutos. Alguns modelos de vídeo podem executar por até uma hora, então um timeout fixo de 10 minutos abandona tarefas que continuam funcionando normalmente. Use deadlineAt da resposta de createTask e acrescente alguns minutos para o servidor registrar o resultado.

Parar de esperar não significa que a tarefa falhou

Quando sua espera termina, você apenas parou de esperar. A tarefa continua executando e ainda pode ter sucesso e ser cobrada. Não a envie novamente com outra Idempotency-Key — isso cria uma segunda tarefa paga. Consulte o mesmo taskId mais tarde ou encontre-o novamente no histórico de tarefas.

JavaScript
// created is the data object returned by createTask.
async function waitForTask(created) {
  // Stop a few minutes after the server's own deadline.
  const giveUpAt = Date.parse(created.deadlineAt) + 5 * 60 * 1000;
  let wait = 2000;

  while (Date.now() < giveUpAt) {
    await new Promise((r) => setTimeout(r, wait));
    wait = Math.min(wait * 1.5, 15000);

    const res = await fetch(
      `https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${encodeURIComponent(created.taskId)}`,
      { headers: { Authorization: `Bearer ${process.env.SPICY_API_KEY}` } },
    );
    const body = await res.json();

    // Being rate-limited is not a task failure — honour Retry-After
    if (body.code === 429) {
      wait = Number(res.headers.get('retry-after') ?? 1) * 1000;
      continue;
    }
    if (body.code !== 200) throw new Error(`${body.code} ${body.msg}`);

    // Terminal means "not one of the two in-flight states"
    const { state } = body.data;
    if (state !== 'queued' && state !== 'running') return body.data;
  }
  // Not a failure: query this taskId again later instead of resubmitting.
  throw new Error(`stopped waiting for ${created.taskId}`);
}

// created = body.data from the createTask example above
const task = await waitForTask(created);
if (task.state === 'succeeded') console.log(task.output.assets[0].url);
else console.log(task.state, task.errorCode);
Python
import os, time, requests
from datetime import datetime

BASE = "https://api.spicyapi.ai/api/v1"
AUTH = {"Authorization": f"Bearer {os.environ['SPICY_API_KEY']}"}


def wait_for_task(created: dict) -> dict:
    """created is the data object returned by createTask."""
    # Stop a few minutes after the server's own deadline.
    deadline = datetime.fromisoformat(created["deadlineAt"].replace("Z", "+00:00"))
    give_up_at = deadline.timestamp() + 5 * 60
    wait = 2.0

    while time.time() < give_up_at:
        time.sleep(wait)
        wait = min(wait * 1.5, 15.0)

        res = requests.get(f"{BASE}/jobs/recordInfo", headers=AUTH, params={"taskId": created["taskId"]})
        body = res.json()

        # Being rate-limited is not a task failure — honour Retry-After
        if body["code"] == 429:
            wait = float(res.headers.get("retry-after", 1))
            continue
        if body["code"] != 200:
            raise RuntimeError(f'{body["code"]} {body["msg"]}')

        # Terminal means "not one of the two in-flight states"
        if body["data"]["state"] not in ("queued", "running"):
            return body["data"]

    # Not a failure: query this taskId again later instead of resubmitting.
    raise TimeoutError(f'stopped waiting for {created["taskId"]}')


# created = body["data"] from the createTask example above
task = wait_for_task(created)
if task["state"] == "succeeded":
    print(task["output"]["assets"][0]["url"])
else:
    print(task["state"], task.get("errorCode"))

5. Confira seu saldo

GET /api/v1/chat/credit retorna available — o que pode financiar novas tarefas —, held — reservado por tarefas em andamento — e total. available pode ficar negativo enquanto um crédito aprovado estiver em uso, e fundos promocionais listados em funding.grants podem pagar apenas determinados modelos. Detalhes completos: Consulta do saldo.

curl
curl https://api.spicyapi.ai/api/v1/chat/credit \
  -H "Authorization: Bearer $SPICY_API_KEY"

Histórico de tarefas e recuperação

GET /api/v1/jobs lista as tarefas visíveis criadas pela chave de API atual. Use-o para recuperar trabalhos após reiniciar a aplicação ou perder um callback. Tarefas de outras chaves e registros ocultos ficam excluídos. As respostas contêm IDs de tarefas, modelo, estado, custo, estado da liquidação, datas e horários e, quando registrado, o requestId da chamada que criou a tarefa — em uma chamada de texto, o mesmo valor de seu cabeçalho X-Request-Id. Elas não incluem prompts, entradas, saídas nem links de mídia. Consulte uma tarefa selecionada com recordInfo para obter o resultado.

Filtre por state, um model exato do catálogo ou alias e datas UTC from / to — formato YYYY-MM-DD, início inclusivo e fim exclusivo. O padrão abrange os últimos sete dias até amanhã em UTC; o intervalo máximo é de 92 dias. limit tem valor padrão 20 e não pode exceder 100.

Omita os filtros que não usar: um valor vazio, como model=, um parâmetro repetido ou um parâmetro desconhecido retorna 400. Uma consulta que leva mais de cinco segundos retorna 503; tente novamente com um intervalo de datas menor.

curl --fail-with-body \
  'https://api.spicyapi.ai/api/v1/jobs?from=2026-09-01&to=2026-09-07&state=failed&limit=20' \
  -H "Authorization: Bearer $SPICY_API_KEY"

Leia data.items. Se data.hasMore for true, envie data.nextCursor como cursor da próxima solicitação, mantendo todos os filtros e datas explícitas sem alterações. A ordem é decrescente por createdAt, depois por ID de tarefa. As páginas refletem o estado ao vivo das tarefas; não são um retrato congelado. Não construa cursores por conta própria. Listar o histórico nunca cria uma tarefa nem repete sua execução.

O mesmo recurso está disponível por client.listTasks(...), spicyapi tasks list e pela ferramenta MCP somente de leitura spicyapi_tasks_list. Mantenha o cost em USD como string decimal; quando settled é false, ainda se trata da estimativa reservada, não da cobrança final.

Se a resposta de criação se perder, primeiro repita a solicitação sem alterações com sua Idempotency-Key original. Uma página de histórico vazia não autoriza recriar o mesmo trabalho pago com outra chave.

Nesta página