Idempotence
Réessayer après un délai dépassé sans générer deux fois ni payer deux fois.
Une clé d’idempotence est une étiquette que vous posez sur un travail, par exemple « rendre la vidéo de la commande 8814 ». Si la même requête étiquetée arrive deux fois chez PopNow, le travail n’est fait qu’une fois.
POST /api/v1/jobs/createTask
Idempotency-Key: order-8814-render-1Pourquoi en avoir besoin
Un délai dépassé vous laisse dans le doute : peut-être que la requête n’est jamais arrivée, peut-être qu’elle est arrivée et que seule la réponse s’est perdue. Sans clé, soit vous abandonnez (et l’utilisateur attend pour rien), soit vous réessayez (et vous risquez de générer et de payer deux fois). Avec une clé, réessayer est toujours sûr : vous obtenez soit une nouvelle tâche, soit celle créée un instant plus tôt.
Comportement
- Un nouvel envoi avec la même clé, depuis la même clé API, renvoie la tâche d’origine dans son état actuel — pas de deuxième tâche, de deuxième réservation ni de deuxième débit.
- La clé est liée à ce que vous avez envoyé :
model, tout l’inputetcallBackUrl. L’ordre des champs et les espaces n’ont pas d’importance, mais tout vrai changement en a. Une requête modifiée avec une ancienne clé renvoie409; donnez-lui une nouvelle clé. - Une clé est mémorisée 24 heures à partir de sa première utilisation. Ensuite, la même clé crée une nouvelle tâche facturée : arrêtez donc les nouvelles tentatives automatiques bien avant.
- Les clés font jusqu’à 128 caractères et doivent seulement être uniques dans votre compte. Une autre clé API de votre compte ne peut pas la réutiliser : elle reçoit
409sans l’identifiant de la tâche d’origine.
| Endpoint | Un nouvel envoi avec la même clé… |
|---|---|
POST /api/v1/jobs/createTask | renvoie la tâche d’origine |
POST /api/v1/jobs/retry | renvoie la tâche créée par ce retry (la clé est limitée à la tâche source) |
POST /v1/videos | renvoie la tâche d’origine |
POST /api/v1/assets | renvoie la ressource déjà enregistrée (l’en-tête est obligatoire ici) |
Les endpoints de lecture et ceux qui émettent des liens d’envoi ou de téléchargement ignorent l’en-tête. jobs/purge n’en a pas besoin : le répéter est déjà sans effet.
Choisir ses clés
- Une clé par travail, pas par tentative HTTP. Chaque nouvelle tentative doit la réutiliser.
- Dérivez-la de quelque chose que vous stockez déjà :
order-8814-render-1se recalcule après un plantage ; un UUID aléatoire, non, sauf si vous l’avez enregistré avant. - Si vous utilisez un UUID, générez-le avant la première tentative et gardez-le à côté du travail auquel il correspond.
Ne générez jamais la clé dans la boucle de nouvelles tentatives
// 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);Conflits
Tous ces cas renvoient code: 409 :
| Ce qui s’est passé | Réessayer ? |
|---|---|
| Deux requêtes avec la même clé sont arrivées au même moment | Oui — attendez quelques centaines de millisecondes et renvoyez avec la même clé |
| La clé a déjà servi pour une autre requête | Non — la requête a changé, il lui faut une nouvelle clé |
| Une autre clé API de votre compte a déjà utilisé cette clé | Non — réessayez avec la clé API qui a fait la première tentative |
Votre code ne doit pas se baser sur msg : autorisez donc une ou deux nouvelles tentatives sur 409, puis remontez l’erreur à une personne.
Exemple complet
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',
);
