Módulo 2: Idempotencia: que repetir no duplique

5. Hacer idempotente una llamada a una API

Descripción

Al terminar esta lección vas a poder hacer idempotente un efecto que vive en una API de terceros —como el charge de Cumbre en su pasarela de pago— usando la cabecera Idempotency-Key que las APIs serias ofrecen justo para esto. Vas a saber cómo pasar esa cabecera desde el nodo HTTP Request, qué hace la API cuando recibe dos veces la misma clave, y qué haces cuando la API no ofrece esa cabecera: el patrón de verificar-antes-de-crear, con la gran advertencia que te prepara para la lección 6.

Esto importa porque el upsert de la lección 4 resuelve el duplicado solo cuando el efecto es tu base de datos, donde tú controlas la restricción de unicidad. Pero la mitad de los efectos peligrosos de un workflow real viven en sistemas que no controlas: una pasarela de pago, un proveedor de correo, un CRM en la nube. Ahí no puedes crear una restricción de unicidad; dependes de que la API te dé un mecanismo. La cabecera Idempotency-Key es ese mecanismo, y es el que arregla el segundo cobro de Cumbre —el daño más caro de todo el caso de estudio—.

Conexión con el módulo: esta es la segunda de las dos formas de aplicar la clave de la lección 3 a un efecto. La primera fue el upsert (lección 4), para tus datos; esta es la cabecera, para datos de terceros. Las dos consumen la misma idempotency_key que calculaste en la lección 3 —la reutilizas tal cual como valor de la cabecera—. La lección 6 toma el patrón débil que aparece al final de esta lección —verificar-antes-de-crear— y muestra por qué esconde una condición de carrera; y el proyecto de la lección 8 aplica esta cabecera al charge real de Cumbre.

El problema: no controlas la base de datos de la pasarela

El charge de Cumbre es una petición POST a la pasarela de pago:

POST /charges
{
  "customer_id": "CUST-118",
  "amount": 1780,
  "currency": "MXN"
}

Ya sabes por la lección 2 que POST no es idempotente: cada llamada crea un cobro nuevo. Y ya sabes por la lección 4 cómo arreglar eso cuando escribes en tu propia base de datos: una restricción de unicidad y un upsert. Pero aquí hay un muro: la base de datos de cobros es de la pasarela, no tuya. No puedes entrar a su servidor y agregarle una restricción de unicidad sobre "customer + amount". No tienes acceso. El upsert no es una opción cuando el efecto vive del otro lado de una API.

Entonces, ¿cómo le dices a un sistema que no controlas "este cobro y el anterior son el mismo, no cobres dos veces"? Le mandas la clave, y confías en que él la sepa usar. Ese es el trato de la cabecera de idempotencia.

La cabecera Idempotency-Key

Las APIs bien diseñadas —sobre todo las que mueven dinero, como las pasarelas de pago— ofrecen una solución al problema del duplicado: una cabecera HTTP especial donde tú les mandas tu clave de idempotencia, y ellas se encargan de no repetir el efecto.

El nombre estándar de esa cabecera, en las pasarelas que la implementan, es Idempotency-Key. La usas así: en tu petición POST, además del cuerpo, agregas una cabecera con tu clave:

POST /charges
Idempotency-Key: a3f1c9e2b8...        ← tu clave de idempotencia (la de la lección 3)
{
  "customer_id": "CUST-118",
  "amount": 1780,
  "currency": "MXN"
}

Y esto es lo que hace la API por dentro cuando recibe esa cabecera:

La primera vez que ve esa Idempotency-Key, no la reconoce. Procesa el cobro normalmente, crea el cargo, y guarda el resultado asociado a esa clave: "la clave a3f1c9e2... produjo el cobro ch_777".

La segunda vez que ve la misma Idempotency-Key —porque el reintento del webhook disparó tu workflow otra vez, y tu clave es estable— la reconoce. No crea un segundo cobro. En su lugar, te devuelve el mismo resultado que la primera vez, el cobro ch_777, como si acabara de procesarlo. Para tu workflow, las dos llamadas devuelven éxito y el mismo cobro. Para el cliente, hay un solo cargo.

