Ir al contenido

Mensajes

Todo envío pasa por una sola operación, POST /v1/instances/{id}/messages: el campo tipo dice qué sale (un texto, una ubicación, una reacción, un borrado…). El motor guarda el envío como un comando y responde enseguida; lo que pasa después (aceptado por WhatsApp, entregado, leído) llega como eventos. Lo que llega de WhatsApp (mensajes, ediciones, reacciones, recibos) también es un evento del ledger de la instancia. Esta página cubre los dos sentidos.

La multimedia tiene su página, Multimedia, y los botones, las listas, los carruseles y las encuestas, la suya, Interactivos. El modelo de comandos y eventos está en Comandos asíncronos y eventos.

Operación Scope
POST /v1/instances/{id}/messages (cualquier tipo) messages:send
POST /v1/instances/{id}/read (leído o reproducido) messages:send
POST /v1/instances/{id}/presence, …/chat-presence, …/presence-subscriptions y GET …/presence-subscriptions presence:write
GET /v1/instances/{id}/messages, …/messages/{message_ref} y …/messages/{provider_id}/receipts messages:read
GET /v1/commands/{id} y POST /v1/commands/{id}/cancel el scope del tipo del comando (messages:send en un envío)
GET /v1/instances/{id}/events events:read
GET /v1/instances/{id}/chats y …/chats/{peer_ref} chats:read
POST /v1/instances/{id}/chats/actions chats:manage

Si la key está restringida a instancias, solo alcanza a esas: una instancia de fuera responde 404.

Cada envío lleva:

  • destino: exactamente uno de telefono (número internacional, con +), jid (un usuario @s.whatsapp.net o @lid, o un grupo @g.us) o peer_ref (el que traen los eventos de esta instancia). Ver Identidades: PN, LID y peer_ref.
  • Idempotency-Key (encabezado, obligatorio, de 1 a 200 caracteres): repetir el pedido con la misma clave devuelve el mismo comando y nunca manda un segundo mensaje; la misma clave con otro contenido es 409. Ver Idempotencia.
  • Opcionales para cualquier tipo: expires_at (si no salió antes, no sale: fallido con comando_vencido), correlation_id (hasta 200 caracteres; vuelve en los eventos del comando) y generacion_esperada (si la instancia se volvió a vincular, 409).

El cuerpo es cerrado: un campo que no va con el tipo (por ejemplo, ubicacion en un text) es 422 con el campo en errors.

tipo Qué manda Campos propios
text (por omisión) Un texto, con cita, menciones o vista previa de un enlace texto (hasta 4096), cita, menciones, vista_previa
location Una ubicación fija ubicacion (latitud, longitud, nombre, direccion), cita
contacts De 1 a 10 tarjetas de contacto contactos, cita
reaction Una reacción (emoji vacío la quita) objetivo, emoji
edit Edita un texto propio objetivo, texto
revoke Borra para todos un mensaje propio objetivo
forward Reenvía un mensaje recibido objetivo
disappearing Fija el temporizador de mensajes temporales del chat segundos
image, video, document, audio, ptt, ptv, sticker, gif Multimedia: ver Multimedia medio, texto, cita, menciones
poll, interactive Encuesta, botones, lista o carrusel: ver Interactivos pregunta, opciones, multiple / interactivo, cita

Los ejemplos de Go usan el SDK cuando trae el método (EnviarTexto, Comando, Eventos). Para el resto, usan este helper con net/http y el mismo cliente:

// El SDK de Go no trae todos los tipos: este helper manda JSON con el token del cliente.
func llamar(ctx context.Context, c *motor.Cliente, metodo, ruta, clave string, cuerpo any) (*http.Response, error) {
var r io.Reader
if cuerpo != nil {
b, err := json.Marshal(cuerpo)
if err != nil {
return nil, err
}
r = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, metodo, c.Base+ruta, r)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.Token)
if cuerpo != nil {
req.Header.Set("Content-Type", "application/json")
}
if clave != "" {
req.Header.Set("Idempotency-Key", clave)
}
return c.HTTP.Do(req)
}

Una cita (cita.provider_id, y cita.autor con el JID de quien lo escribió si es un grupo) tiene que ser un mensaje de este mismo chat en esta instancia: si no, 422 cita_ajena.

