> ## 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.

# Idempotencia y reintentos

> Cómo reintentar eventos de servidor fallidos sin crear conversiones duplicadas: usa un event_id estable y espera cada vez más entre un intento y el siguiente.

Los fallos de red y los errores temporales del servidor ocurren. La API de eventos de servidor está pensada para que los reintentos sean seguros, siempre que sigas el patrón del `event_id`.

## Cómo funciona la idempotencia

Todos los eventos aceptan un campo `event_id`. Cuando el mismo `event_id` llega más de una vez, el sistema marca la segunda aparición como duplicada y la excluye de la analítica. Tus recuentos de conversiones se mantienen exactos aunque reintentes una petición varias veces.

**La regla:** genera el `event_id` una sola vez, antes del primer intento, y reutiliza ese mismo valor en cada reintento del evento.

## Patrón de implementación

```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
```

## Calendario de reintentos

| Intento | Espera previa |
| ------- | ------------- |
| 1       | Inmediato     |
| 2       | 1 segundo     |
| 3       | 2 segundos    |
| 4       | 4 segundos    |
| 5       | 8 segundos    |

Limita la espera a 30–60 segundos. Después de 5 intentos fallidos, registra el evento y avisa: no reintentes indefinidamente.

## Cuándo reintentar y cuándo parar

| Estado                                      | Acción                                                                  |
| ------------------------------------------- | ----------------------------------------------------------------------- |
| Tiempo de espera de red / error de conexión | Reintentar con espera creciente                                         |
| `429` Too Many Requests                     | Reintentar pasado el valor de la cabecera `Retry-After` (o 60 segundos) |
| `500`, `502`, `503`, `504`                  | Reintentar con espera creciente                                         |
| `400` Bad Request                           | Parar: corrige el cuerpo de la petición                                 |
| `401` Unauthorized                          | Parar: revisa tu clave de API                                           |
| `403` Forbidden                             | Parar: comprueba que la clave y el site\_id coinciden                   |
| `422` Unprocessable Entity                  | Parar: corrige el error de validación del campo                         |

## Guardar el event\_id de los eventos críticos

En conversiones de alto valor (compras, suscripciones), genera y guarda el `event_id` antes de la llamada de red, para poder reintentar incluso después de reiniciar el proceso:

```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
}
```
