> ## Documentation Index
> Fetch the complete documentation index at: https://docs.scanova.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotence et relances

> Relancer un événement serveur en échec sans créer de conversion en double : un event_id stable et des délais d'attente qui doublent à chaque nouvel essai.

Les pannes réseau et les erreurs serveur passagères arrivent. L'API des événements serveur est conçue pour que les relances soient sans danger, à condition de suivre le principe de l'`event_id`.

## Le fonctionnement de l'idempotence

Chaque événement accepte un champ `event_id`. Lorsque le même `event_id` arrive plusieurs fois, le traitement marque la deuxième occurrence comme doublon et l'exclut des analyses. Vos totaux de conversions restent justes, même si vous relancez une requête plusieurs fois.

**La règle :** générez l'`event_id` une seule fois, avant la première tentative, et réutilisez la même valeur à chaque relance de cet événement.

## Schéma de mise en œuvre

```javascript theme={null}
// Node.js example
import crypto from 'node:crypto';

async function sendWithRetry(eventPayload, maxAttempts = 5) {
  // Generate event_id once — do not regenerate on retry
  const payload = {
    ...eventPayload,
    event_id: eventPayload.event_id ?? crypto.randomUUID(),
  };

  let delay = 1000; // start at 1 second

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      const response = await fetch('https://track.scanova.io/server-events', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-API-Key': process.env.SCANOVA_API_KEY,
        },
        body: JSON.stringify(payload),
        signal: AbortSignal.timeout(10_000), // 10s timeout
      });

      // Do not retry on client errors — fix the payload or auth first
      if (response.status >= 400 && response.status < 500) {
        const error = await response.json();
        throw new Error(`Permanent error ${response.status}: ${JSON.stringify(error)}`);
      }

      if (response.ok) return; // success

      // Server error (5xx) — retry
    } catch (err) {
      if (attempt === maxAttempts) throw err;
    }

    await new Promise(resolve => setTimeout(resolve, delay));
    delay = Math.min(delay * 2, 30_000); // cap at 30 seconds
  }
}
```

```python theme={null}
# Python example
import os
import time
import uuid
import requests

def send_with_retry(event_payload: dict, max_attempts: int = 5) -> None:
    payload = {**event_payload, "event_id": event_payload.get("event_id") or str(uuid.uuid4())}
    delay = 1.0

    for attempt in range(1, max_attempts + 1):
        try:
            response = requests.post(
                "https://track.scanova.io/server-events",
                headers={
                    "Content-Type": "application/json",
                    "X-API-Key": os.environ["SCANOVA_API_KEY"],
                },
                json=payload,
                timeout=10,
            )

            if 400 <= response.status_code < 500:
                raise ValueError(f"Permanent error {response.status_code}: {response.text}")

            if response.ok:
                return

        except ValueError:
            raise  # do not retry permanent errors
        except Exception:
            if attempt == max_attempts:
                raise

        time.sleep(delay)
        delay = min(delay * 2, 30)  # cap at 30 seconds
```

## Rythme des relances

| Tentative | Délai avant la tentative |
| --------- | ------------------------ |
| 1         | Immédiat                 |
| 2         | 1 seconde                |
| 3         | 2 secondes               |
| 4         | 4 secondes               |
| 5         | 8 secondes               |

Plafonnez le délai à 30–60 secondes. Après 5 tentatives infructueuses, enregistrez l'événement et déclenchez une alerte — ne relancez pas indéfiniment.

## Quand relancer, quand arrêter

| Statut                                     | Action                                                               |
| ------------------------------------------ | -------------------------------------------------------------------- |
| Délai réseau dépassé / erreur de connexion | Relancer avec un délai croissant                                     |
| `429` Too Many Requests                    | Relancer après la valeur de l'en-tête `Retry-After` (ou 60 secondes) |
| `500`, `502`, `503`, `504`                 | Relancer avec un délai croissant                                     |
| `400` Bad Request                          | Arrêter — corrigez le corps de la requête                            |
| `401` Unauthorized                         | Arrêter — vérifiez votre clé d'API                                   |
| `403` Forbidden                            | Arrêter — vérifiez la correspondance entre la clé et le site\_id     |
| `422` Unprocessable Entity                 | Arrêter — corrigez l'erreur de validation du champ                   |

## Conserver event\_id pour les événements critiques

Pour les conversions à forte valeur (achats, abonnements), générez et enregistrez l'`event_id` avant l'appel réseau — vous pourrez ainsi relancer même après un redémarrage du processus :

```javascript theme={null}
// Generate and store before the API call
const eventId = crypto.randomUUID();
await db.trackingEvents.create({ eventId, status: 'pending', orderId });

try {
  await sendWithRetry({ event_id: eventId, event_name: 'purchase', ... });
  await db.trackingEvents.update({ eventId, status: 'sent' });
} catch (err) {
  await db.trackingEvents.update({ eventId, status: 'failed' });
  // Retry from your job queue
}
```