Las menciones van en menciones (hasta 50): cada una es un teléfono internacional (+número), un JID de usuario o un peer_ref de esta instancia, y el motor la resuelve a su JID. El texto debería nombrar a cada mencionado con @número. Una mención que no se resuelve es 422 mencion_invalida.

Un texto puede llevar la vista previa de un enlace que aparece en él (vista_previa.url, con titulo, descripcion y miniatura_media_id, una imagen subida). El motor no visita la página salvo con obtener: true y un host de los destinos permitidos (los mismos de los webhooks); si no, 422 url_no_permitida.

Ventana de terminal
curl -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: pedido-1042-respuesta" \
-d '{
"destino": {"jid": "GRUPO@g.us"},
"tipo": "text",
"texto": "@NUMERO_DEL_CONTACTO tu pedido 1042 sale hoy.",
"cita": {"provider_id": "PROVIDER_MESSAGE_ID", "autor": "CONTACTO@s.whatsapp.net"},
"menciones": ["+NUMERO_DEL_CONTACTO"]
}'
202 Accepted
{
"command_id": "5c2e7a10-0000-4000-8000-0000000000c1",
"instance_id": "8a1d2c3e-0000-4000-8000-0000000000a1",
"idempotency_key": "pedido-1042-respuesta",
"type": "message.text",
"generation": 1,
"status": "en_cola",
"created_at": "2026-10-24T15:10:00Z",
"updated_at": "2026-10-24T15:10:00Z"
}

Si el ritmo de la instancia no deja mandarlo ya, la respuesta es 429 con el comando guardado, su next_try y Retry-After: no lo repitas, sale solo a su turno. El ritmo de fábrica es de 1 mensaje por segundo con ráfagas de 2, por instancia (cada instancia puede tener el suyo: config.mensajes_por_segundo y config.rafaga). Si la cola de la instancia está llena (200 envíos pendientes de fábrica), también es 429, pero sin guardar: ahí sí hay que reintentar después.

Una ubicación lleva latitud (-90 a 90) y longitud (-180 a 180), y opcionalmente nombre y direccion. Los contactos son de 1 a 10: con nombre y telefonos (hasta 5), el motor arma la vCard; con vcard propia (hasta 10 000 caracteres), la valida (422 vcard_invalida si está mal formada).

Ventana de terminal
curl -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: pedido-1042-ubicacion" \
-d '{
"destino": {"telefono": "+NUMERO_DEL_CONTACTO"},
"tipo": "location",
"ubicacion": {"latitud": -12.0464, "longitud": -77.0428, "nombre": "Tienda Centro", "direccion": "Av. Principal 123"}
}'
Cuerpo de un envío de contactos
{
"destino": {"telefono": "+NUMERO_DEL_CONTACTO"},
"tipo": "contacts",
"contactos": [{"nombre": "Soporte", "telefonos": ["+NUMERO_DEL_CONTACTO"]}]
}

Las cuatro operaciones apuntan a otro mensaje con objetivo.provider_id:

  • reaction: emoji (hasta 32 caracteres) sobre un mensaje de este chat; emoji: "" quita la reacción.
  • edit: solo un texto propio de este chat, hasta 20 minutos después de mandarlo.
  • revoke: borra para todos un mensaje propio de este chat, hasta 2 días y 12 horas después.
  • forward: reenvía un mensaje que el motor guardó con su contenido, de cualquier chat de la instancia, al destino.

El motor revisa la ventana al aceptar (422 tipificado) y otra vez al salir: si venció mientras esperaba en la cola, el comando falla con fuera_de_ventana sin mandar nada.

Ventana de terminal
# Reaccionar
curl -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: reaccion-1042" \
-d '{"destino": {"peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1"},
"tipo": "reaction", "objetivo": {"provider_id": "PROVIDER_MESSAGE_ID"}, "emoji": "👍"}'
# Editar un texto propio
curl -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: edicion-1042" \
-d '{"destino": {"peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1"},
"tipo": "edit", "objetivo": {"provider_id": "PROVIDER_MESSAGE_ID"}, "texto": "Texto corregido"}'
Cuerpos de un borrado y de un reenvío
{"destino": {"peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1"}, "tipo": "revoke", "objetivo": {"provider_id": "PROVIDER_MESSAGE_ID"}}
{"destino": {"telefono": "+NUMERO_DEL_CONTACTO"}, "tipo": "forward", "objetivo": {"provider_id": "PROVIDER_MESSAGE_ID"}}