Reconoces el patrón de la lección 2, ¿verdad? Es el DELETE que responde distinto pero deja el mismo estado. Aquí la primera respuesta dice "creado" y la segunda "aquí está el que ya tenía", respuestas que se sienten distintas, pero el estado de la pasarela es el mismo: un solo cobro. La API te está dando idempotencia sobre un POST que por naturaleza no la tiene.

La analogía: es el folio de una transferencia bancaria. Cuando ordenas una transferencia y le pones un número de referencia, y luego —por nervios, porque la app se congeló— la vuelves a ordenar con el mismo número de referencia, el banco reconoce el folio y no manda el dinero dos veces; te dice "esa transferencia ya la hice". El folio es tu Idempotency-Key, y el banco es la API que lo respeta.

De dónde sale la clave que mandas

Un punto que conecta toda la cadena del módulo: la clave que pones en la cabecera es exactamente la idempotency_key que calculaste en la lección 3. No es una clave nueva ni distinta. Es la misma. La calculaste temprano, después del webhook, y ha viajado con el item hasta aquí; ahora la lees y la mandas en la cabecera.

Esto explica, por fin, por qué en la lección 3 fuimos tan estrictos con que la clave debía ser estable entre reintentos. Si hubieras usado una marca de tiempo o un randomUUID(), la segunda llamada a la pasarela llevaría una clave distinta, la pasarela no la reconocería, y crearía el segundo cobro. Toda la protección de la cabecera se apoya en que tu clave sea la misma en las dos llegadas. La cabecera es el mecanismo; la estabilidad de la clave es lo que lo hace funcionar.

Hay un matiz que las pasarelas serias añaden y conviene conocer: si mandas la misma clave pero con un cuerpo distinto —misma Idempotency-Key, pero esta vez amount: 9999 en lugar de 1780— muchas APIs te devuelven un error a propósito, en vez de procesar. Es una protección: la API asume que si la clave es la misma, la operación debería ser la misma, y una diferencia en el cuerpo huele a bug de tu lado. Tenlo presente: la clave y el contenido deben ir de la mano.

Ejemplo trabajado: la cabecera en el nodo HTTP Request

Vamos a hacer idempotente el charge de Cumbre en el nodo HTTP Request. Asumimos que la idempotency_key ya viene en el item (la calculaste en la lección 3).

Paso 1 — El nodo HTTP Request con su cuerpo. Configuras el nodo para hacer el POST al endpoint de cobros de la pasarela, con el cuerpo de siempre: customer_id, amount, currency. Hasta aquí es el cobro que ya tenías, el que duplicaba.

Paso 2 — Agregar la cabecera. En el nodo HTTP Request hay una opción para enviar cabeceras —normalmente un interruptor tipo Send Headers que, al activarlo, te deja agregar pares de nombre y valor—. Agregas una cabecera:

  • Nombre: Idempotency-Key
  • Valor: {{ $json.idempotency_key }}

La expresión {{ $json.idempotency_key }} toma la clave del item que entra —la que calculaste en la lección 3— y la pone como valor de la cabecera. Verifica en tu versión la etiqueta exacta del interruptor de cabeceras; el concepto no cambia, el texto del botón a veces sí.

Paso 3 — Verificar el nombre de la cabecera contra la documentación de la API. Este paso no es opcional y es fácil de olvidar. El nombre Idempotency-Key es el que usan varias pasarelas grandes, pero no todas las APIs lo llaman igual, y algunas no lo ofrecen en absoluto. Antes de confiar en él, abre la documentación de la API específica que estás llamando y confirma tres cosas: (a) que soporta idempotencia, (b) el nombre exacto de la cabecera, y (c) cuánto tiempo recuerda la clave. En el momento de escribir esta guía, la pasarela de referencia usa Idempotency-Key y recuerda la clave durante unas 24 horas; pero eso puede cambiar y cambia entre proveedores, así que la fuente de verdad es siempre su documentación vigente, no esta lección.

Paso 4 — Probar la idempotencia. Dispara el workflow con ORD-2041. Mira el cobro creado en la pasarela (su panel o su API de consulta). Ahora dispara otra vez el mismo ORD-2041, simulando el reintento.

