Ir al contenido

Idempotencia

Una red que se corta deja a tu app sin saber si su pedido llegó. Si lo repite sin más, puede mandar dos veces el mismo mensaje. La Idempotency-Key resuelve eso: repetir un pedido con la misma clave devuelve lo mismo que la primera vez, sin un segundo efecto en WhatsApp.

Mandas la clave en el encabezado Idempotency-Key: un texto tuyo de 1 a 200 caracteres (los espacios de los extremos se quitan). Lo que pasa depende de si el motor ya la vio:

La clave… Con el mismo contenido Con otro contenido
Es nueva Se acepta el pedido y la clave queda tomada. —
Ya se usó Devuelve el mismo comando (o instancia, o replay). Nada nuevo sale a WhatsApp. 409: el motor no adivina cuál de los dos querías.

Un comando repetido vuelve con su estado actual: si la primera vez respondió en_cola y el mensaje ya salió, el reintento trae enviado, con el mismo command_id y el mismo provider_message_id.

Una clave no vence. El motor no borra los comandos: una clave usada queda tomada para siempre en su alcance.

Dónde Alcance Consecuencia
Comandos de una instancia (mensajes, chats, grupos, perfil…) App + instancia La misma clave en dos instancias son dos pedidos distintos. En una misma instancia, la comparten todas las operaciones: usar pedido-1042 para un mensaje y después para una etiqueta es un 409.
Alta de instancias (POST /v1/instances) App Una clave por alta en toda la app.
Replays (POST /v1/subscriptions/{id}/replays) App Entre todas las suscripciones de la app.

Nunca es global: dos apps pueden usar la misma clave sin enterarse.

El motor compara una huella del contenido, no el texto crudo del cuerpo.

No cambian la huella: los espacios del JSON, el orden de los campos y la zona horaria de una fecha (2026-10-24T18:00:00Z y 2026-10-24T15:00:00-03:00 son lo mismo).

Sí la cambian, en un comando: el tipo, la instancia, cualquier dato del cuerpo (el destino, el texto, la cita, las menciones…), expires_at, correlation_id y la generación de la instancia. En el alta de una instancia, el nombre y el owner_ref. En un replay, la suscripción, el rango o los event_ids y el motivo.

En los envíos y en los comandos de chats, etiquetas, perfil, canales e historial, el motor busca la clave antes de revisar las condiciones que cambian con el tiempo: la ventana para editar un mensaje, la cola llena, el estado de la instancia, que una etiqueta siga existiendo. Así un reintento de algo ya aceptado devuelve el comando aunque ahora el pedido fuera rechazado (la ventana de edición venció, la instancia se desconectó). Sin esto, un reintento tardío recibiría un error por algo que sí se hizo.

Sin ella, la respuesta es 422. Son las operaciones que mandan algo a WhatsApp o que crean trabajo durable:

Dominio Operaciones
Mensajes POST …/messages (todos los tipos, también publicar en un canal) y POST …/read
Chats y etiquetas POST …/chats/actions, POST …/appstate/resync, POST …/labels, PATCH y DELETE …/labels/{label_id}, POST …/labels/{label_id}/associations
Perfil y privacidad PUT …/profile/name, …/profile/about, …/profile/picture, DELETE …/profile/picture, PUT …/privacy, PUT …/privacy/disappearing, POST …/blocklist
Grupos y comunidades POST …/groups, POST …/groups/join, PUT …/groups/{grupo_ref}/name, …/description, …/picture, …/settings, DELETE …/groups/{grupo_ref}/picture, POST …/groups/{grupo_ref}/participants, …/requests, …/leave, …/invite-link, …/invite/send, POST …/communities, POST …/communities/{comunidad_ref}/links
Canales POST …/newsletters, POST …/newsletters/follow, POST …/newsletters/{canal_ref}/unfollow, …/view, …/reaction, PUT …/newsletters/{canal_ref}/mute
Status POST …/status
Historial POST …/history/requests, POST …/history/imports
Webhooks POST /v1/subscriptions/{id}/replays
Operación Con clave Sin clave
POST /v1/instances Repetir devuelve la misma instancia con 200 (la primera vez, 201) y no gasta cupo. Cada pedido crea una instancia.
POST /v1/instances/{id}/commands Repetir devuelve el mismo comando de control. El motor le pone una clave propia: cada pedido es un comando nuevo.
DELETE /v1/instances/{id} Igual. Igual que arriba; archivar dos veces no hace daño.
Operación Qué pasa si la repites
POST /v1/subscriptions Crea otra suscripción. Lista antes de repetir.
POST /v1/subscriptions/{id}/rotate Emite otro secreto.
POST /v1/instances/{id}/pairing Abre otra generación y reemplaza el intento anterior (es lo que quieres para un QR vencido).
POST /v1/instances/{id}/media/uploads Sube otro archivo, con otro media_id. ?sha256= comprueba la integridad, no deduplica.
POST …/contacts/check, …/contacts/info, …/contacts/picture Vuelve a consultar al servidor y vuelve a gastar cuota.
POST …/calls/{call_id}/reject Nada: el rechazo es idempotente por llamada.
POST /v1/commands/{id}/cancel Devuelve el mismo comando cancelado.
PATCH /v1/instances/{id}, PATCH /v1/subscriptions/{id} Aplica el cambio otra vez. Usa version_esperada para no pisar un cambio ajeno (409 si la versión cambió).