POST /v1/instances/{id}/read (scope messages:send, Idempotency-Key obligatoria) es un comando durable: chat.read o, con reproducido: true, chat.played. Indica el chat en destino y uno de los dos: provider_ids (hasta 100 mensajes recibidos de ese chat) o hasta (todos los recibidos hasta ese momento que no estaban leídos); los dos o ninguno es 422. En un grupo sale un recibo por remitente.

Reproducido no es leído: es para notas de voz y vídeos; marcar así otro tipo termina fallido con no_reproducible. Si el chat no tiene mensajes que marcar, fallido con sin_mensajes. Al completarse, cada mensaje sube a leido o reproducido con su message.updated.

Ventana de terminal
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/read \
-H "Authorization: Bearer TU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: leido-chat-1042" \
-d '{"destino": {"peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1"}, "hasta": "2026-10-24T15:12:00Z"}'

La presencia no es un comando: sale en el momento por la sesión conectada, sin cola. Si la instancia no está conectada, 409 no_conectada.

  • POST /v1/instances/{id}/presence con {"disponible": true} pone la instancia «en línea» (false, fuera de línea). Responde 204.
  • POST /v1/instances/{id}/chat-presence con destino y estado (escribiendo, grabando o pausado) muestra «escribiendo…» o «grabando audio…» en ese chat. Responde 204.
  • POST /v1/instances/{id}/presence-subscriptions con destino y ttl_seg (60 a 86 400; por omisión, 3600) sigue la presencia de un contacto: mientras está vigente, cada cambio llega como evento presence. Pasado el TTL caduca sola; repetirla la renueva. Responde 201.
Ventana de terminal
# 1. En línea (WhatsApp no manda la presencia de otros a quien no lo está)
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/presence \
-H "Authorization: Bearer TU_API_KEY" -H "Content-Type: application/json" \
-d '{"disponible": true}'
# 2. «Escribiendo…» en el chat
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/chat-presence \
-H "Authorization: Bearer TU_API_KEY" -H "Content-Type: application/json" \
-d '{"destino": {"telefono": "+NUMERO_DEL_CONTACTO"}, "estado": "escribiendo"}'
# 3. Seguir su presencia 10 minutos
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/presence-subscriptions \
-H "Authorization: Bearer TU_API_KEY" -H "Content-Type: application/json" \
-d '{"destino": {"telefono": "+NUMERO_DEL_CONTACTO"}, "ttl_seg": 600}'
  • GET /v1/instances/{id}/messages lista lo que el motor capturó (en vivo, importado del historial o enviado), del más nuevo al más viejo, con next_cursor y has_more. Filtros: peer_ref, direccion (entrante, saliente), origen (live, history, operator), operacion (mensaje, edicion, revocacion, reaccion, indescifrable), desde, hasta y limit. No es una búsqueda en WhatsApp: lo que el motor no capturó no aparece.
  • GET /v1/instances/{id}/messages/{message_ref} trae un mensaje, su estado y los recibos de cada destinatario.
  • GET /v1/instances/{id}/messages/{provider_id}/receipts trae entregado, leido y reproducido de cada destinatario de un mensaje propio (en un grupo, de cada participante).
Ventana de terminal
curl "https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/messages?peer_ref=4b7d9e01-0000-4000-8000-0000000000b1&limit=50" \
-H "Authorization: Bearer TU_API_KEY"
curl https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/messages/PROVIDER_MESSAGE_ID/receipts \
-H "Authorization: Bearer TU_API_KEY"
GET …/receipts · 200 OK
{
"recibos": [
{
"destinatario": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": ["CONTACTO@s.whatsapp.net"] },
"tipo": "entregado",
"momento": "2026-10-24T15:10:03Z"
},
{
"destinatario": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": ["CONTACTO@s.whatsapp.net"] },
"tipo": "leido",
"momento": "2026-10-24T15:11:40Z"
}
]
}

El estado de cada chat en la app de WhatsApp (fijado, archivado, silenciado, la marca «no leído», borrado, con candado y sus etiquetas) se lee con GET /v1/instances/{id}/chats y GET …/chats/{peer_ref} (scope chats:read). Se cambia con POST /v1/instances/{id}/chats/actions (scope chats:manage, Idempotency-Key obligatoria), un comando durable con accion pin, unpin, archive, unarchive, mute, unmute, mark_read, mark_unread, star, unstar o delete (irreversible: exige confirmar_irreversible: true). El resultado llega como appstate.changed. «Archivado» en WhatsApp no es «conversación resuelta» de tu CRM: no se sincronizan solos. Las etiquetas están en Etiquetas y Business.

