# Peticiones idempotentes

> Usa la cabecera Idempotency-Key para reintentar con seguridad las peticiones de envío y de reenvío. Formato de la clave, la ventana de 24 horas, repeticiones, respuestas 409 y 503, y estrategias para elegir la clave.

Las redes fallan. Cuando se agota el tiempo de espera de una petición de envío, no puedes saber si Emailit la ha recibido, y volver a enviarla podría hacer que tu cliente recibiera el email dos veces. Una cabecera `Idempotency-Key` hace que el reintento sea seguro: Emailit procesa la primera petición y devuelve la misma respuesta a cualquier repetición con la misma clave.

## Cómo funciona

Añade una cabecera `Idempotency-Key` a `POST /emails` o a `POST /emails/{id}/forward`.

1. **Primera petición.** Emailit reserva la clave para tu espacio de trabajo y procesa la petición.
2. **Éxito.** Emailit guarda la respuesta durante 24 horas. Cualquier petición con la misma clave dentro de ese plazo recibe la respuesta guardada con `200`, y no se crea ningún email nuevo.
3. **Fallo.** Si la petición falla, por ejemplo con un `400` o un `402`, Emailit libera la clave. Corrige el problema y reinténtalo con la misma clave.
4. **Solapamiento.** Si llega una segunda petición mientras la primera todavía se está procesando, recibe `409` y no se envía nada. Reinténtalo al poco tiempo con la misma clave.

Las claves están limitadas a tu espacio de trabajo, así que dos espacios de trabajo pueden usar la misma clave sin conflictos.

> **Es la clave, no el cuerpo, lo que identifica la petición:** Emailit no compara los cuerpos de las peticiones. Una repetición con la misma clave devuelve la respuesta original aunque el cuerpo sea distinto. Usa una clave nueva para cada email diferente.

## Formato de la clave

| Regla | Valor |
| --- | --- |
| Longitud | De 1 a 256 caracteres |
| Caracteres | Letras `A–Z` y `a–z`, dígitos `0–9`, guion `-` y guion bajo `_` |
| Ámbito | Por espacio de trabajo |
| Ventana | 24 horas desde la primera respuesta correcta |

Una clave con otros caracteres, como `:` o `/`, se rechaza con `400 Invalid Idempotency-Key`.

## Elegir una clave

Obtén la clave a partir del evento que provoca el email, para que todos los caminos de reintento generen la misma clave:

| Email | Clave de ejemplo |
| --- | --- |
| Recibo de un pedido | `order-1042-receipt` |
| Restablecimiento de contraseña | `password-reset-7f3c9a1e` (el ID del token de restablecimiento) |
| Resumen semanal | `digest-user-881-2026-w40` |
| Tarea en segundo plano | El ID de la tarea, o un UUID que generas al poner la tarea en cola y guardas con ella |

Evita las claves que cambian entre intentos, como las marcas de tiempo o un UUID generado dentro del bucle de reintentos. Hacen que cada reintento parezca una petición nueva.

## Enviar con una clave de idempotencia

Los ejemplos de Node.js, Python y PHP reintentan cuando hay errores de red y con las respuestas `409`, `429` y `5xx`, reutilizando la misma clave cada vez. El ejemplo de cURL usa el reintento integrado de curl, que cubre los tiempos de espera agotados, `429` y la mayoría de las respuestas `5xx`.

**cURL**

```bash
curl https://api.emailit.com/v2/emails \
  -H "Authorization: Bearer $EMAILIT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-receipt" \
  --retry 3 \
  -d '{
    "from": "Acme <orders@acme.com>",
    "to": "ada@example.com",
    "subject": "Receipt for order 1042",
    "text": "Thanks for your order."
  }'
```

**Node.js**

