Idempotência
Repita uma solicitação após um tempo limite sem gerar nem pagar duas vezes.
Uma chave de idempotência é um identificador que você associa a uma operação, como “gerar o vídeo do pedido 8814”. Se a mesma solicitação com esse identificador chegar duas vezes, executamos o trabalho apenas uma vez.
createTask recebe a chave no cabeçalho Idempotency-Key. Enviar duas vezes com a mesma chave, pela mesma conta e com a mesma chave de API, retorna a tarefa criada na primeira vez: não há uma segunda entrada na fila, reserva de saldo ou cobrança.
POST /api/v1/jobs/createTask
Idempotency-Key: order-8814-render-1A chave fica vinculada ao que você enviou: o modelo, todo o input e o callBackUrl. A ordem dos campos e os espaços em branco não importam, mas qualquer alteração real importa. Enviar a mesma chave com uma solicitação diferente retorna 409, com Idempotency-Key has already been used for a different request. Repetir esse 409 nunca resolve o problema: se a solicitação mudou de fato, ela é uma nova operação e precisa de outra chave.
Por que usar
Um tempo limite de rede deixa você sem saber o que aconteceu: a solicitação pode nunca ter chegado, ou pode ter chegado e apenas a resposta se perdeu. Sem chave de idempotência, restam duas opções ruins: não tentar novamente, deixando o usuário esperar por nada, ou tentar novamente, correndo o risco de gerar e pagar duas vezes.
Com uma chave, a repetição é segura: a tarefa é criada ou você recebe a que foi criada instantes antes.
Quais endpoints leem o cabeçalho
| Endpoint | O que acontece ao repetir com a mesma chave |
|---|---|
POST /api/v1/jobs/createTask | Retorna a tarefa original em seu estado atual |
POST /api/v1/jobs/retry | Retorna a tarefa criada pela nova tentativa. O escopo da chave é a tarefa original; usar a mesma chave com tarefas originais diferentes não causa conflito |
POST /v1/videos | Retorna a tarefa original |
POST /api/v1/jobs/stream e endpoints de texto em /v1 e /v1beta | Retorna 400; veja abaixo |
Os endpoints de leitura e os que emitem links de upload e download ignoram o cabeçalho. POST /api/v1/jobs/purge não precisa de chave: repeti-lo para o mesmo taskId já é inofensivo.
Em endpoints de texto e streaming, a chave evita uma cobrança duplicada, mas não recupera uma resposta perdida
A resposta de uma solicitação de texto é enviada à medida que é gerada e não é armazenada. Repetir uma chave cuja solicitação já foi concluída, com o mesmo corpo, retorna 400 (This Idempotency-Key was already used for a completed request, and its answer cannot be replayed. Use a new Idempotency-Key to send a new request) sem nova cobrança; o texto não pode ser recuperado em nenhum lugar. Se a solicitação anterior falhou, a repetição retorna o mesmo erro, para que você decida se deve tentar novamente com outra chave. Com um corpo diferente, a chave retorna 409.
Portanto, nesses endpoints, use uma nova chave para cada nova mensagem e reutilize uma chave somente para repetir exatamente a mesma solicitação após um erro de rede, aceitando que sua resposta foi perdida. Se quiser uma resposta novamente, envie a solicitação com outra chave e pague por ela. Veja Texto e streaming.
O prazo é de 24 horas
Uma chave permanece válida por 24 horas a partir do momento em que foi aceita pela primeira vez. As repetições não estendem esse prazo. Depois disso, a mesma chave é tratada como uma solicitação nova e cria uma tarefa nova e cobrada; encerre as tentativas automáticas bem antes desse prazo.
Vinte e quatro horas é um equilíbrio entre necessidades. As tentativas dos clientes terminam em segundos ou minutos, então basta que o prazo cubra esse intervalo; por outro lado, quanto maior o prazo, mais tempo a chave fica indisponível para reutilização. Com uma chave fixa no código, você só conseguiria enviar uma tarefa por dia.
Como escolher as chaves
- Uma chave por solicitação lógica, não por chamada HTTP. As repetições precisam reutilizar a mesma chave; caso contrário, o mecanismo perde sua função.
- Derive a chave de um identificador que você já tem:
order-8814-render-1é melhor que um UUID aleatório, pois você consegue reconstruí-la após reiniciar o processo. - Quando não houver um identificador de origem, gere um UUID antes da primeira tentativa e salve-o, por exemplo no banco de dados, junto ao pedido correspondente; as novas tentativas leem o valor salvo.
- As chaves são registradas dentro da sua conta e não precisam ser globalmente únicas, mas todas as tentativas de uma solicitação lógica também precisam usar a mesma chave de API. O tamanho máximo é de 128 caracteres.
Outra chave de API da conta não pode recuperar a tarefa por repetição
O registro da chave de idempotência vale para a conta inteira, mas a visibilidade das tarefas é isolada por chave de API. Outra chave de API que usar uma chave de idempotência já registrada recebe 409 Conflict (Idempotency-Key is already in use by this account), e a resposta não revela o ID da tarefa original. Isso impede que uma chave investigue as tarefas de outra integração por repetição. Tentar novamente não ajuda; envie a repetição com a chave de API usada na primeira tentativa.
Nunca gere a chave dentro do laço de tentativas
// Wrong: a new key per attempt, which defeats the whole mechanism
for (let i = 0; i < 3; i++) {
await create({ 'Idempotency-Key': crypto.randomUUID() });
}
// Right: generate once, outside the loop
const key = crypto.randomUUID();
for (let i = 0; i < 3; i++) {
await create({ 'Idempotency-Key': key });
}Como é uma solicitação repetida
Em createTask, a resposta contém o mesmo taskId e estimatedCost, mas state é o estado atual da tarefa original. Ele pode ser queued, running ou um estado final. A repetição não cria outra tarefa, reserva de saldo ou cobrança.
Nenhum cabeçalho ou campo indica que você recebeu uma repetição. Para ler o registro completo da tarefa, use o taskId em recordInfo.
Envios simultâneos com a mesma chave
Quando duas solicitações da mesma chave de API, com a mesma chave de idempotência, chegam ao mesmo tempo, exatamente uma cria uma tarefa e a outra recebe o mesmo taskId. A decisão se apoia em uma restrição de unicidade no banco de dados, em vez de uma sequência de verificar e depois inserir: sob concorrência, ambas as verificações poderiam não encontrar nada, as duas tarefas seriam criadas e as duas reservas seriam mantidas.
Em casos raros, essa disputa aparece como:
{ "code": 409, "msg": "Idempotency key conflict, please retry shortly" }Aguarde algumas centenas de milissegundos e tente novamente com a mesma chave.
Outros dois erros 409 em createTask não devem ser repetidos: Idempotency-Key has already been used for a different request, quando você alterou a solicitação, e Idempotency-Key is already in use by this account, quando outra chave de API da conta já usou essa chave. Os três compartilham code: 409, e seu código não deve tomar decisões com base em msg; portanto, permita no máximo uma ou duas novas tentativas para 409 e depois apresente o erro para análise humana.
Exemplo completo
A função recebe a chave como argumento. Calcule-a com base nos seus próprios registros, aqui o ID do pedido, para que um processo reiniciado envie a mesma chave novamente.
// These are business codes from the response body, not HTTP statuses.
const RETRYABLE = new Set([429, 500, 50301]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const backoff = (attempt) => sleep(2 ** attempt * 500 + Math.random() * 300);
async function createTaskWithRetry(payload, idempotencyKey, { attempts = 4 } = {}) {
let conflicts = 0;
for (let i = 0; i < attempts; i++) {
// Declared outside try so the checks below can read them.
let res;
let body;
try {
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',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(payload),
});
body = await res.json();
} catch {
// Transport failure or unreadable body: the request may well have landed.
// Retrying with the same key is safe.
await backoff(i);
continue;
}
if (res.status === 202 && body.code === 200) return body.data;
// Only one of the three 409s is a transient race, so cap 409 retries at one.
const retry = body.code === 409 ? ++conflicts <= 1 : RETRYABLE.has(body.code);
if (!retry) {
throw new Error(`${body.code} ${body.msg} (request_id=${body.request_id})`);
}
await backoff(i);
}
// Still unknown whether the task exists. Try again later with the SAME key.
throw new Error(`out of retries for Idempotency-Key ${idempotencyKey}`);
}
// payload is { model, input } built from the live catalog; order is your own record.
// The key is derived from the order, so it is identical after a restart.
const task = await createTaskWithRetry(payload, `order-${order.id}-render-1`);
console.log(task.taskId);import os, random, time, requests
# These are business codes from the response body, not HTTP statuses.
RETRYABLE = {429, 500, 50301}
def create_task_with_retry(payload: dict, idempotency_key: str, attempts: int = 4) -> dict:
conflicts = 0
for i in range(attempts):
try:
res = requests.post(
"https://api.spicyapi.ai/api/v1/jobs/createTask",
headers={
"Authorization": f"Bearer {os.environ['SPICY_API_KEY']}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
json=payload,
timeout=30,
)
body = res.json()
except (requests.RequestException, ValueError):
# Transport failure or unreadable body: the request may well have landed.
# Retrying with the same key is safe.
time.sleep(2**i * 0.5 + random.random() * 0.3)
continue
if res.status_code == 202 and body["code"] == 200:
return body["data"]
# Only one of the three 409s is a transient race, so cap 409 retries at one.
if body["code"] == 409:
conflicts += 1
retry = conflicts <= 1
else:
retry = body["code"] in RETRYABLE
if not retry:
raise RuntimeError(f'{body["code"]} {body["msg"]} (request_id={body.get("request_id")})')
time.sleep(2**i * 0.5 + random.random() * 0.3)
# Still unknown whether the task exists. Try again later with the SAME key.
raise RuntimeError(f"out of retries for Idempotency-Key {idempotency_key}")
# payload is {"model": ..., "input": ...} built from the live catalog; order_id is yours.
# The key is derived from the order, so it is identical after a restart.
task = create_task_with_retry(payload, f"order-{order_id}-render-1")
print(task["taskId"])Falhas de transporte também exigem a mesma chave
Uma exceção em fetch, um tempo limite de conexão ou um tempo limite de leitura: em todos esses casos, você não sabe se a solicitação chegou. É justamente para isso que serve a chave de idempotência: repita com a mesma chave, e o pior resultado será receber a tarefa anterior. Se acabar o número de tentativas, mantenha a chave: uma chamada posterior ainda retornará a mesma tarefa dentro do prazo de 24 horas.
Webhooks
Diferencie os payloads v1 e v2, selecione o ID correto da tarefa e verifique assinaturas com exemplos funcionais em Node.js, Python e curl.
Cotações e compatibilidade de protocolos
Descubra o preço exato de uma requisição antes de pagar, envie-a por esse preço, recupere-se de uma resposta perdida e entenda como são cobradas as APIs compatíveis de texto e vídeo.

