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.
Requisitos
Sección titulada «Requisitos»| 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.
Enviar un mensaje
Sección titulada «Enviar un mensaje»Cada envío lleva:
destino: exactamente uno detelefono(número internacional, con+),jid(un usuario@s.whatsapp.neto@lid, o un grupo@g.us) opeer_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:fallidoconcomando_vencido),correlation_id(hasta 200 caracteres; vuelve en los eventos del comando) ygeneracion_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)}Texto, cita y menciones
Sección titulada «Texto, cita y menciones»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.
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"] }'c := motor.Nuevo("https://motor.ejemplo.com", apiKey)cmd, err := c.EnviarTexto(ctx, instanciaID, "pedido-1042-respuesta", motor.Texto{ Destino: motor.Destino{JID: "GRUPO@g.us"}, Texto: "@NUMERO_DEL_CONTACTO tu pedido 1042 sale hoy.", Cita: &motor.Cita{ProviderID: "PROVIDER_MESSAGE_ID", Autor: "CONTACTO@s.whatsapp.net"}, Menciones: []string{"+NUMERO_DEL_CONTACTO"},})if err != nil { return err}fmt.Println(cmd.ID, cmd.Estado) // … en_colaconst res = await fetch(`https://motor.ejemplo.com/v1/instances/${instanciaId}/messages`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'pedido-1042-respuesta', }, body: JSON.stringify({ 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'], }),});const comando = await res.json(); // 202: { command_id, status: "en_cola", … }{ "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.
Ubicación y contactos
Sección titulada «Ubicación y contactos»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).
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"} }'res, err := llamar(ctx, c, http.MethodPost, "/v1/instances/"+instanciaID+"/messages", "pedido-1042-ubicacion", map[string]any{ "destino": map[string]string{"telefono": "+NUMERO_DEL_CONTACTO"}, "tipo": "location", "ubicacion": map[string]any{ "latitud": -12.0464, "longitud": -77.0428, "nombre": "Tienda Centro", "direccion": "Av. Principal 123", }, })await fetch(`https://motor.ejemplo.com/v1/instances/${instanciaId}/messages`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'pedido-1042-ubicacion', }, body: JSON.stringify({ destino: { telefono: '+NUMERO_DEL_CONTACTO' }, tipo: 'location', ubicacion: { latitud: -12.0464, longitud: -77.0428, nombre: 'Tienda Centro', direccion: 'Av. Principal 123' }, }),});{ "destino": {"telefono": "+NUMERO_DEL_CONTACTO"}, "tipo": "contacts", "contactos": [{"nombre": "Soporte", "telefonos": ["+NUMERO_DEL_CONTACTO"]}]}Reaccionar, editar, borrar y reenviar
Sección titulada «Reaccionar, editar, borrar y reenviar»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, aldestino.
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.
# Reaccionarcurl -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 propiocurl -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"}'destino := map[string]string{"peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1"}objetivo := map[string]string{"provider_id": "PROVIDER_MESSAGE_ID"}ruta := "/v1/instances/" + instanciaID + "/messages"
// Reaccionar_, err := llamar(ctx, c, http.MethodPost, ruta, "reaccion-1042", map[string]any{"destino": destino, "tipo": "reaction", "objetivo": objetivo, "emoji": "👍"})
// Editar un texto propio_, err = llamar(ctx, c, http.MethodPost, ruta, "edicion-1042", map[string]any{"destino": destino, "tipo": "edit", "objetivo": objetivo, "texto": "Texto corregido"})const enviar = (clave, cuerpo) => fetch(`https://motor.ejemplo.com/v1/instances/${instanciaId}/messages`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Idempotency-Key': clave, }, body: JSON.stringify(cuerpo), });
const destino = { peer_ref: '4b7d9e01-0000-4000-8000-0000000000b1' };const objetivo = { provider_id: 'PROVIDER_MESSAGE_ID' };await enviar('reaccion-1042', { destino, tipo: 'reaction', objetivo, emoji: '👍' });await enviar('edicion-1042', { destino, tipo: 'edit', objetivo, texto: 'Texto corregido' });{"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"}}Marcar como leído o reproducido
Sección titulada «Marcar como leído o reproducido»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.
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"}'res, err := llamar(ctx, c, http.MethodPost, "/v1/instances/"+instanciaID+"/read", "leido-chat-1042", map[string]any{ "destino": map[string]string{"peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1"}, "hasta": "2026-10-24T15:12:00Z", }) // 202 con el comando chat.readawait fetch(`https://motor.ejemplo.com/v1/instances/${instanciaId}/read`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'leido-chat-1042', }, body: JSON.stringify({ destino: { peer_ref: '4b7d9e01-0000-4000-8000-0000000000b1' }, hasta: '2026-10-24T15:12:00Z', }),}); // 202 con el comando chat.readPresencia: en línea y «escribiendo»
Sección titulada «Presencia: en línea y «escribiendo»»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}/presencecon{"disponible": true}pone la instancia «en línea» (false, fuera de línea). Responde 204.POST /v1/instances/{id}/chat-presencecondestinoyestado(escribiendo,grabandoopausado) muestra «escribiendo…» o «grabando audio…» en ese chat. Responde 204.POST /v1/instances/{id}/presence-subscriptionscondestinoyttl_seg(60 a 86 400; por omisión, 3600) sigue la presencia de un contacto: mientras está vigente, cada cambio llega como eventopresence. Pasado el TTL caduca sola; repetirla la renueva. Responde 201.
# 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 chatcurl -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 minutoscurl -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}'base := "/v1/instances/" + instanciaIDdestino := map[string]string{"telefono": "+NUMERO_DEL_CONTACTO"}
_, err := llamar(ctx, c, http.MethodPost, base+"/presence", "", map[string]bool{"disponible": true})_, err = llamar(ctx, c, http.MethodPost, base+"/chat-presence", "", map[string]any{"destino": destino, "estado": "escribiendo"})_, err = llamar(ctx, c, http.MethodPost, base+"/presence-subscriptions", "", map[string]any{"destino": destino, "ttl_seg": 600})const base = `https://motor.ejemplo.com/v1/instances/${instanciaId}`;const post = (ruta, cuerpo) => fetch(base + ruta, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify(cuerpo), });
const destino = { telefono: '+NUMERO_DEL_CONTACTO' };await post('/presence', { disponible: true }); // 204await post('/chat-presence', { destino, estado: 'escribiendo' }); // 204await post('/presence-subscriptions', { destino, ttl_seg: 600 }); // 201Consultar mensajes y recibos
Sección titulada «Consultar mensajes y recibos»GET /v1/instances/{id}/messageslista lo que el motor capturó (en vivo, importado del historial o enviado), del más nuevo al más viejo, connext_cursoryhas_more. Filtros:peer_ref,direccion(entrante,saliente),origen(live,history,operator),operacion(mensaje,edicion,revocacion,reaccion,indescifrable),desde,hastaylimit. 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}/receiptstraeentregado,leidoyreproducidode cada destinatario de un mensaje propio (en un grupo, de cada participante).
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"res, err := llamar(ctx, c, http.MethodGet, "/v1/instances/"+instanciaID+"/messages/"+url.PathEscape(providerID)+"/receipts", "", nil)if err != nil { return err}defer res.Body.Close()var out struct { Recibos []struct { Destinatario motor.Peer `json:"destinatario"` Tipo string `json:"tipo"` Momento time.Time `json:"momento"` } `json:"recibos"`}err = json.NewDecoder(res.Body).Decode(&out)const res = await fetch( `https://motor.ejemplo.com/v1/instances/${instanciaId}/messages/${providerId}/receipts`, { headers: { Authorization: `Bearer ${apiKey}` } },);const { recibos } = await res.json(); // [{ destinatario, tipo, momento }]{ "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.
El comando y sus eventos
Sección titulada «El comando y sus eventos»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. |
{ "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_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"}Recibir mensajes
Sección titulada «Recibir mensajes»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.
{ "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.
{ "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.
{ "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.
Estados de un mensaje
Sección titulada «Estados de un mensaje»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ó. |
Presencia
Sección titulada «Presencia»{ "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.
Mensajes temporales
Sección titulada «Mensajes temporales»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": { "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.
{ "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.
Errores comunes
Sección titulada «Errores comunes»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. |
Límites de WhatsApp
Sección titulada «Límites de WhatsApp»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,grabandoypausado, 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, conduracion_segytermina_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.expiredcada uno, y sus medios se borraron del almacenamiento con el motivomensaje_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
ambiguoreal: con la red del motor cortada en el instante del envío, el comando quedóambiguo(etapafrontera) 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).