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:
- Você envia o trabalho e recebe imediatamente um
taskId. Nada terminou ainda; o preço do trabalho é separado do seu saldo — uma reserva. - Depois, você volta e pergunta “esse
taskIdjá 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. - Quando
stateésucceeded, o link do arquivo está emdata.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. Confirasettledecostpara 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/createTaskEsta 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çalho | Obrigatório | Observações |
|---|---|---|
Authorization | Sim | Bearer sk-spicy-… |
Content-Type | Sim | application/json |
Idempotency-Key | Não | Altamente recomendado: permite repetir com segurança após um erro de rede. Veja Idempotência |
X-Spicy-Retention | Não | Mantenha 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: 2O 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"
}| Campo | Observações |
|---|---|
taskId | ID da tarefa, com prefixo job_. Todas as operações seguintes dependem dele; salve-o |
state | queued para uma nova tarefa; o estado atual da tarefa original em um reenvio idempotente com a mesma chave |
estimatedCost | Valor 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 |
deadlineAt | Prazo 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.
| HTTP | code | Significado mais comum | O que fazer |
|---|---|---|---|
| 400 | 400 | Um campo de input não existe no schema do modelo, um valor está fora do intervalo ou callBackUrl é inválida | Compare com o inputSchema do modelo; msg identifica o problema |
| 400 | 40004 | Todos os campos são válidos, mas essa combinação não pode ser atendida agora | Altere a combinação, por exemplo a resolução ou a duração |
| 401 | 401 | A chave de API está ausente, incorreta ou desativada | Confira o cabeçalho Authorization |
| 402 | 40201 | Não há saldo suficiente para reservar estimatedCost | Adicione saldo no console e reenvie com a mesma Idempotency-Key |
| 402 | 40202 | Foi atingido um limite de gastos da chave, equipe ou conta | Aumente o limite no console ou aguarde a renovação do limite diário ou mensal |
| 403 | 40301 | Esta chave não tem permissão para chamar o modelo | Use outra chave ou altere as restrições de modelos da chave |
| 409 | 409 / 40901 | Conflito de idempotência ou uma estimativa que já não corresponde | Veja Idempotência e Estimativas |
| 413 | 413 | O corpo da solicitação é grande demais, normalmente devido a mídia embutida | Faça upload do arquivo e envie a URI spicy://; veja Mídia |
| 429 | 429 | Muitas solicitações em pouco tempo | Aguarde o período de Retry-After e reenvie com a mesma chave |
| 503 | 50301 | O modelo está temporariamente indisponível | Tente 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.
# 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.
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);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
queuedrunningsucceededfailedexpiredUma 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.
| Estado | Significado | Terminal | Cobrança |
|---|---|---|---|
queued | Criada, com saldo reservado, aguardando processamento | Não | Reservado, ainda não liquidado |
running | O modelo está gerando; aguardando o resultado | Não | Reservado, ainda não liquidado |
succeeded | Geração concluída com sucesso | Sim | Liquidada pelo uso real, limitada ao valor reservado |
failed | A geração falhou | Sim | Normalmente, 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 |
canceled | Apenas compatibilidade histórica; não há uma ação de cancelamento disponível | Sim | Reserva liberada integralmente |
expired | Ultrapassou o tempo máximo de execução do modelo | Sim | Reserva 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 -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"}}'| Resultado | O que você recebe |
|---|---|
| Estado terminal dentro do tempo disponível | 200 com o registro da tarefa — o mesmo formato retornado por recordInfo |
| O tempo disponível acaba | O 202 normal, idêntico ao de uma solicitação sem wait. Continue consultando a partir daí |
| Você se desconecta | Nada 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 "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
| Campo | Tipo | Observações |
|---|---|---|
taskId | string | ID da tarefa |
sourceTaskId | string | Apenas em tarefas criadas por retry: a tarefa de origem da nova tentativa |
model | string | Identificador do modelo |
state | string | Veja a tabela acima |
input | object | Parâmetros validados; imagens embutidas viram URIs de arquivos da plataforma. Omitido após o fim da retenção das entradas |
output | object | Descrição dos arquivos gerados. Só tem significado quando a tarefa está succeeded |
errorCode | string | Presente 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 |
errorMessage | string | Motivo da falha legível por pessoas, em inglês, a menos que outro idioma seja selecionado. Presente apenas em caso de falha |
cost | string | Valor liquidado ou reserva, se ainda não houve liquidação; a cobrança final não pode exceder essa reserva |
settled | boolean | Indica se a cobrança é definitiva. Enquanto for false, cost ainda pode mudar |
createdAt | string | RFC 3339 |
deadlineAt | string | Prazo de execução registrado no aceite; eventos de webhook antigos podem omiti-lo |
completedAt | string | RFC 3339. Apenas estados terminais |
contentState | string | present, expired — removido pela política de retenção — ou purged — você o destruiu. Veja Retenção e destruição |
contentRemovedBy | string | user ou system; omitido enquanto o conteúdo ainda está presente |
purgedAt | string | RFC 3339. Apenas após a destruição do conteúdo por você |
retention | object | outputsExpireAt — 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
taskIde 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 esettled, em vez de deduzir um reembolso com base em um cronômetro local.output.assets[].expiresAtencerra 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.
{
"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"
}{
"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[].
| Campo | Tipo | Observações |
|---|---|---|
key | string | Identificador de compatibilidade para atualização opcional do link de download. Vazio enquanto pending |
url | string | URL GET assinada direta. Omitida quando o arquivo está pendente ou indisponível; não envie uma chave de API |
expiresAt | string | Expiração desta URL em RFC 3339; consulte novamente para atualizar o link durante o período de retenção |
mime | string | Tipo de conteúdo, como image/png ou video/mp4 |
width · height | integer | Dimensões em pixels. Ausentes em áudio e texto |
durationSeconds | number | Duração em segundos. Apenas vídeo e áudio |
bytes | integer | Tamanho em bytes |
pending | boolean | Veja abaixo |
unavailable | boolean | Veja abaixo |
layer | object | Apenas 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, ekeyeurlestão vazios por enquanto. Consulte novamente após alguns segundos; chamardownload-urlsem umakeynesse momento retorna409.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.
| Callback | Polling | |
|---|---|---|
| Latência | No momento em que a tarefa termina | Seu intervalo entre consultas |
| Solicitações | 1 por tarefa, feita por nós | N por tarefa, feitas por você |
| Consome seu limite de solicitações | Não | Sim — veja Limites de solicitações |
| Exige um endpoint HTTPS público | Sim | Nã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
deadlineAtda resposta decreateTaske 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.
// 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);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 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.