Qué esperar: la segunda llamada devuelve una respuesta exitosa —no un error— y, al revisar la pasarela, sigue habiendo un solo cobro, el mismo de la primera vez. Si en cambio ves dos cobros, la causa casi siempre es una de dos: la cabecera no se está enviando (revisa que el interruptor esté activo y el nombre bien escrito), o tu idempotency_key no es estable entre las dos llegadas (vuelve a la lección 3 y revisa que no incluya nada temporal). Esas dos son el 90% de los fallos.

Fíjate en lo elegante de esto: no tuviste que construir ninguna tabla, ningún registro, ningún mecanismo propio. Reutilizaste la clave que ya tenías y activaste una cabecera. La pasarela hizo todo el trabajo pesado de recordar y deduplicar. Cuando la API coopera, la idempotencia es casi gratis.

Ejemplo trabajado: las dos llamadas, lado a lado

Para que veas exactamente qué recibe tu workflow, miremos las dos peticiones concretas cuando el webhook de ORD-2041 llega dos veces. Asumimos que tu idempotency_key estable es a3f1c9e2b8....

Primera llamada (primera llegada del webhook):

POST /charges
Idempotency-Key: a3f1c9e2b8...
{ "customer_id": "CUST-118", "amount": 1780, "currency": "MXN" }

Qué esperar en la respuesta:

HTTP/1.1 200 OK
{ "id": "ch_777", "amount": 1780, "status": "succeeded" }

La pasarela nunca había visto esa clave. Creó el cobro ch_777 y guardó "la clave a3f1c9e2... → cobro ch_777". En este momento, el estado de la pasarela es: un cobro, ch_777.

Segunda llamada (reintento del webhook, misma clave porque es estable):

POST /charges
Idempotency-Key: a3f1c9e2b8...
{ "customer_id": "CUST-118", "amount": 1780, "currency": "MXN" }

Qué esperar en la respuesta:

HTTP/1.1 200 OK
{ "id": "ch_777", "amount": 1780, "status": "succeeded" }

Aquí está la magia. La respuesta es un 200 OK exitoso, igual que la primera —tu workflow ni se entera de que fue un reintento—, pero fíjate en el id: es ch_777, el mismo de la primera vez. La pasarela reconoció la clave, no creó un cobro nuevo, y te devolvió el que ya tenía. El estado de la pasarela sigue siendo un cobro, ch_777.

RespuestaEstado de la pasarela después
1.ª llamada200 OK, cobro ch_777 (creado)1 cobro: ch_777
2.ª llamada200 OK, cobro ch_777 (recuperado)1 cobro: ch_777 (sin cambios)

Es el mismo fenómeno del DELETE de la lección 2: la respuesta de la segunda llamada puede sentirse igual o distinta, pero lo que importa es que el estado no cambió. Un solo cobro. El cliente ve un solo cargo. Eso es la idempotencia funcionando.

Cuánto tiempo recuerda la clave la API

Un detalle que afecta tus decisiones de diseño: las APIs no recuerdan las claves para siempre. Guardar el resultado de cada clave cuesta espacio, así que las olvidan pasado un tiempo —en la pasarela de referencia, alrededor de 24 horas, pero verifica el número vigente en su documentación—.

¿Qué implica esto en la práctica? Que la protección de la cabecera cubre los reintentos que ocurren dentro de esa ventana. Un reintento del webhook que llega tres segundos, tres minutos o tres horas después está cubierto: la clave sigue en la memoria de la API. Pero si por alguna razón el "mismo" evento se reprocesara días después —un replay manual de una ejecución vieja, por ejemplo—, la clave ya habría expirado, la API la vería como nueva, y crearía un segundo cobro.

Para los reintentos normales —que ocurren en segundos o minutos— la ventana de 24 horas es más que suficiente y no tienes que pensar en esto. Pero es una razón más por la que un PUT idempotente (que fija un estado y lo es para siempre, sin ventana de expiración) es a veces preferible a la cabecera, y por la que el registro propio del módulo 4 —que no expira a menos que tú lo decidas— es la red de seguridad última para efectos que no puedes permitirte duplicar jamás.

Cuando la API no ofrece la cabecera

