Idempotencia
Reintenta tras un timeout sin generar dos veces ni pagar dos veces.
Una clave de idempotencia es una etiqueta que le pones a un trabajo, como «renderizar el video del pedido 8814». Si la misma solicitud etiquetada llega dos veces a PopNow, el trabajo se hace una sola vez.
POST /api/v1/jobs/createTask
Idempotency-Key: order-8814-render-1Por qué la necesitas
Un timeout te deja adivinando: quizá la solicitud nunca llegó, o quizá llegó y solo se perdió la respuesta. Sin clave, o te rindes (y el usuario espera para nada) o reintentas (y quizá generas y pagas dos veces). Con clave, reintentar siempre es seguro: obtienes una tarea nueva o la que se creó hace un momento.
Cómo se comporta
- Repetir con la misma clave desde la misma clave de API devuelve la tarea original en su estado actual: ni segunda tarea, ni segunda retención, ni segundo cobro.
- La clave queda ligada a lo que enviaste:
model, todo elinputycallBackUrl. El orden de los campos y los espacios no importan, pero cualquier cambio real sí. Una solicitud modificada con una clave vieja devuelve409; dale una clave nueva. - Una clave se recuerda 24 horas desde su primer uso. Pasado ese tiempo, la misma clave crea una tarea nueva que se cobra, así que detén los reintentos automáticos mucho antes.
- Las claves tienen hasta 128 caracteres y solo tienen que ser únicas dentro de tu cuenta. Otra clave de API de tu cuenta no puede reutilizarla: recibe
409sin el ID de la tarea original.
| Endpoint | Repetir con la misma clave… |
|---|---|
POST /api/v1/jobs/createTask | devuelve la tarea original |
POST /api/v1/jobs/retry | devuelve la tarea que creó ese reintento (la clave se limita a la tarea de origen) |
POST /v1/videos | devuelve la tarea original |
POST /api/v1/assets | devuelve el recurso ya guardado (aquí la cabecera es obligatoria) |
Los endpoints de lectura y los que emiten enlaces de subida o descarga ignoran la cabecera. jobs/purge no la necesita: repetirlo ya es inofensivo.
Cómo elegir claves
- Una clave por trabajo, no por intento HTTP. Cada reintento debe reutilizarla.
- Derívala de algo que ya guardas:
order-8814-render-1se puede recalcular tras un fallo; un UUID aleatorio no, salvo que lo guardes antes. - Si usas un UUID, genéralo antes del primer intento y guárdalo junto al trabajo al que pertenece.
Nunca generes la clave dentro del bucle de reintentos
// 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);Conflictos
Todos estos casos devuelven code: 409:
| Qué pasó | ¿Reintentar? |
|---|---|
| Llegaron a la vez dos solicitudes con la misma clave | Sí: espera unos cientos de milisegundos y reenvía con la misma clave |
| La clave ya se usó para otra solicitud | No: la solicitud cambió y necesita una clave nueva |
| Otra clave de API de tu cuenta ya usó esta clave | No: reintenta con la clave de API que hizo el primer intento |
Tu código no debería decidir según msg, así que permite uno o dos reintentos de 409 y después muestra el error a una persona.
Ejemplo 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',
);
