Idempotencia

Una red se cae, salta un timeout, no recibes la respuesta. ¿Reintentas? Con una Idempotency-Key, sí, sin miedo: la misma operación no se hace dos veces ni se cobra dos veces. Hoy la honra el alta de enlaces cortos. Es opcional; si no mandas la llave, todo funciona como siempre.

5 min de lectura

Qué es

La idempotencia garantiza que repetir una petición tenga el mismo efecto que hacerla una sola vez. Tú generas un identificador único por operación y lo envías en el header Idempotency-Key. Si tu organización ya creó un enlace con esa llave, la API te devuelve ese enlace en lugar de crear otro, y no lo vuelve a cobrar.

La llave no vence: queda guardada con el enlace que creó, para siempre, y es única dentro de tu organización. Reintentar un minuto después o un mes después es igual de seguro. La otra cara: una llave usada no sirve para otro enlace, ni siquiera meses más tarde.

Piensa en la llave como "una por intención de negocio", no "una por reintento". El reintento del mismo envío reúsa la misma llave. Dos envíos distintos llevan dos llaves distintas.

Cómo usarla

  1. Genera un identificador único por operación. Recomendamos un UUID v4.
  2. Envíalo en el header Idempotency-Key de la petición.
  3. Si la operación falla por red, por timeout o con un 5xx, reinténtala con la misma llave y el mismo cuerpo.
  4. Para una operación distinta, usa una llave nueva.

La llave acepta de 1 a 255 caracteres del conjunto A-Z a-z 0-9 _ -. El alcance es por organización: tu llave nunca choca con la de otra cuenta. Un header vacío es lo mismo que no mandarlo.

Ejemplos

Solo agregas un header. El cuerpo y el resto de la petición no cambian.

curl -X POST https://developers.hablame.co/api/v6/urlshortener/links \
  -H "Authorization: Bearer hk_TU_API_KEY" \
  -H "Idempotency-Key: 5f3b2c10-9a7e-4b2d-8c1f-0a1b2c3d4e5f" \
  -H "Content-Type: application/json" \
  -d '{"longUrl":"https://hablame.co/promo"}'

Respuestas

La API te dice qué pasó con cada llave en el header Idempotency-Status.

Nueva: se ejecutó

La primera vez. El enlace se crea, se cobra (salvo que tu plan lo incluya) y la llave queda guardada con él.

HTTP
HTTP/1.1 201 Created
Idempotency-Status: created

{ "success": true, "data": { "domain": "h0b.co", "code": "a1b2c3d", "shortUrl": "https://h0b.co/a1b2c3d", "status": "active" } }

Repetida: replay

Reintento con la misma llave y el mismo cuerpo. No se crea otro enlace, no se cobra otra vez y no gasta el cupo diario de tu plan: recibes el mismo 201 con el enlace que ya existe. Llega como está hoy: si lo editaste después, con su destino nuevo; si lo eliminaste, con status: deleted. Cuenta como el mismo cuerpo aunque cambien las mayúsculas del alias, los espacios alrededor del destino o la zona horaria con la que escribes el mismo vencimiento.

HTTP
HTTP/1.1 201 Created
Idempotency-Status:   replayed
Idempotency-Replayed: true

{ "success": true, "data": { "domain": "h0b.co", "code": "a1b2c3d", "shortUrl": "https://h0b.co/a1b2c3d", "status": "active" } }

Dos a la vez: un solo enlace

Si mandas la misma llave mientras la primera petición todavía se está procesando —por ejemplo, porque tu cliente se cansó de esperar y reintentó—, no recibes un error ni tienes que esperar: las dos terminan con el mismo enlace. Se crea y se cobra uno solo, y la que llega segunda responde Idempotency-Status: replayed.

Reuso con otro cuerpo: 422

Usaste una llave que tu organización ya usó, con un cuerpo distinto. No se crea nada ni se cobra. Casi siempre es un error del cliente: una llave identifica una operación, no varias. Usa una llave nueva.

HTTP
HTTP/1.1 422 Unprocessable Entity

{ "success": false, "error": { "code": "IDEMPOTENCY_KEY_REUSED" } }

Llave inválida: 400

La llave es demasiado larga o trae caracteres fuera de A-Z a-z 0-9 _ -. No se hace nada.

HTTP
HTTP/1.1 400 Bad Request

{ "success": false, "error": { "code": "IDEMPOTENCY_KEY_INVALID" } }

Recomendaciones para reintentos automáticos

  • Genera la llave antes del primer intento. Guárdala y reúsala en cada reintento del mismo evento. Si generas una llave nueva por intento, pierdes la protección.
  • Ante un timeout, un error de red o un 5xx, reintenta con la misma llave y el mismo cuerpo: si el enlace alcanzó a crearse, lo recibes; si no, se crea ahora. En ningún caso se cobra dos veces.
  • Trata el 422 como un error de tu lado: cambiaste el cuerpo bajo la misma llave. Corrige la lógica, no reintentes en bucle.
  • Los reintentos gastan el cupo del endpoint como cualquier petición: usa retroceso exponencial con variación aleatoria, igual que con los límites de uso.

La llave solo queda guardada cuando se crea un enlace. Si la primera petición falló con un error —un dato inválido, saldo insuficiente, un alias ocupado—, no se creó nada y la llave sigue libre: el reintento se ejecuta desde cero y, si el problema sigue, recibe el mismo error. No se guardan errores para repetirlos.

Dónde aplica hoy

Hoy la honran dos endpoints, los dos del alta de enlaces cortos: `POST /api/v6/urlshortener/links`, el alta de un enlace, y `POST /api/v6/urlshortener/links/batch`, el alta en lote. En el lote la llave es del lote entero: al repetirlo, los enlaces que ya se crearon vuelven como replayed —sin crearse ni cobrarse otra vez— y lo que faltó se vuelve a intentar. En los demás endpoints la cabecera no tiene efecto: un reintento se vuelve a ejecutar y, si la operación se cobra, se cobra otra vez.