Idempotência
Tente de novo depois de um timeout sem gerar duas vezes nem pagar duas vezes.
Uma chave de idempotência é uma etiqueta que você coloca num trabalho, como "renderizar o vídeo do pedido 8814". Se a mesma requisição etiquetada chegar duas vezes à PopNow, o trabalho é feito uma vez só.
POST /api/v1/jobs/createTask
Idempotency-Key: order-8814-render-1Por que usar
Um timeout deixa você no escuro: talvez a requisição nunca tenha chegado, talvez tenha chegado e só a resposta se perdeu. Sem chave, ou você desiste (e o usuário espera à toa) ou tenta de novo (e talvez gere e pague duas vezes). Com chave, tentar de novo é sempre seguro: você recebe uma tarefa nova ou a que acabou de ser criada.
Como funciona
- Repetir com a mesma chave, a partir da mesma chave de API, devolve a tarefa original no estado atual — nada de segunda tarefa, segunda reserva ou segunda cobrança.
- A chave fica presa ao que você enviou:
model, oinputinteiro ecallBackUrl. Ordem dos campos e espaços não importam, mas qualquer mudança real importa. Uma requisição alterada com uma chave antiga retorna409; dê uma chave nova a ela. - Uma chave é lembrada por 24 horas a partir do primeiro uso. Depois disso, a mesma chave cria uma tarefa nova e cobrada, então pare as tentativas automáticas bem antes.
- As chaves têm até 128 caracteres e só precisam ser únicas na sua conta. Outra chave de API da sua conta não pode reaproveitá-la: recebe
409sem o ID da tarefa original.
| Endpoint | Repetir com a mesma chave… |
|---|---|
POST /api/v1/jobs/createTask | devolve a tarefa original |
POST /api/v1/jobs/retry | devolve a tarefa que aquele retry criou (a chave vale por tarefa de origem) |
POST /v1/videos | devolve a tarefa original |
POST /api/v1/assets | devolve o asset já salvo (aqui o cabeçalho é obrigatório) |
Endpoints de leitura e os que geram links de upload ou download ignoram o cabeçalho. jobs/purge não precisa: repeti-lo já é inofensivo.
Escolhendo chaves
- Uma chave por trabalho, não por tentativa HTTP. Toda nova tentativa precisa reutilizá-la.
- Derive de algo que você já guarda:
order-8814-render-1pode ser recalculada depois de uma queda; um UUID aleatório, não — a não ser que você o salve antes. - Se usar UUID, gere antes da primeira tentativa e guarde junto com o trabalho a que ele pertence.
Nunca gere a chave dentro do loop de novas tentativas
// Wrong: a new key per attempt defeats the whole mechanism
for (let i = 0; i < 3; i++) await createTask(body, crypto.randomUUID());
// Right: one key, created once
const key = crypto.randomUUID();
for (let i = 0; i < 3; i++) await createTask(body, key);Conflitos
Todos estes casos retornam code: 409:
| O que aconteceu | Tentar de novo? |
|---|---|
| Duas requisições com a mesma chave chegaram juntas | Sim — espere algumas centenas de milissegundos e reenvie com a mesma chave |
| A chave já foi usada em outra requisição | Não — a requisição mudou e precisa de chave nova |
| Outra chave de API da sua conta já usou esta chave | Não — tente com a chave de API que fez a primeira tentativa |
Seu código não deve decidir pelo msg, então permita uma ou duas novas tentativas em 409 e depois mostre o erro para uma pessoa.
Exemplo completo
const RETRYABLE = new Set([429, 500, 503, 50301]); // business codes from the body
async function createTaskOnce(body, idempotencyKey) {
for (let attempt = 0; attempt < 4; attempt++) {
try {
const res = await fetch('https://api.popnow.ai/api/v1/jobs/createTask', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POPNOW_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(body),
});
const json = await res.json();
if (json.code === 200) return json.data; // a new task, or the original one
if (!RETRYABLE.has(json.code)) throw new Error(`${json.code} ${json.msg} (${json.request_id})`);
} catch (err) {
if (!(err instanceof TypeError)) throw err; // network error: safe to retry with the same key
}
await new Promise((r) => setTimeout(r, 2 ** attempt * 500 + Math.random() * 300));
}
throw new Error('createTask did not succeed after 4 attempts');
}
const task = await createTaskOnce(
{ model: 'alibaba/z-image-turbo/text-to-image', input: { prompt: 'A paper boat on a calm lake' } },
'order-8814-render-1',
);
