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

# Idempotencia

> Cómo evitar disparar 2 llamadas si tu sistema reintenta la misma petición.

Endpoints `POST` que crean recursos (como `/v1/calls`) aceptan un header `Idempotency-Key`. Si tu sistema reintenta una petición por timeout o error de red, mandar la misma key garantiza que la operación se ejecute **una sola vez**.

## Cómo funciona

```http theme={null}
POST /v1/calls
Authorization: Bearer ryvo_live_...
Idempotency-Key: lead-abc-2026-04-30-001
Content-Type: application/json

{ "agent_id": "agent_8801kpabc123", "to": "+5215555550100" }
```

Cuando recibimos esta petición:

1. Buscamos en cache por la combinación `(client_id, endpoint, key)`.
2. **Hit con mismo body** → devolvemos la respuesta cacheada de la primera petición. No volvemos a disparar la llamada.
3. **Hit con body distinto** → respondemos `409 idempotency_conflict`. Es un bug en tu lado: estás reusando una key con datos diferentes.
4. **Miss** → procesamos la petición normal. Si es exitosa, guardamos `(key, body, response)` en cache por 24h.

## Cuándo usarla

<CardGroup cols={2}>
  <Card title="Disparar llamadas desde un workflow" icon="bolt">
    Tu n8n o GHL puede reintentar por timeout. Sin idempotency, cada reintento dispara una llamada nueva.
  </Card>

  <Card title="Mensajes en cola" icon="layer-group">
    Si tu cola entrega mensajes "at-least-once" (SQS, RabbitMQ), el mismo mensaje puede llegar 2 veces.
  </Card>

  <Card title="Webhooks de tu CRM" icon="arrows-rotate">
    Tu CRM puede reintentar webhooks fallidos — usa el mismo ID del CRM como Idempotency-Key.
  </Card>

  <Card title="Cualquier código async" icon="circle-nodes">
    Donde reintenten reintentos, manda la key.
  </Card>
</CardGroup>

## Formato de la key

* 1 a 255 caracteres.
* Solo ASCII imprimible (`!` a `~`).
* Recomendado: UUID v4 (`crypto.randomUUID()`) o un identificador de tu sistema (ej. `lead-{lead_id}-{deal_id}`).

## TTL del cache

Las keys expiran **24 horas** después de la primera petición exitosa. Pasado ese plazo, la misma key puede usarse para una nueva operación distinta.

Si necesitas garantía de duplicados más allá de 24h, usa identificadores estables del lado de tu sistema (ej. `webhook_id` único en tu CRM) y guarda tu propio mapping `webhook_id → call_id`.

## Aislamiento por cliente

Las keys están aisladas por API key (cliente). Si tú usas `Idempotency-Key: 12345` y otro cliente Ryvo usa la misma cadena, no hay colisión — son namespaces separados.

## Ejemplo: integración con backoff

```javascript theme={null}
const idempotencyKey = `lead-${lead.id}-${deal.id}`

async function dispararLlamada(retries = 3) {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      const r = await fetch("https://api.ryvo.so/v1/calls", {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${process.env.RYVO_API_KEY}`,
          "Content-Type": "application/json",
          "Idempotency-Key": idempotencyKey,
        },
        body: JSON.stringify({ agent_id: agent.id, to: lead.phone }),
      })

      if (r.ok) return await r.json()

      // 4xx (excepto 429) → no reintentes
      if (r.status >= 400 && r.status < 500 && r.status !== 429) {
        throw new Error(`API error: ${(await r.json()).error}`)
      }

      // 429 / 5xx → backoff
      const delay = Math.pow(2, attempt) * 1000 + Math.random() * 500
      await new Promise(r => setTimeout(r, delay))
    } catch (err) {
      if (attempt === retries) throw err
    }
  }
}
```

Como mandamos siempre la **misma** `idempotencyKey`, aunque el primer intento haya disparado la llamada exitosa pero el response se perdió en la red, el reintento devuelve la misma respuesta cacheada y NO dispara una segunda llamada.

## Errores comunes

| Error                         | Causa                                                        |
| ----------------------------- | ------------------------------------------------------------ |
| `400 invalid_idempotency_key` | Caracteres no-ASCII o más de 255 chars.                      |
| `409 idempotency_conflict`    | Reusaste una key con un body distinto. Cambia uno o el otro. |
