Mandarle un mensaje a una persona desde tu sistema
Tu sistema escribe en una conversación que ya existe, por el mismo canal en que la persona habla con nosotros, sin que corra el agente. Sirve para una encuesta de satisfacción cuando el caso llega a su etapa de cierre, o para responderle desde tu mesa de soporte a alguien cuyo caso ya está escalado, sin copiar y pegar en la bandeja.
Base de la API de Lelemon: https://api.lelemon.cl
La idea de fondo: el mensaje sale con el texto que tú mandas, firmado con el nombre de tu credencial, y no pasa por el modelo. No gasta tokens, el agente no contesta por encima y la persona ve un mensaje de tu organización, no uno suyo.
Antes de empezar
Necesitas una credencial con el alcance conversations:send. Se emite en Integraciones API del dashboard, y al elegir ese alcance la pantalla te pide el nombre con el que firma. Ese nombre queda en cada mensaje que mande la credencial. Es lo que ve tu equipo en la bandeja y lo que llega en el aviso conversation.human_message, y en un widget embebido que lee el historial llega hasta la pantalla de la persona. Elige uno que diga qué es, como Encuesta de satisfacción o Mesa de soporte.
- El nombre lo fija la credencial, no el pedido. Un campo de autor en el cuerpo se rechaza con
400en vez de ignorarse, para que nadie crea que firmó con otro nombre. Así una integración no puede firmar con el nombre de una persona. - Una credencial con
conversations:sendsin nombre de autor no se puede emitir, y un nombre de autor sin ese alcance tampoco. - Rotar la credencial conserva el nombre.
El alcance va separado de agents:converse a propósito. Conversar escribe como si lo mandara la persona y hace correr al modelo; esto escribe como tu organización y no corre nada.
El pedido
Code
{id} es el id de la conversación, el mismo conversation.dealId que viaja en los avisos y el que devuelven las otras rutas.
| Campo | Tipo | Límite | Qué hace |
|---|---|---|---|
message | texto | 4.000 caracteres | El mensaje, tal cual sale. No puede venir vacío ni de solo espacios. |
externalRef | texto | 120 caracteres | Tu referencia para este envío. Es obligatoria: es lo que impide que un reintento mande el mensaje dos veces. Ver más abajo. |
Manda sólo estos campos. Un campo de autor (senderName, authorName, author, senderId o userId) se rechaza con 400 y un mensaje que explica que el autor lo fija la credencial. Cualquier otro campo de más se ignora.
Code
El mensaje queda guardado y encolado en el canal. Encolado no es entregado: el transporte lo despacha después, como con cualquier mensaje que escribe una persona de tu equipo.
Cuándo se puede mandar
La regla no es la etapa del caso sino quién lo está atendiendo. Si el agente está al mando de un caso abierto, tu mensaje llegaría por encima de lo que el agente está diciendo, y por eso se rechaza.
| Estado de la conversación | Resultado |
|---|---|
| El agente la atiende y el caso está abierto | 409, código agent_in_control |
| Escalada, esperando que alguien la tome | Sale |
| La tomó una persona del equipo | Sale |
| El caso está cerrado, aunque siga marcado como atendido por el agente | Sale |
El caso cerrado cubre la encuesta de cierre: un caso que llegó a su etapa final queda cerrado y nadie lo atiende, así que se le puede escribir.
Si te llega el 409, toma el control de la conversación desde la bandeja o cierra el caso, y vuelve a mandar con la misma externalRef: un rechazo no gasta la referencia.
Mandar un mensaje no cambia nada más. No mueve el caso de etapa, no cambia quién lo atiende y no reabre un caso cerrado.
La ventana de 24 horas de WhatsApp
Por WhatsApp, Meta sólo deja escribirle con texto libre dentro de las 24 horas siguientes al último mensaje de la persona. Pasado ese plazo sólo acepta plantillas aprobadas.
Esta ruta no envía plantillas. Si la ventana está cerrada, o si la persona nunca escribió, responde 422 con el código channel_window_closed y no manda nada:
Code
Tómalo como un error de verdad, no como un aviso: el mensaje no salió. Para una encuesta de cierre, mándala dentro de las 24 horas del último mensaje de la persona.
La ventana es una regla del canal de WhatsApp, no de la ruta. Las conversaciones que entraron por la API o por el copiloto in-app no tienen ventana.
La referencia: reintentar sin duplicar
externalRef identifica un envío tuyo en una conversación. Dos llamadas a la misma conversación con la misma referencia mandan un solo mensaje:
- La primera responde
alreadySent: falsey elmessageIddel mensaje guardado. - Las siguientes responden
200conalreadySent: truey el mismomessageId, y no escriben nada.
Funciona también cuando los reintentos llegan a la vez. Si tu flujo se cae después de mandar y vuelve a intentar, o dos ejecuciones corren en paralelo, la persona recibe un solo mensaje.
- La referencia vale por conversación: la misma referencia en otra conversación es otro envío.
- Un reintento responde igual aunque el caso haya cambiado de manos desde el primer envío: lo ya enviado no se vuelve a evaluar.
- Una referencia distinta es un mensaje distinto, aunque el texto sea el mismo.
- Arma la referencia con el hecho que origina el mensaje, como
encuesta-<id del ticket>, y no con un valor que cambie en cada intento, como la hora.
Errores
Llegan con el cuerpo habitual (statusCode, message, path). Los de esta ruta traen además code.
| Código | Qué pasó | Qué hacer |
|---|---|---|
400 | Falta message o externalRef, vienen vacíos, muy largos o no son texto, el cuerpo trae un campo de autor, o el {id} no es un UUID. | Arreglar el pedido. Reintentar igual no sirve. |
401 | Falta la credencial o no es válida. | Revisar la cabecera y la clave. |
403 | A la credencial le falta conversations:send, o la tiene pero no tiene nombre de autor. | Emitir una credencial con el alcance y su nombre. |
404 | La conversación no existe o es de otra organización. | Verificar el id. |
409 agent_in_control | El agente atiende un caso abierto. | Tomar el control o cerrar el caso, y reintentar con la misma externalRef. |
422 channel_window_closed | Pasaron más de 24 horas desde el último mensaje de la persona y sólo se puede con plantilla. | No reintentar igual: no va a cambiar solo. |
422 no_outbound_channel | La conversación no tiene por dónde escribirle a la persona. | Escribirle por otro medio. |
429 | Pasaste un techo por minuto: 3.000 pedidos por credencial o 200 por dirección IP, compartidos por toda la API externa. | Esperar y reintentar. Es seguro. Si mandas muchos mensajes seguidos, espácialos: la ruta no tiene envío en lote. |
500 | Error nuestro. | Reintentar con espera y la misma externalRef: es seguro. |
Ejemplo con curl
Code
Cómo ver lo que se mandó
GET /v1/external/conversations/{id}/messages, conconversations:read, devuelve el hilo. El mensaje sale con rolhuman.- Si tienes configurado el aviso
conversation.human_message, te llega también cuando manda tu propia credencial. El aviso no trae el texto: trae dónde encontrarlo y el nombre del autor.
Bajar los archivos de una conversación
Para traer a tu sistema lo que la persona mandó por el chat y lo que quedó en la ficha del caso:
Code
Pide conversations:read, el mismo alcance que leer la conversación. Acepta limit y offset, como las demás listas.
Code
| Campo | Qué es |
|---|---|
id | El id del archivo en la ficha, o el id del mensaje que lo trajo si vino por el chat. |
nombre | El nombre original del archivo. Una imagen, un audio o un video del chat no guardan el nombre que tenían en el teléfono: salen con el nombre con que se guardaron. |
mime | El tipo del archivo. |
tamano | Bytes. null en lo que vino por el chat, que no guarda el peso. |
fecha | Cuándo llegó al chat o se adjuntó a la ficha. |
origen | chat o ficha. |
subidoPor | cliente o equipo. Todo lo de chat es del cliente; en ficha, lo que entró por WhatsApp es del cliente y lo que cargó alguien del equipo en la ficha es del equipo. |
url | Dirección firmada para descargar, sin credencial. |
venceEn | Hasta cuándo sirve la url. |
Qué trae cada origen:
chat: las imágenes, los audios y los videos que mandó la persona. No trae los archivos que tu equipo manda por el chat.ficha: todo lo que está en la ficha del caso: los documentos que la persona mandó por el chat, que quedan guardados ahí con su nombre original, y los archivos que subió tu equipo. Un documento no sale dos veces. Para quedarte sólo con lo del cliente, filtra porsubidoPor: "cliente".
El más nuevo viene primero, mezclando los dos orígenes.
La url vive 10 minutos. Descárgala apenas pidas la lista; si venció, pide la lista de nuevo. Baja con su nombre en Content-Disposition. Sólo los pdf, las imágenes, el audio y el video se abren en línea; cualquier otro tipo, y en particular un html o un svg, baja siempre como archivo.
Una imagen que el filtro de seguridad bloqueó no se ofrece. Tampoco sale lo que se borró.
Un 404 significa que la conversación no existe o es de otra organización. El resto de los errores son los habituales de la API: 401, 403 si falta conversations:read, 429.
Lo que esta ruta no hace
- No envía plantillas de WhatsApp. Fuera de las 24 horas no hay forma de usarla.
- No escribe sobre un caso que el agente está atendiendo. No hay una opción para forzarlo.
- No manda archivos. Sólo texto.
- No elige el autor por pedido. Lo fija la credencial.
- No crea conversaciones. Para empezar una con un agente, ver
lelemon-api-agentes.md, y para que el agente escriba primero,lelemon-api-triggers.md.
Notas internas: avisarle al equipo sin escribirle a la persona
Una nota interna es un mensaje que queda en el chat de la conversación y que ve solo tu equipo, con el rótulo "Nota interna", su autor y su hora. Sirve para dejar constancia desde tu sistema, por ejemplo que el ticket no se pudo crear al escalar un caso.
Una nota nunca sale a la persona por ningún canal, no la lee el agente ni entra a sus resúmenes, y no viaja en los avisos hacia tu sistema. Tampoco mueve la espera ni la última actividad del caso, no reinicia el reloj del cierre por inactividad ni cuenta para el tope de mensajes de la conversación, y se puede dejar con el agente al mando, escalada o con el caso cerrado.
Necesitas una credencial con el alcance conversations:annotate, aparte de conversations:send: una integración que solo avisa de sus fallos no necesita poder escribirle a la persona. La nota se firma con el nombre de autor de la credencial si lo tiene y, si no, con el nombre de la credencial.
Code
| Campo | Tipo | Límite | Qué hace |
|---|---|---|---|
texto | texto | 4.000 caracteres | La nota. No puede venir vacía ni de solo espacios. |
externalRef | texto, opcional | 120 caracteres | Tu referencia para esta nota. Con la misma referencia en la misma conversación, un reintento no deja la nota dos veces. |
Un campo de autor (senderName, authorName, author, senderId o userId) se rechaza con 400: el autor lo fija la credencial.
Code
Un reintento con la misma externalRef responde 200 con alreadyCreated: true y el mismo messageId. Los errores son los de la ruta de enviar, salvo 409 y 422, que acá no existen. Un 404 es una conversación que no existe o es de otra organización, y un 403 es una credencial sin conversations:annotate.
Leer las notas
GET /v1/external/conversations/{id}/messages no las devuelve, para no cambiar el hilo a quien ya lo lee. Con ?incluirNotas=true las trae en su lugar, con role: "internal_note" y el nombre del autor en author:
Code
incluirNotas=true lo puede usar cualquier credencial con conversations:read: no hay un alcance aparte para leer notas. Dale ese alcance solo a sistemas que pueden ver lo que anota tu equipo.
Si muestras ese hilo a la persona, descarta los mensajes con rol internal_note.
Desde el dashboard
En el chat, el compositor tiene el modo "Nota interna". Lo puede usar quien puede responder conversaciones, sin tomar el control, y la nota queda firmada con su nombre.