Cada envío es un comando con su type: message.text, message.location, message.contacts, message.reaction, message.edit, message.revoke, message.forward o message.disappearing (la multimedia y los interactivos tienen los suyos). Nace en_cola, pasa a ejecutando y termina enviado, fallido o ambiguo. Leer y reproducir son chat.read y chat.played, que nacen aceptado y terminan completado o fallido. Consulta cualquiera con GET /v1/commands/{id}; cancélalo con POST /v1/commands/{id}/cancel mientras siga aceptado o en_cola.

Evento Cuándo
command.accepted En la misma transacción que el comando.
message.sent El servidor de WhatsApp aceptó el envío. No es entregado ni leído.
message.updated (operacion: estado) El mensaje subió a entregado, leido o reproducido.
command.failed No tuvo efecto: error_code y stage.
command.ambiguous Pudo salir o no. El motor nunca lo reenvía; si llega su eco o un recibo, se concilia.
message.sent · data (una edición propia)
{
"chat": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@s.whatsapp.net", "clase": "usuario", "aliases": [] },
"provider_id": "PROVIDER_MESSAGE_ID_DE_LA_EDICION",
"command_id": "5c2e7a10-0000-4000-8000-0000000000c3",
"aceptado_en": "2026-10-24T15:13:02Z",
"operacion": "edicion",
"objetivo": "PROVIDER_MESSAGE_ID"
}

En un envío normal, message.sent no trae operacion ni objetivo; en una edición, un borrado o una reacción propios, operacion es edicion, revocacion o reaccion y objetivo es el provider_id del mensaje al que apunta. Si el comando había quedado ambiguo y lo confirmó su eco o un recibo, trae "conciliado": true.

command.failed · data
{
"command_id": "5c2e7a10-0000-4000-8000-0000000000c4",
"type": "message.edit",
"error_code": "fuera_de_ventana",
"stage": "previa",
"provider_message_id": "PROVIDER_MESSAGE_ID_DE_LA_EDICION"
}

Lo que llega de WhatsApp queda en el ledger y te llega por webhook, stream o broker con el sobre común. Cada chat, autor o votante viaja como un peer; usa su peer_ref como clave y para responder.

Evento Qué es
message.received Un mensaje nuevo, entrante o el eco de uno propio escrito en el teléfono (from_me: true).
message.updated Un cambio sobre un mensaje: operacion estado (un recibo), edicion o ubicacion_actualizada.
message.revoked Alguien borró un mensaje para todos (operacion: revocacion).
message.reaction Una reacción; quitada, llega con reaccion: "".
message.undecryptable Llegó un mensaje que no se pudo descifrar. Nunca trae un texto inventado; si después llega legible, es un message.received con el mismo provider_id.
receipt Un recibo crudo de uno o más mensajes (tipo: entregado, leido, reproducido, leido_propio, reproducido_propio, remitente, reintento u otro). El estado del mensaje se mueve aparte, con message.updated.
message.expired Venció un mensaje temporal: el motor borró su contenido y sus medios.
chat.ephemeral Alguien fijó o quitó el temporizador de mensajes temporales del chat.
presence Un cambio de presencia de un contacto que sigues.
poll.vote, interactive.response Votos y respuestas: ver Interactivos.

contenido.tipo de un message.received es texto, imagen, video, audio, documento, sticker, ubicacion, ubicacion_en_vivo, contactos, encuesta o no_soportado. Un no_soportado trae el tipo del proveedor en contenido.no_soportado y nunca un texto inventado.

message.received · data (en un grupo, con cita y mención)
{
"chat": { "peer_ref": "9c1e0a77-0000-4000-8000-0000000000b2", "jid": "GRUPO@g.us", "clase": "grupo", "aliases": [] },
"autor": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": ["CONTACTO@s.whatsapp.net"] },
"provider_id": "PROVIDER_MESSAGE_ID",
"from_me": false,
"es_grupo": true,
"operacion": "mensaje",
"contenido": {
"tipo": "texto",
"texto": "@TU_NUMERO ¿ya salió el pedido?",
"menciones": ["TU_NUMERO@lid"],
"cita": { "provider_id": "PROVIDER_MESSAGE_ID_CITADO", "autor": "TU_NUMERO@lid", "de_mi": false }
},
"nombre_perfil": "Nombre del contacto",
"momento": "2026-10-24T15:08:12Z"
}