No todas las APIs son tan amables. Muchos endpoints —sobre todo de servicios más viejos o más simples— no ofrecen ninguna cabecera de idempotencia. Le mandas dos veces "crea un contacto" y crea dos contactos, y no hay clave que puedas mandar para evitarlo. ¿Qué haces entonces?

El patrón que a todos se nos ocurre primero es verificar antes de crear: primero le preguntas a la API "¿ya existe este recurso?", y solo si no existe, lo creas.

1. GET /contacts?email=luna@example.com   → ¿ya existe?
2. Si NO existe → POST /contacts          → créalo
   Si SÍ existe → no hagas nada

En un nodo de n8n serían dos nodos: un HTTP Request que consulta, un If que decide, y un segundo HTTP Request que crea solo si el If dice que no existía.

Y aquí tengo que ser honesto contigo, porque es el corazón de la siguiente lección: este patrón es mejor que nada, pero es frágil, y su fragilidad es sutil. Funciona perfectamente cuando las ejecuciones ocurren una después de otra, con tiempo de sobra entre ellas. Pero cuando dos ejecuciones corren casi al mismo tiempo —justo lo que pasa cuando el webhook se dispara doble y n8n procesa las dos casi juntas— las dos pueden preguntar "¿existe?", las dos ver "no", y las dos crear. El duplicado que querías evitar aparece igual, porque entre "verificar" y "crear" pasó tiempo, y en ese tiempo la otra ejecución también verificó.

Este problema tiene nombre —la trampa de "verificar y luego actuar"—, es la razón de ser de la lección 6 entera, y por ahora solo quiero que salgas de esta lección con la jerarquía clara:

El orden de preferencia para hacer idempotente un efecto de API:

  1. Si la API ofrece Idempotency-Key (o equivalente): úsala. Es la solución robusta. La API deduplica de forma atómica del lado del servidor, sin ventanas de carrera. Es lo que hicimos con el charge de Cumbre.
  2. Si no la ofrece, pero el recurso tiene un identificador que tú controlas: intenta un PUT en vez de un POST. Recuerda la lección 2: PUT /contacts/luna@example.com "pone el contacto en este estado" y es idempotente por naturaleza, mientras que POST /contacts crea. Si la API te deja fijar por id, ganaste.
  3. Solo si nada de lo anterior es posible, usa verificar-antes-de-crear, sabiendo que tiene una ventana de carrera y protegiéndolo lo mejor que puedas —idealmente respaldándolo con idempotencia en tu propio lado (una tabla con restricción de unicidad que registre "ya llamé a esta API para este evento", que es justo lo que construye el módulo 4)—.

La lección más grande de esta unidad no es la cabecera en sí; es esta jerarquía. Prefiere que el servidor deduplique (opción 1). Si no, fija un estado en vez de crear (opción 2). Y trata "verificar y crear" (opción 3) como el último recurso que es, no como la solución por defecto.

Hay una razón profunda detrás del orden de esta jerarquía, y vale la pena nombrarla: cuanto más cerca de los datos vive la garantía de unicidad, más fuerte es. La cabecera y el PUT ponen la garantía en el servidor que es dueño del dato —el lugar más cercano posible—, y por eso son atómicos y no tienen huecos. Verificar-y-crear pone la garantía en tu workflow, lejos del dato, coordinando dos operaciones a distancia, y por eso tiene una ventana de carrera. La misma idea explica por qué el upsert de la lección 4 es tan robusto: la garantía vive dentro de la base de datos, pegada al dato. Cuando puedas elegir dónde poner la unicidad, ponla lo más cerca del dato que puedas; cuando te toque ponerla lejos, ya sabes que estás en terreno frágil y que necesitas un árbitro atómico propio para compensar.

Un cuidado extra: el correo y otros efectos "de una sola vía"

El charge no es el único efecto del HTTP Request de Cumbre. También manda un correo de confirmación, y los correos tienen una particularidad incómoda: son de una sola vía. Una vez que el correo salió, salió; no hay forma de "des-enviarlo". No puedes hacer un upsert sobre la bandeja de entrada de tu cliente.