```javascript
async function sendOnce(payload, key, attempts = 4) {
  for (let attempt = 1; attempt <= attempts; attempt++) {
    try {
      const res = await fetch('https://api.emailit.com/v2/emails', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.EMAILIT_API_KEY}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': key,
        },
        body: JSON.stringify(payload),
      });
      if (res.ok) return res.json();
      if (![409, 429].includes(res.status) && res.status < 500) {
        throw new Error(`Send failed: ${res.status} ${await res.text()}`);
      }
      const wait = Number(res.headers.get('retry-after')) || attempt * 2;
      await new Promise((r) => setTimeout(r, wait * 1000));
    } catch (err) {
      if (err.message.startsWith('Send failed') || attempt === attempts) throw err;
      await new Promise((r) => setTimeout(r, attempt * 2000));
    }
  }
  throw new Error('Send failed after retries');
}

const email = await sendOnce(
  {
    from: 'Acme <orders@acme.com>',
    to: 'ada@example.com',
    subject: 'Receipt for order 1042',
    text: 'Thanks for your order.',
  },
  'order-1042-receipt',
);
```

**Python**

```python
import os
import time
import requests

def send_once(payload, key, attempts=4):
    for attempt in range(1, attempts + 1):
        try:
            res = requests.post(
                "https://api.emailit.com/v2/emails",
                headers={
                    "Authorization": f"Bearer {os.environ['EMAILIT_API_KEY']}",
                    "Idempotency-Key": key,
                },
                json=payload,
                timeout=30,
            )
        except requests.RequestException:
            if attempt == attempts:
                raise
            time.sleep(attempt * 2)
            continue
        if res.ok:
            return res.json()
        if res.status_code not in (409, 429) and res.status_code < 500:
            res.raise_for_status()
        time.sleep(int(res.headers.get("retry-after", attempt * 2)))
    raise RuntimeError("Send failed after retries")

email = send_once(
    {
        "from": "Acme <orders@acme.com>",
        "to": "ada@example.com",
        "subject": "Receipt for order 1042",
        "text": "Thanks for your order.",
    },
    "order-1042-receipt",
)
```

**PHP**

```php
function sendOnce(array $payload, string $key, int $attempts = 4): array
{
    for ($attempt = 1; $attempt <= $attempts; $attempt++) {
        $ch = curl_init('https://api.emailit.com/v2/emails');
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer ' . getenv('EMAILIT_API_KEY'),
                'Content-Type: application/json',
                'Idempotency-Key: ' . $key,
            ],
            CURLOPT_POSTFIELDS => json_encode($payload),
        ]);
        $body = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        curl_close($ch);

        if ($body !== false && $status >= 200 && $status < 300) {
            return json_decode($body, true);
        }
        if ($body !== false && !in_array($status, [409, 429]) && $status < 500) {
            throw new RuntimeException("Send failed: $status $body");
        }
        sleep($attempt * 2);
    }
    throw new RuntimeException('Send failed after retries');
}

$email = sendOnce([
    'from' => 'Acme <orders@acme.com>',
    'to' => 'ada@example.com',
    'subject' => 'Receipt for order 1042',
    'text' => 'Thanks for your order.',
], 'order-1042-receipt');
```

## Respuestas

| Estado | Cuándo | Qué hacer |
| --- | --- | --- |
| `200` | Primera petición correcta, o una repetición suya en un plazo de 24 horas | Usa la respuesta. Una repetición tiene el mismo cuerpo, incluido el mismo `id`. |
| `400 Invalid Idempotency-Key` | La clave está vacía, es demasiado larga o tiene caracteres no válidos | Corrige la clave. |
| `409 Idempotency key in progress` | Todavía se está procesando una petición con la misma clave | Espera un momento y reinténtalo con la misma clave. |
| `503 Idempotency unavailable` | Emailit no ha podido acceder a su almacén de idempotencia, así que ha rechazado la petición para no arriesgarse a un duplicado | Reinténtalo con la misma clave. |
| Cualquier otro error | La petición ha fallado y la clave se ha liberado | Corrige la causa y reinténtalo con la misma clave. |

Los límites de velocidad se comprueban antes que la clave, así que un reintento puede recibir igualmente `429`. Espera lo que indique la cabecera `retry-after` y vuelve a enviar la misma clave.

## Ver también

- [Idempotencia](/es/docs/api-reference/idempotency/) en la referencia de la API
- [Enviar un email](/es/docs/email-api/send-email/)
- [Reenviar un email](/es/docs/email-api/retry-and-forward/)
- [Límites de velocidad](/es/docs/api-reference/rate-limits/)
- [¿Por qué se envían mis emails dos veces?](/es/docs/kb/duplicate-emails-sent/)

---
Fuente: https://emailit.com/es/docs/email-api/idempotency/