La regla es simple: genera la clave una vez, guárdala con el pedido y repite siempre con la misma. Repite ante un error de red, un timeout o un 5xx; no repitas ante un 4xx, que no se arregla solo.

Ventana de terminal
# La clave sale de tu sistema, no del reloj: el mismo pedido, la misma clave.
CLAVE="pedido-1042-confirmacion"
for intento in 1 2 3 4 5; do
status=$(curl -s -o respuesta.json -w '%{http_code}' -X POST \
https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/messages \
-H "Authorization: Bearer TU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $CLAVE" \
-d '{"destino": {"telefono": "+NUMERO_DEL_CONTACTO"}, "texto": "Tu pedido 1042 está confirmado."}')
case "$status" in
202|429) break ;; # aceptado (un 429 de ritmo trae el comando guardado)
000|5*) sleep $((intento * 2)) ;; # red o 5xx: repetir con la misma clave
*) break ;; # 4xx: no se arregla repitiendo
esac
done

Si agotas los intentos, guarda la clave y repite más tarde: mientras uses la misma, nunca habrá un segundo mensaje.

Una buena clave sale de tu sistema, no del momento en que haces el pedido:

Situación Clave Por qué
Un mensaje que nace de una acción de tu sistema pedido-1042-confirmacion El mismo pedido de negocio da la misma clave aunque tu proceso se reinicie.
Una respuesta a un evento <event_id>:1, <event_id>:2 Si tu app procesa dos veces el mismo evento (llega «al menos una vez»), responde una sola vez.
Un mensaje que sí quieres repetir recordatorio-1042-2026-10-24 Otra clave a propósito: es otro mensaje.
Un alta de instancia alta-sucursal-centro Repetir el alta no crea dos instancias.

Evita:

  • Un UUID nuevo en cada intento. Es como no mandar clave: cada reintento es un mensaje nuevo.
  • La hora del pedido. Dos reintentos en segundos distintos son dos claves.
  • Datos personales en la clave (el teléfono, el nombre). La clave vuelve en las respuestas (idempotency_key) y en los registros de tu app.
  • Reusar una clave para otra cosa en la misma instancia: es un 409, aunque sea otra operación.

La generación de una instancia sube cada vez que se vuelve a vincular. En un envío (POST …/messages), si mandas generacion_esperada y no coincide con la actual, el pedido responde 409 y no se acepta. Úsala cuando el mensaje solo tiene sentido para ese número: si el operador desvinculó la instancia y vinculó otro teléfono, tu mensaje no sale por el número nuevo.

Aunque no la mandes, un comando aceptado antes de volver a vincular nunca sale por la nueva vinculación: el motor lo hace fallar con generacion_cambiada. generacion_esperada adelanta ese rechazo al momento de pedir.

En la vinculación, generacion_esperada evita que dos pantallas se pisen: ver La generación.

expires_at pone fecha de vencimiento a un comando: si no salió antes, no sale, y termina fallido con comando_vencido. Sirve para lo que pierde sentido con el tiempo: un código de verificación, un «tu pedido llega en 10 minutos».

expires_at es parte del contenido: la misma clave con otro expires_at es un 409. Si tu app calcula el vencimiento con la hora actual, calcúlalo una vez y guárdalo con la clave, para que el reintento lleve la misma fecha.

  • No deduplica eventos. Tu receptor deduplica por event_id (ver El primer webhook).
  • No vuelve exacto un ambiguo. Un comando ambiguo pudo salir o no: repetirlo con la misma clave devuelve el mismo comando ambiguo, y con otra clave puede duplicar el mensaje. Espera su conciliación; lo explica Comandos asíncronos y eventos.
  • No protege lo que no es un comando. Una subida de multimedia, una consulta de contactos o el alta de una suscripción no la aceptan (tabla de arriba).