Para efectos así, la idempotencia se apoya en lo mismo, con un matiz. Algunos proveedores de correo transaccional sí ofrecen su propia cabecera o campo de idempotencia —le mandas una clave y no envían dos veces el mismo mensaje—; verifica en la documentación del tuyo. Cuando no lo ofrecen, la protección se mueve a tu lado: antes de enviar, consultas tu propio registro de "¿ya mandé el correo de confirmación de ORD-2041?" y solo envías si no. Ese registro es, otra vez, una tabla con restricción de unicidad —el ledger del módulo 4—, y es la razón por la que ese módulo existe: para los efectos que ninguna API deduplica por ti, la memoria de "esto ya lo hice" tiene que vivir en tu sistema.

Por ahora, la conclusión práctica para Cumbre: el charge lo hacemos idempotente con la cabecera de la pasarela (opción 1). El correo, si el proveedor lo permite, con su cabecera; y si no, apoyándonos en el registro propio que el módulo 4 va a construir. No todo se resuelve en esta lección, y está bien: aquí resolvemos el cobro, que es el daño más caro.

Hay un principio general detrás de esto que conviene enunciar, porque te va a guiar en efectos que ni siquiera hemos mencionado. Ordena tus efectos por reversibilidad, y protégelos en ese orden. Un cobro se puede revertir con esfuerzo (una devolución); un correo enviado, no; un mensaje de WhatsApp a un cliente, tampoco. Cuanto menos reversible sea un efecto, más te conviene invertir en hacerlo idempotente antes de dispararlo, porque no vas a tener una segunda oportunidad de arreglarlo después. En order-triage, si tuvieras que elegir un solo efecto para blindar primero, sería el que no puedes deshacer. El módulo 6 retoma esta idea con las "acciones compensatorias" —cómo deshacer lo que no pudiste evitar repetir—, pero la mejor acción compensatoria es la que nunca necesitas porque el efecto fue idempotente desde el principio.

Errores comunes

Confiar en un nombre de cabecera sin verificarlo (práctico). Qué pasa: alguien lee que la cabecera se llama Idempotency-Key, la agrega a un HTTP Request que llama a una API que en realidad la llama distinto —o que no la soporta— y asume que quedó protegido. En producción, los cobros se duplican igual porque la API ignoró una cabecera que no reconoce. Por qué pasa: Idempotency-Key es el nombre de varias pasarelas grandes, y es fácil generalizar que "así se llama en todas". No es cierto: cada API decide su nombre, y algunas no ofrecen ninguna. Cómo detectarlo: revisa la documentación de la API específica que llamas y busca la sección de idempotencia; si no la encuentras, la API probablemente no la soporta y una cabecera inventada no hace nada. Cómo corregirlo: usa el nombre exacto que diga la documentación de esa API. Si no ofrece idempotencia, baja en la jerarquía —intenta un PUT, o respáldalo con un registro propio—; no supongas que una cabecera con un nombre plausible te está protegiendo.

Mandar una clave que cambia entre llegadas (práctico). Qué pasa: la cabecera está bien puesta, con el nombre correcto, pero el valor es {{ $now }}, un randomUUID(), o una clave que incluye la hora. La API recibe una clave distinta en cada llegada, no reconoce ninguna como repetida, y duplica. Por qué pasa: es el error de la lección 3 manifestándose aquí. La cabecera solo funciona si la clave es la misma en las dos llegadas del mismo evento. Cómo detectarlo: mira el valor de la cabecera en dos ejecuciones del mismo evento; si son distintos, ese es el bug. Cómo corregirlo: manda la idempotency_key estable que calculaste en la lección 3 —derivada del contenido, sin nada temporal—. La cabecera correcta con una clave inestable no protege nada.