Para saber si a ti te mencionaron, compara contenido.menciones con los JID de tu instancia (su PN y su LID): no hay un campo «me mencionaron». La cita trae el provider_id del mensaje citado y, si WhatsApp lo informa, su autor; de_mi es true cuando ese autor es el PN o el LID de tu número (sin autor, false). Otras banderas posibles: efimero, ver_una_vez, pedido_al_telefono y estado_de_grupo.

message.updated · data (una edición)
{
"chat": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@s.whatsapp.net", "clase": "usuario", "aliases": [] },
"provider_id": "PROVIDER_MESSAGE_ID_DE_LA_EDICION",
"from_me": false,
"es_grupo": false,
"operacion": "edicion",
"objetivo": { "provider_id": "PROVIDER_MESSAGE_ID", "de_mi": false },
"contenido": { "tipo": "texto", "texto": "Mejor a las 5" },
"momento": "2026-10-24T15:14:10Z",
"version": 1
}

version cuenta las operaciones del mismo tipo sobre el mismo mensaje (1, 2…): la mayor es la vigente. Un borrado llega igual, como message.revoked con operacion: revocacion, su objetivo y sin contenido. En objetivo, de_mi: true dice que el mensaje afectado lo mandó tu instancia.

receipt · data
{
"chat": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": ["CONTACTO@s.whatsapp.net"] },
"from_me": false,
"es_grupo": false,
"provider_ids": ["PROVIDER_MESSAGE_ID"],
"tipo": "leido",
"momento": "2026-10-24T15:11:40Z",
"aplicados": 1,
"pendientes": 0
}

aplicados son los mensajes que el motor ya conocía; pendientes, los que todavía no (el recibo se aplica cuando lleguen). En un grupo viene además remitente, el participante que generó el recibo.

El estado de un mensaje solo sube: un recibo viejo nunca lo baja.

Estado Qué significa
pendiente Un envío del motor que todavía no salió.
recibido Un mensaje entrante.
enviado El servidor de WhatsApp aceptó el mensaje propio (o llegó su eco).
entregado Llegó al teléfono del destinatario.
leido Lo leyó (o, en uno recibido, lo marcaste leído).
reproducido Escuchó la nota de voz o vio el vídeo.
fallido El envío no salió.
presence · data
{
"de": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": [] },
"estado_chat": "escribiendo",
"momento": "2026-10-24T15:15:00Z",
"vence_en": "2026-10-24T15:25:00Z"
}

disponible y ultima_vez aparecen solo si WhatsApp los manda (y la última vez, solo si el contacto la comparte); chat aparece cuando lo que hace es en un chat distinto del suyo (un grupo). vence_en es el fin de tu suscripción.

tipo: disappearing fija el temporizador del chat (un 1:1 o un grupo) con segundos: 0 (desactivado), 86400 (24 horas), 604800 (7 días) o 7776000 (90 días). Desde ese momento, los mensajes nuevos del chat salen temporales. Cada cambio, del motor o del teléfono, llega como chat.ephemeral.

chat.ephemeral · data
{
"chat": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@s.whatsapp.net", "clase": "usuario", "aliases": [] },
"provider_id": "PROVIDER_MESSAGE_ID",
"from_me": true,
"es_grupo": false,
"segundos": 86400,
"momento": "2026-10-24T15:16:00Z"
}

Un mensaje temporal trae contenido.expiracion_seg. WhatsApp no avisa el vencimiento: el motor calcula vence_en (el momento más la expiración) y, al pasarlo, borra el contenido y los medios del mensaje y emite un message.expired por mensaje. En GET …/messages, el mensaje sigue, con contenido_disponible: false y motivo_no_disponible: vencido; una cita a él queda solo con su provider_id y citado_vencido: true.

message.expired · data
{
"chat": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@s.whatsapp.net", "clase": "usuario", "aliases": [] },
"provider_id": "PROVIDER_MESSAGE_ID",
"from_me": false,
"message_ref": "1f2e3d4c-0000-4000-8000-0000000000e1",
"momento": "2026-10-23T15:16:30Z",
"vence_en": "2026-10-24T15:16:30Z"
}

