PopNowDocs API
Costruire con l’API
Contenuto principale

Idempotenza

Ritentare dopo un timeout senza generare due volte né pagare due volte.

Una chiave di idempotenza è un’etichetta che metti su un lavoro, come «renderizzare il video dell’ordine 8814». Se la stessa richiesta etichettata arriva due volte a PopNow, il lavoro viene fatto una volta sola.

POST /api/v1/jobs/createTask
Idempotency-Key: order-8814-render-1

Perché ti serve

Un timeout ti lascia nel dubbio: forse la richiesta non è mai arrivata, forse è arrivata e si è persa solo la risposta. Senza chiave puoi solo arrenderti (e l’utente aspetta per niente) o ritentare (e magari generare e pagare due volte). Con la chiave ritentare è sempre sicuro: ottieni una nuova attività oppure quella creata un attimo prima.

Come si comporta

  • Un nuovo invio con la stessa chiave, dalla stessa chiave API, restituisce l’attività originale nel suo stato attuale: nessuna seconda attività, trattenuta o addebito.
  • La chiave è legata a ciò che hai inviato: model, tutto l’input e callBackUrl. Ordine dei campi e spazi non contano, ma ogni modifica reale sì. Una richiesta modificata con una vecchia chiave restituisce 409; dalle una chiave nuova.
  • Una chiave viene ricordata per 24 ore dal primo utilizzo. Dopo, la stessa chiave crea una nuova attività a pagamento, quindi ferma i tentativi automatici molto prima.
  • Le chiavi arrivano a 128 caratteri e devono essere uniche solo nel tuo account. Un’altra chiave API del tuo account non può riusarla: riceve 409 senza l’ID dell’attività originale.
EndpointUn nuovo invio con la stessa chiave…
POST /api/v1/jobs/createTaskrestituisce l’attività originale
POST /api/v1/jobs/retryrestituisce l’attività creata da quel retry (la chiave vale per l’attività di origine)
POST /v1/videosrestituisce l’attività originale
POST /api/v1/assetsrestituisce l’asset già salvato (qui l’header è obbligatorio)

Gli endpoint di lettura e quelli che emettono link di caricamento o download ignorano l’header. jobs/purge non ne ha bisogno: ripeterlo è già innocuo.

Scegliere le chiavi

  • Una chiave per lavoro, non per tentativo HTTP. Ogni nuovo tentativo deve riusarla.
  • Ricavala da qualcosa che conservi già: order-8814-render-1 si può ricalcolare dopo un crash; un UUID casuale no, a meno che tu non l’abbia salvato prima.
  • Se usi un UUID, generalo prima del primo tentativo e conservalo accanto al lavoro a cui appartiene.

Non generare mai la chiave dentro il ciclo dei tentativi

// 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);

Conflitti

Tutti questi casi restituiscono code: 409:

Cosa è successoRitentare?
Due richieste con la stessa chiave sono arrivate insiemeSì — aspetta qualche centinaio di millisecondi e reinvia con la stessa chiave
La chiave è già stata usata per un’altra richiestaNo — la richiesta è cambiata e serve una chiave nuova
Un’altra chiave API del tuo account ha già usato questa chiaveNo — ritenta con la chiave API che ha fatto il primo tentativo

Il tuo codice non deve decidere in base a msg: concedi uno o due tentativi su 409 e poi mostra l’errore a una persona.

Esempio completo

JavaScript
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',
);

In questa pagina