Tratar "verificar y luego crear" como una solución robusta (conceptual). Qué pasa: la API no ofrece cabecera, así que se arma el patrón de dos nodos —consultar si existe, y crear si no—, se prueba disparando el evento una vez, funciona, y se da por resuelto. En producción, cuando el webhook se dispara doble y las dos ejecuciones corren casi juntas, aparece el duplicado. Por qué pasa: entre el nodo que verifica y el nodo que crea hay una ventana de tiempo, y dos ejecuciones concurrentes pueden verificar las dos "no existe" antes de que cualquiera cree. La prueba manual, secuencial, nunca abre esa ventana. Cómo detectarlo: pregúntate "¿qué pasa si dos copias de este workflow corren al mismo tiempo con el mismo evento?". Si la respuesta es "las dos verifican, las dos crean", tienes el bug. Cómo corregirlo: prefiere las opciones más altas de la jerarquía (cabecera de idempotencia, o un PUT); y cuando de verdad no haya más remedio que verificar-y-crear, respáldalo con idempotencia atómica en tu lado —una restricción de unicidad en tu tabla que impida registrar dos veces el mismo evento—. La lección 6 desarma esta trampa en detalle; que no te agarre desprevenido.

Ejercicios

Ejercicio 1 — Elige la estrategia por API. Para cada efecto, di qué opción de la jerarquía usarías (cabecera de idempotencia, PUT en vez de POST, o verificar-y-crear como último recurso) y por qué:

(a) Un charge en una pasarela de pago cuya documentación describe una cabecera Idempotency-Key. (b) Guardar el perfil de un cliente en un CRM que ofrece PUT /customers/{id} para "poner el cliente en este estado". (c) Crear una tarea en una herramienta vieja que solo ofrece POST /tasks y no menciona idempotencia en ninguna parte de su documentación.

Ver solución

(a) Cabecera de idempotencia (opción 1). La API la ofrece explícitamente; es la solución robusta y del lado del servidor. Manda tu idempotency_key estable en la cabecera Idempotency-Key y listo. No inventes nada más complicado.

(b) PUT en vez de POST (opción 2). El CRM te deja fijar el cliente por su id. PUT /customers/CUST-118 "pone el cliente en este estado" y es idempotente por naturaleza (lección 2): hazlo diez veces y el cliente queda igual. No necesitas cabecera ni verificación; el propio verbo te da la idempotencia.

(c) Verificar-y-crear, como último recurso, respaldado en tu lado (opción 3). No hay cabecera ni forma de fijar por id, así que no queda otra que consultar "¿ya existe esta tarea?" y crear si no. Pero sabiendo que tiene ventana de carrera, lo respaldas con idempotencia propia: antes de llamar a la API, registras el evento en una tabla tuya con restricción de unicidad; si el registro ya existía, no llamas. Así la atomicidad de tu base de datos cubre la ventana que la API no cubre. (Ese registro es el ledger del módulo 4.)

Por qué funciona: aplicaste la jerarquía en orden. La mejor solución disponible cambia según lo que la API ofrezca, y reconocerlo evita tanto complicar de más el caso (a) como confiar de menos en el caso (c).

Ejercicio 2 — Diagnostica el cobro duplicado. Un workflow manda el charge con una cabecera Idempotency-Key bien puesta, con el nombre correcto que confirmaste en la documentación de la pasarela. Aun así, en producción aparecen cobros duplicados. La cabecera se envía en las dos llegadas. ¿Qué revisarías, y cuál es la causa más probable?

Ver solución

Si la cabecera se envía con el nombre correcto en las dos llegadas, la causa más probable es que el valor de la clave es distinto en cada llegada —es decir, la idempotency_key no es estable—.

Qué revisar: compara el valor de la cabecera Idempotency-Key en las dos ejecuciones del mismo evento. Si son distintos, ese es el problema. Rastrea de dónde sale ese valor —el nodo Code de la lección 3— y busca el veneno de siempre: una marca de tiempo (new Date(), Date.now(), un created_at generado al procesar), un randomUUID(), o cualquier fuente de azar dentro del cálculo. La pasarela recibe dos claves distintas, no reconoce la segunda como repetida, y crea el segundo cobro.

Cómo corregirlo: hacer que la clave dependa solo del contenido estable del evento —el order_id natural, o un hash de los campos que no cambian entre reintentos—. La cabecera perfecta con una clave inestable es como poner el folio correcto pero cambiándolo cada vez: el banco nunca lo reconoce.

Por qué funciona: separaste dos cosas que se confunden —"la cabecera está bien puesta" y "la clave es estable"—. La cabecera es el sobre; la clave estable es la carta. Un sobre correcto con una carta distinta cada vez no deduplica nada.