El temporizador por defecto de los chats nuevos es otra cosa: es un ajuste de privacidad (PUT /v1/instances/{id}/privacy/disappearing), en Perfil y privacidad.

Los errores siguen RFC 9457. Cuando hay un código propio, va al final de type (urn:motor-whatsapp:error:<código>); los de un comando que ya se aceptó llegan en error_code. La lista completa está en Errores.

HTTP / estado Código Cuándo
409 — La Idempotency-Key ya se usó con otro mensaje; o generacion_esperada no coincide; o la instancia no está vinculada, está bloqueada o reemplazada.
409 no_conectada Presencia con la instancia desconectada.
422 — Un solo destino válido; un campo que no va con el tipo; provider_ids y hasta juntos al marcar leído.
422 cita_ajena La cita no es un mensaje de este chat en esta instancia.
422 mencion_invalida Una mención no es un teléfono, un JID de usuario ni un peer_ref de la instancia.
422 objetivo_desconocido El objetivo no existe en este chat.
422 mensaje_ajeno Editar o borrar un mensaje que no mandó este número.
422 fuera_de_ventana Editar pasados 20 minutos o borrar pasados 2 días y 12 horas.
422 no_editable Editar algo que no es un texto o que ya se borró.
422 no_reenviable El motor no guardó el contenido de ese mensaje o su tipo no se reenvía.
422 vcard_invalida Una vCard propia mal formada.
422 url_no_permitida vista_previa.obtener con un host fuera de los destinos permitidos.
429 — Con el comando en el cuerpo: el ritmo lo dejó en cola (no repitas). Sin comando: la cola está llena.
fallido destino_invalido, rechazado_por_whatsapp, no_salio, generacion_cambiada, comando_vencido El envío no tuvo efecto (stage dice dónde).
fallido no_reproducible, sin_mensajes Un chat.played sobre algo que no es audio ni vídeo, o un chat sin mensajes que marcar.

Lo comprobado con números reales en las pruebas manuales (octubre de 2026):

  • Ventanas de edición y borrado: 20 minutos para editar y 2 días y 12 horas para borrar para todos; las dos se revisan otra vez al salir. Editar y borrar desde el motor se vio como «Editado» y «Se eliminó este mensaje» en iOS y Android (caso A24, 21/10/2026).
  • Recibos: si quien lee tiene apagadas las confirmaciones de lectura, nunca llega el leido, y el motor no lo inventa (casos A30 y A11; ver Otros límites observados). Un recibo que no llegó puede no llegar nunca.
  • Presencia: llegan escribiendo, grabando y pausado, pero no el «en línea» real ni la última vez de un contacto, y nada si la instancia no se puso antes «en línea» (caso A32, 21/10/2026; falla F22 cerrada). Ver Presencia.
  • Ubicación en tiempo real: de una compartida, el motor solo recibe el primer mensaje (ubicacion_en_vivo, con duracion_seg y termina_en); no llegan sus actualizaciones ni su fin (caso A23, 21/10/2026). Ver Ubicación en tiempo real.
  • Temporales: el temporizador por defecto cambiado en el teléfono no llega al motor (21/10/2026). El vencimiento real lo aplica el motor: en la prueba de la ronda R14 (22/10/2026), los mensajes vencidos quedaron sin contenido, con un message.expired cada uno, y sus medios se borraron del almacenamiento con el motivo mensaje_vencido.
  • Lo que el motor no reconoce llega como no_soportado, nunca como un texto inventado. Una edición hecha desde un iPhone llegaba así hasta que se corrigió (falla F20, repetida y cerrada el 20/10/2026). Ver Mensajes que el motor no reconoce.
  • Un ambiguo real: con la red del motor cortada en el instante del envío, el comando quedó ambiguo (etapa frontera) a los 75 s, sin ningún reenvío; WhatsApp no lo había recibido y nunca llegó (caso A15, 16/10/2026).

Correcciones de las pruebas que ya están en el contrato: la cita del motor se ve con su contenido y su autor (falla F6, 20/10/2026); los recibos de un 1:1 que manda el motor llegan a su fila aunque el otro conteste por su LID (falla F21, 20/10/2026); una mención acepta +número (ronda R8, 21/10/2026); y en una reacción a un mensaje propio, objetivo.de_mi es true (ronda R8, 21/10/2026).