Ejercicio 3 — Reescribe el efecto como idempotente. Cumbre llama a un servicio de facturación con este POST, que crea una factura nueva en cada reintento. El servicio, según su documentación, ofrece tanto una cabecera Idempotency-Key como un endpoint PUT /invoices/{invoice_number}. Propón dos formas de volverlo idempotente y di cuál preferirías.

POST /invoices
{
  "order_id": "ORD-2041",
  "customer_id": "CUST-118",
  "amount": 1780
}
Ver solución

Forma A — cabecera de idempotencia (opción 1): mantienes el POST /invoices y agregas la cabecera Idempotency-Key con tu idempotency_key estable (que aquí puede ser el propio order_id, ORD-2041, ya que es una clave natural buena). La segunda llegada, con la misma clave, no crea una segunda factura.

POST /invoices
Idempotency-Key: ORD-2041
{ "order_id": "ORD-2041", "customer_id": "CUST-118", "amount": 1780 }

Forma B — PUT por identificador (opción 2): en vez de crear, fijas la factura por un número que tú controlas. Si usas el order_id como número de factura, PUT /invoices/ORD-2041 "pone la factura de este pedido en este estado", idempotente por naturaleza.

PUT /invoices/ORD-2041
{ "customer_id": "CUST-118", "amount": 1780 }

Cuál preferir: las dos son robustas (ambas del lado del servidor, ambas atómicas). La Forma B (PUT) es ligeramente más limpia conceptualmente, porque la idempotencia viene del propio verbo HTTP y no depende de que la pasarela recuerde la clave durante cierta ventana de tiempo —un PUT es idempotente para siempre, mientras que una Idempotency-Key se olvida pasadas unas horas—. Si el servicio ofrece las dos, un PUT por un identificador estable como order_id es una elección muy sólida. La Forma A es igual de válida y es la única opción cuando el recurso no tiene un identificador que tú controles.

Por qué funciona: reconociste que el mismo efecto se puede volver idempotente por dos caminos de la jerarquía, y evaluaste sus matices —la cabecera depende de una ventana de memoria de la API; el PUT no—. Cuando tienes las dos, entender esa diferencia es lo que te deja elegir con criterio en lugar de por costumbre.

Resumen y siguiente paso

En esta lección resolviste el duplicado del efecto más caro de Cumbre —el charge— usando la cabecera Idempotency-Key: le mandas a la pasarela tu clave estable de la lección 3, y ella se encarga de no cobrar dos veces, guardando el resultado de la primera llamada y devolviéndolo idéntico cuando llega la segunda con la misma clave. Lo aterrizaste con el folio de una transferencia bancaria, viste cómo pasar la cabecera desde el nodo HTTP Request con {{ $json.idempotency_key }}, y aprendiste el paso que no se salta: verificar en la documentación de la API específica el nombre exacto de la cabecera y cuánto tiempo recuerda la clave, porque Idempotency-Key es común pero no universal, y algunas APIs no la ofrecen. Para esas, conociste la jerarquía de preferencia: primero la cabecera; si no, un PUT que fija un estado en vez de un POST que crea; y solo como último recurso el patrón verificar-antes-de-crear, que es mejor que nada pero esconde una ventana de carrera.

Antes de avanzar a la lección 6 deberías poder: agregar una cabecera Idempotency-Key a un HTTP Request con la clave de la lección 3; explicar por qué la cabecera solo funciona si la clave es estable entre llegadas; y ordenar la jerarquía de preferencia para hacer idempotente un efecto de API.

Esa "ventana de carrera" que mencioné dos veces —el hueco entre verificar y crear— es tan importante y tan traicionera que merece su propia lección. La lección 6 la desarma: por qué "busco si existe y si no lo creo" no es idempotente aunque parezca que lo es, por qué sobrevive intacto a todas tus pruebas y explota solo en producción con dos ejecuciones concurrentes, y por qué la solución correcta no vive en dos nodos separados sino dentro de una sola operación atómica —el upsert de la lección 4 y la cabecera de esta—. Es el error más sutil del módulo, y el que más distingue a quien entiende idempotencia de quien solo la copió.

Recursos