Hablarle a un agente de Lelemon desde otra aplicación
Documento para el equipo que va a escribir la integración. Describe los tres endpoints con los que una aplicación externa le delega una conversación a un agente de Lelemon y alcanza para implementar el lado de ustedes sin preguntarnos nada.
Base de la API de Lelemon: https://api.lelemon.cl
Qué resuelve esto
Hasta ahora una app externa podía recibir avisos nuestros y contarnos que resolvió algo, pero no podía delegarle nada a un agente. Esto es lo contrario: ustedes abren una conversación contra un agente que ya existe en su organización, le mandan mensajes y reciben lo que el agente contesta. El caso típico es un ticket, un chat de ustedes o un formulario que quieren que atienda el agente antes de que lo toque una persona.
La conversación no vive en un lugar aparte. Entra por la etapa de entrada del flujo del agente, exactamente igual que una conversación de WhatsApp, y por eso aparece en la misma bandeja: no hay nada especial que hacer para lograrlo. Si alguien del equipo la toma a mano, el agente se calla; si el agente escala, sale el aviso conversation.escalated por el webhook de siempre.
Los tres endpoints van con la misma credencial de organización en la cabecera Authorization:
Code
Es la única forma de mandarla: no hay una cabecera alternativa. Tuvimos una y la sacamos, porque nuestro logger sabe ocultar el valor de authorization pero no el de una cabecera inventada por nosotros, y en producción eso habría dejado la credencial entera y reutilizable en el registro de cada pedido.
El prefijo es /v1 desde el primer día. Del otro lado va a haber una integración escrita por otra empresa, y romperla sin dejarle una versión vieja a la que volver no es una opción.
1. Abrir una conversación
Code
Donde {slug} es el identificador del agente en su organización, el mismo que se ve en la pantalla del agente.
El agente tiene que tener habilitado el canal api en su configuración. Es una casilla más, al lado de WhatsApp y del chat web, en la pestaña Resumen del agente, y la marcan ustedes. Si no está, el POST devuelve 422 diciendo qué canales atiende hoy. Lo pedimos por adelantado en vez de dejar que falle a mitad de camino: sin la casilla, el motor rechaza el turno recién después de haberlo empezado, y del otro lado eso se ve como un error temporal que nunca se arregla solo.
Alcance que exige: agents:converse.
El cuerpo
| Campo | Tipo | Límite | Obligatorio | Qué hace |
|---|---|---|---|---|
externalId | texto | 120 caracteres | sí | El identificador del caso del lado de ustedes. Es la llave de idempotencia; ver sección 4. |
contactName | texto | 255 caracteres | no | Quién es el contacto, para que en la bandeja no aparezca como un id. |
contactEmail | texto | 255 caracteres, formato de correo | no | |
contactPhone | texto | 50 caracteres | no |
Manden sólo estos campos. Cualquier otro se descarta antes de llegar a nuestro código, en silencio y sin error, así que no cuenten con un 400 para detectar un campo mal escrito. Eso incluye cualquier intento de nombrar una organización: la organización sale de la credencial y de ningún otro lado, así que un organizationId en el cuerpo no hace nada. Un externalId mal escrito sí lo van a notar, porque el campo es obligatorio y su ausencia devuelve 400.
Si no mandan contactName, el contacto queda con el externalId como nombre y no con un "Sin nombre". Es a propósito: en la bandeja, alguien que ve TCK-4471 puede ir a buscarlo del lado de ustedes.
La respuesta
201 Created:
Code
conversationId— el id que van a usar en los otros dos endpoints. Guárdenlo junto al caso de ustedes.agentSlug— el agente que quedó atendiendo. Siempre es el que pidieron en la URL: si no fuera a ser él quien contesta, el pedido habría fallado con422en vez de sustituirlo en silencio.created—truesi esta llamada abrió la conversación,falsesi ya existía una para eseexternalId. Ver sección 4.
Ejemplo con curl
Code
Códigos de estado
| Código | Qué pasó | Qué hacer |
|---|---|---|
201 | La conversación quedó abierta, o ya estaba. Mirar created. | Guardar el conversationId. |
400 | Falta externalId, viene vacío o se pasó de los 120 caracteres; contactEmail no tiene forma de correo; o el cuerpo trae un campo que no está en la lista. El campo errors dice cuál falló. | Arreglar el pedido. Reintentar tal cual no sirve. |
401 | Falta la credencial o no es válida. Ver sección 6. | Revisar la cabecera y la clave. |
403 | Tres causas posibles, distinguibles por el mensaje: a la credencial le falta agents:converse; la organización de la credencial fue eliminada; o la suscripción está cancelada. | Escribirnos. Ninguna se arregla reintentando. |
403 | El agente '<slug>' es interno: contesta al dueño de la organización y no se expone por API. | Usar un agente de cara al público. Ver abajo. |
404 | No existe el agente '<slug>'. | Revisar el slug. Es el mismo 404 exista el agente en otra organización o no exista en ninguna: decir "existe pero no es tuyo" le confirmaría el slug a quien está probando agentes ajenos. |
422 | El agente '<slug>' no está activo. | Activarlo desde la pantalla del agente. |
422 | El agente '<slug>' no tiene habilitado el canal 'api'... | Habilitar el canal api en la configuración del agente. El mensaje dice qué canales atiende hoy. |
422 | Ese agente no está conectado a ningún flujo... | Asignarlo a una etapa desde la pantalla del flujo. Sin flujo no hay dónde dejar la conversación. |
422 | El agente '<slug>' está en ese flujo pero no en su etapa de entrada... | Ver abajo. |
429 | Se pasaron del techo por credencial. Ver sección 5. | Esperar y reintentar. Es seguro. |
500 | Error nuestro. | Reintentar con espera. Si persiste, escríbannos: los 500 nos llegan como alerta. |
Por qué un agente interno da 403
Un agente interno es el del CRM sin pantalla: le contesta al dueño de la organización y trae herramientas que leen contactos con su teléfono, tratos y líneas de cartola con el RUT de la contraparte. En WhatsApp lo protege una lista blanca de teléfonos; acá no hay teléfono que valga, así que la puerta es la audiencia declarada del agente. Sólo se puede conversar por API con un agente marcado como externo.
El mensaje sí dice cuál es el problema, a diferencia del 404 de más arriba, porque la credencial pertenece a la misma organización dueña del agente: no le estamos revelando nada que no sea suyo.
Por qué la etapa de entrada da 422
Un agente no tiene flujo propio. El vínculo vive del otro lado: en la etapa que lo tiene asignado. Nosotros buscamos el flujo que tenga alguna etapa con ese agente y metemos la conversación por la etapa de entrada de ese flujo, que es el mismo recorrido que hace una conversación de WhatsApp.
Si el agente que nombraron está en el flujo pero no en su etapa de entrada, quien contestaría sería el agente de la etapa de entrada —con otro prompt y otras herramientas— mientras la respuesta del open sigue diciendo el slug que pidieron. Antes que sustituirlo en silencio, se rechaza y se explica. La salida es asignarlo a la etapa de entrada, o abrir la conversación con el agente que sí está ahí.
2. Mandar un mensaje
Code
Donde {id} es el conversationId que devolvió el paso anterior. Tiene que ser un UUID válido.
Alcance que exige: agents:converse.
El cuerpo
| Campo | Tipo | Límite | Obligatorio |
|---|---|---|---|
message | texto | 4.000 caracteres | sí |
externalRef | texto | 120 caracteres | no, pero mándenlo siempre |
externalRef es el id de este mensaje en el sistema de ustedes, el mismo campo que usa POST /conversations/{id}/send. Con él, reintentar es seguro: un segundo POST con el mismo externalRef en la misma conversación no guarda el mensaje otra vez ni hace contestar al agente de nuevo, y la respuesta trae "alreadySent": true. Qué devuelve ese reintento:
- Si el agente ya contestó:
200answered, con la misma respuesta. - Si el turno sigue corriendo:
202pending. - Si alguien del equipo tomó la conversación:
200human. - Si el turno había fallado (el
503): lo volvemos a intentar, una vez por fallo, y responde como un envío normal. - Si el
externalRefya se usó en esa conversación con otro texto:409. Usen uno nuevo para cada mensaje.
Sin externalRef no tenemos con qué darnos cuenta de que es un reintento: cada POST guarda el mensaje y hace contestar al agente.
El tope son 4.000 y no los 1.000 del chat web porque del otro lado no hay una persona tecleando en una landing: hay un sistema que reenvía el cuerpo de un ticket, y 1.000 lo partiría al medio. Son los mismos 4.000 que ya acepta el message del endpoint de resolución, un solo criterio para toda la superficie externa, y quedan holgadamente bajo el tope del motor: el que corta somos nosotros, con un 400 claro, y no el motor con un error suyo.
La respuesta
La llamada es sincrónica: se manda el mensaje y se espera a que el agente conteste. Hay tres desenlaces y el código de estado los distingue, para que del otro lado no haya que leer el cuerpo para saber si queda algo por hacer. El status viaja igual adentro, porque un cliente que sólo mira res.ok tiene que poder enterarse.
200 OK, el agente contestó:
Code
200 OK, la tomó una persona:
Code
No viene response y no es un error. Alguien del equipo tomó la conversación a mano, así que el agente se calla: el mensaje de ustedes sí quedó guardado y le aparece a esa persona en la bandeja. Contestar por encima le hablaría al contacto con dos voces. Ojo con la distinción: una conversación escalada —el agente pidió ayuda pero nadie la tomó todavía— no cae acá; ahí el agente sigue contestando.
202 Accepted, se venció el techo de espera:
Code
Esperamos hasta 25 segundos por la respuesta del agente. Un turno con herramientas puede tardar bastante, y una respuesta sin techo es una promesa que no se puede cumplir: del otro lado hay un cliente HTTP con su propio tiempo de espera, y cuando corta, nosotros seguimos gastando tokens para nadie. Los 25 segundos dejan margen bajo los 30 que traen por defecto casi todos los clientes, así que el que corta somos nosotros, con una respuesta que se entiende, y no la red con un socket caído.
Lo importante del 202: el turno no se cancela. Sigue corriendo y su respuesta se escribe igual en la conversación. Cortarlo dejaría al contacto sin respuesta después de haber pagado el modelo. Lo que hay que hacer entonces es ir a buscarla con el GET de la sección 3, esperando un poco y reintentando. No hay evento que les avise cuando llegó; ver "Lo que todavía no hacemos".
Ejemplo con curl
Code
Códigos de estado
| Código | Qué pasó | Qué hacer |
|---|---|---|
200 | El agente contestó (answered) o la conversación la tiene tomada una persona (human). | Leer status antes de leer response. |
202 | Se vencieron los 25 segundos. El mensaje quedó guardado y la respuesta se va a escribir. | Ir a buscarla con el GET, o reintentar el POST con el mismo externalRef. Sin externalRef, no reintentar: mandaría el mismo mensaje dos veces. |
400 | Falta message, viene vacío, pasa los 4.000 caracteres, el cuerpo trae un campo de más, o el {id} de la ruta no es un UUID válido. | Arreglar el pedido. |
401 | Falta la credencial o no es válida. | Ver sección 6. |
403 | A la credencial le falta agents:converse, la organización fue eliminada, o la suscripción está cancelada. | Escribirnos. |
403 | Esta conversación no se abrió por la API: sólo se puede conversar en las que abriste tú. | Ver abajo. |
404 | No existe la conversación <id>. La conversación no existe, está borrada o es de otra organización. | Revisar que sea el conversationId que devolvió el open. No reintentar en loop. |
409 | Esta conversación llegó a 200 mensajes. Abre una nueva con otro externalId. | Ver abajo. |
409 | Ese externalRef ya se usó en esta conversación con otro mensaje. Usa uno nuevo para cada mensaje. | Generar un externalRef distinto por mensaje. |
422 | La conversación está en una etapa sin agente: no hay quién conteste. La etapa donde quedó parada la conversación no tiene ningún agente asignado. | Revisar el flujo desde la pantalla. Reintentar no cambia nada hasta que se asigne un agente a esa etapa. |
422 | El agente '<slug>' no tiene habilitado el canal 'api'... Le sacaron el canal después de que la conversación ya existía, o la conversación se movió a una etapa cuyo agente no lo tiene. | Habilitar el canal api en ese agente. Reintentar antes de eso no sirve. |
429 | Se pasaron del techo por credencial. | Esperar y reintentar. |
503 | El turno falló antes de los 25 segundos. El mensaje ya quedó guardado. Con externalRef: El agente no pudo responder. Reintenta con el mismo externalRef: lo volvemos a intentar sin guardar el mensaje dos veces. Sin él: El agente no pudo responder y el mensaje ya quedó registrado: no lo mandes de nuevo, porque quedaría dos veces. Lee el hilo con GET /conversations/{id}/messages. | Con externalRef, reintentar con el mismo. Sin él, no reintentar: esperar y mirar el hilo con el GET. |
500 | Error nuestro. | Reintentar con espera. |
Por qué una conversación ajena da 403
Una credencial con agents:converse no puede escribir dentro de una conversación que entró por otro canal. El mensaje se persiste con rol user, o sea "esto lo dijo el contacto": en un hilo de WhatsApp eso sería ponerle palabras en la boca a una persona. Y el id de conversación no es un secreto —lo mandamos nosotros en cada aviso saliente de escalamiento—, así que sin esta puerta bastaría con haber recibido un webhook para escribir en el WhatsApp de cualquier contacto de la organización. Sólo se puede conversar en las conversaciones que abrió la propia API.
Por qué hay un tope de 200 mensajes
Cada mensaje reenvía el historial al modelo, así que una conversación que no termina nunca se va poniendo más cara con cada turno. Al llegar a 200 se corta con el 409 y la salida es abrir una conversación nueva con otro externalId, que además es lo correcto: 200 mensajes ya no son el mismo caso.
El tope se mira antes de escribir. Pasado el límite, el mensaje ni siquiera se guarda. Guardarlo y no contestarlo sería peor: el contacto quedaría con un mensaje colgado que nadie va a responder.
3. Leer el hilo
Code
Alcance que exige: conversations:read, no agents:converse. Leer y hacer gastar tokens son dos permisos distintos, así que una integración que sólo consulta el hilo no necesita poder correr al modelo.
Es la contraparte del 202: sin esto, una respuesta que se pasó del techo quedaría escrita y sin forma de leerla desde afuera.
La respuesta
200 OK:
Code
role—useres lo que mandaron ustedes por la API,assistantes el agente IA yhumanes alguien del equipo de Lelemon que tomó la conversación. Van separados a propósito: del otro lado importa saber si contestó el agente o una persona.content— el texto del mensaje.createdAt— cuándo se escribió, en ISO 8601 UTC.
Los mensajes vienen ordenados del más viejo al más nuevo, hasta 200 —el mismo tope de la conversación, así que en la práctica salen todos—. Los mensajes de rol system quedan afuera: son notas internas nuestras, como resoluciones o evidencia de escalamientos, y no conversación con el contacto.
Ejemplo con curl
Code
Códigos de estado
| Código | Qué pasó | Qué hacer |
|---|---|---|
200 | El hilo. | — |
400 | El {id} de la ruta no es un UUID válido. | Arreglar el pedido. |
401 | Falta la credencial o no es válida. | Ver sección 6. |
403 | A la credencial le falta conversations:read, la organización fue eliminada, o la suscripción está cancelada. | Escribirnos. |
404 | La conversación no existe, está borrada o es de otra organización. | Revisar el id. |
429 | Se pasaron de un techo por minuto (200 por IP o 3.000 por credencial). Ojo con esto si están sondeando un pending. | Esperar lo que diga Retry-After. |
500 | Error nuestro. | Reintentar con espera. |
Leer no mira el origen
A diferencia de escribir, leer no mira por dónde entró la conversación: con conversations:read se lee cualquier conversación de la organización, también las que llegaron por WhatsApp, con lo que escribió el equipo humano. Traten una credencial con ese alcance como acceso a todas las conversaciones, y no la compartan con un sistema que sólo necesita las suyas.
4. Idempotencia: el externalId
El externalId es obligatorio y es la llave de idempotencia. Sin él, el reintento de una llamada que ya escribió abriría una segunda conversación para el mismo caso, y del lado de ustedes quedarían dos hilos donde había uno.
Lo que garantiza: dentro de una organización, un externalId tiene a lo más una conversación abierta con ese agente. Usen la llave del caso del lado de ustedes, que ya es única ahí: el número del ticket, el id del chat, lo que corresponda.
Cómo se resuelve, en dos redes:
- Antes de escribir nada, buscamos si ya hay una conversación viva para ese
externalIden el flujo de ese agente. Si la hay, la devolvemos concreated: falsey no tocamos nada más. - Aun así, dos reintentos pueden entrar a la vez y las escrituras no están en una transacción. Por eso las dos inserciones se apoyan en índices únicos que ya existen en la base, y el reintento que pierde la carrera resuelve a la fila del que ganó en vez de reventar.
Por eso reintentar el POST de apertura es seguro: el peor caso es que reciban un 201 con created: false y el mismo conversationId que ya tenían.
created es justamente lo que un reintento necesita saber para no duplicar su propio trabajo. Si viene false y ustedes iban a mandar el primer mensaje de bienvenida, ya lo mandaron antes.
Un detalle sobre los datos del contacto: en una llamada repetida, contactName, contactEmail y contactPhone se refrescan si vienen, pero nunca se pisan con nulos. Un segundo pedido más pobre que el primero no le borra el nombre a un contacto que ya lo tenía.
Y una aclaración que conviene tener a mano: el externalId vive en un espacio de nombres fijo, no atado a la credencial. Rotar la clave —que es algo que se espera que pase— no deja huérfanas las conversaciones abiertas.
El open es idempotente por externalId; el messages, por externalRef. Sin externalRef, mandar el mensaje dos veces lo manda dos veces: si reciben un 202 o un 503, el mensaje ya quedó escrito y la salida es el GET, no repetir el POST.
5. El techo de pedidos
Hay dos techos por minuto, en ventana deslizante:
- 200 pedidos desde una misma dirección IP. Es el que topa primero una integración, porque todo su tráfico sale de la misma IP. Pasado,
429con el cuerpoDemasiadas solicitudes, intenta de nuevo en un momento. - 3.000 pedidos por credencial. Pasado,
429con el cuerpoDemasiadas solicitudes con esta credencial, intenta de nuevo en un momento.
Los dos traen Retry-After con los segundos que faltan.
Dos cosas prácticas:
- El rechazo no cuenta como pedido. Si contara, la ventana se correría sola con cada reintento y quien insiste quedaría castigado para siempre.
- Los tres endpoints comparten el mismo balde. Si van a sondear el
GETesperando una respuesta que quedópending, espacien los reintentos: un sondeo cada segundo se come el cupo del minuto entero y deja sin lugar a los mensajes.
El de la IP es el límite general de toda la API, no sólo de esta superficie.
6. La credencial
Formato: lel_<12 caracteres hexadecimales>_<64 caracteres hexadecimales>. El bloque del medio es un prefijo público con el que ubicamos la fila; el resto es el secreto.
Para entrar tienen que darse cuatro cosas, en este orden: la clave existe y no está revocada, el secreto coincide, la organización sigue viva y no cancelada, y la credencial tiene el alcance que el endpoint exige.
Los 401 que pueden ver:
Falta la credencial. Mandala en el header Authorization: Bearer <clave>.— no mandaron ninguna, o no viene con el prefijoBearer.Credencial invalida.— la clave no existe, está revocada, o no tiene la forma esperada. Es el mismo mensaje para los tres casos a propósito: distinguirlos le diría a quien está probando si acertó la forma.
Los 403 que no son de alcance:
La organizacion de esta credencial fue eliminada.La suscripcion de esta organizacion esta cancelada: la API queda cerrada hasta reactivarla.
Los dos aparecen recién después de haber probado que se tiene la clave. Antes de eso, contar que una organización está borrada le regalaría información a quien sólo está probando prefijos al azar. Un atraso en el pago no cierra la API: sólo la cancelación.
Alcances de esta superficie:
agents:converse— abrir una conversación con un agente y mandarle mensajes.conversations:read— leer el hilo de una conversación.
agents:converse va separado de conversations:resolve porque son dos poderes distintos: resolver es cerrar algo que nosotros escalamos, mientras que conversar hace correr al modelo, gasta tokens y deja al agente ejecutando herramientas. Una integración que sólo cierra tickets no tiene por qué poder hacer eso. Un endpoint que no declare explícitamente su alcance se rechaza, así que la clave no se abre sola a superficie nueva.
Lo demás de la credencial —que se entrega una sola vez, que de nuestro lado sólo vive su hash, que se revoca pero no se recupera— vale igual acá.
No la manden por un canal que quede registrado en claro: nada de correo, chat corporativo, ticket ni comentario de un issue. Si sospechan que quedó expuesta, avísennos y la revocamos.
7. El cuerpo de los errores
Todos los errores llegan con esta forma:
Code
Códigos de error
El campo code es lo que su integración debe leer para decidir qué hacer. Es un texto en inglés, en minúsculas, y no cambia: el message puede reescribirse, el code no. Un código nuevo puede aparecer más adelante, así que su integración debe tratar uno que no conoce según su statusCode.
code | Status | Cuándo pasa | Qué hacer |
|---|---|---|---|
validation_failed | 400 | El cuerpo no cumple lo que pide la ruta. | Corregir el pedido. No reintentar igual. |
api_key_invalid | 401 | Falta la credencial, está mal escrita, fue revocada o venció. No distinguimos entre esos casos a propósito. | Revisar la clave; si estaba bien, pedir una nueva. |
scope_missing | 403 | La credencial no tiene el permiso que pide la ruta. | Pedir que le agreguen ese permiso. |
organization_deleted | 403 | La organización de la credencial fue eliminada. | Escribirnos. |
subscription_canceled | 403 | La suscripción está cancelada y la API queda cerrada. | Reactivar la suscripción. |
agent_not_exposed | 403 | El agente es interno y no se habla con él por API. | Usar otro agente. |
conversation_not_from_api | 403 | La conversación no se abrió por la API. | Abrir una con open. |
agent_without_api_channel | 422 | El agente no tiene el canal api activo. | Activar el canal en el agente. |
conversation_limit_reached | 409 | La conversación llegó al tope de mensajes. | Abrir otra con otro externalId. |
external_ref_conflict | 409 | Ese externalRef ya se usó con otro mensaje. | Usar un externalRef nuevo por mensaje. |
agent_turn_failed | 503 | El agente no pudo responder. | Reintentar con el mismo externalRef; sin él, mirar el hilo antes. |
action_origin_not_allowed | 403 | La credencial no puede crear acciones hacia ese sitio. | Que un administrador agregue el sitio a la credencial. |
action_destination_blocked | 400 | La dirección de la acción no es pública o no es http o https. | Usar una URL pública. |
rate_limited | 429 | Pasaron el límite de pedidos. | Esperar lo que diga Retry-After. |
Si el error no tiene un código propio, code toma uno según el statusCode:
| Status | code |
|---|---|
| 400 | invalid_request |
| 401 | unauthorized |
| 403 | forbidden |
| 404 | not_found |
| 409 | conflict |
| 413 | payload_too_large |
| 422 | unprocessable |
| 429 | rate_limited |
| 500 | internal_error |
| 503 | service_unavailable |
Los 400 de validación traen code: "validation_failed", el message genérico Validation failed y un campo errors con un elemento por campo que falló: field (el nombre), constraints (qué regla no cumplió) y received (la forma de lo que llegó, por ejemplo string(130), nunca el valor). Los demás errores de este documento traen el texto que aparece en las tablas.
8. Si algo falla
Todos los mensajes vuelven 202. El agente se está pasando de los 25 segundos, muy probablemente porque el turno usa herramientas. Las respuestas están quedando escritas igual: confírmenlo con el GET. Si el hilo tiene las respuestas y sólo llegan tarde, no hay nada roto del lado de ustedes; escríbannos y miramos qué está demorando el turno.
Un 409 a la primera. Están reusando un externalId de un caso viejo que llegó a 200 mensajes. Abran uno nuevo con otro externalId.
Un 403 diciendo que la conversación no se abrió por la API. Están usando un conversationId que salió de un aviso de escalamiento de WhatsApp, no del open. Esta superficie sólo alcanza las conversaciones que abrió la propia API.
Un 422 de etapa sin agente en una conversación que venía funcionando. La conversación se movió de etapa —lo más común es un escalamiento del agente— y quedó parada en una etapa que no tiene ningún agente asignado. No es algo que se arregle desde afuera.
Un 404 con un id que sí devolvió el open. Revisen que estén mandando el conversationId y no el externalId de ustedes. Si es el correcto, escríbannos.
Un 401 de un día para el otro. La clave se revocó o se rotó. Escríbannos y emitimos una nueva.
Un 503. El turno del agente falló y el mensaje ya quedó registrado. Si lo mandaron con externalRef, reintenten con el mismo y lo volvemos a intentar. Si no, no lo reenvíen: esperen unos minutos y miren el hilo.
9. Lo que todavía no hacemos
Cuatro límites conocidos, para que no los descubran el día que los necesiten.
Los dos techos cuentan en memoria de cada instancia del servidor. Con varias instancias corriendo, el techo efectivo se multiplica por la cantidad de instancias. No diseñen contra los números exactos como si fueran una garantía en ninguna de las dos direcciones.
No hay ningún aviso saliente de "el agente contestó". El único evento que sale por webhook hoy es conversation.escalated. Cuando un mensaje devuelve 202, la única forma de enterarse de la respuesta es sondear el GET del hilo. Si sondean, espacien: el techo de 200 por minuto por IP es el mismo para toda la API.
No se puede cerrar una conversación desde afuera. Los únicos endpoints externos que existen son estos tres, más los del acuerdo de escalamiento —avisar que un escalamiento se resolvió y consultar el estado de una conversación—. No hay forma de decirnos "este caso terminó, archívalo". Lo que sí pueden hacer es dejar de mandarle mensajes y abrir una conversación nueva con otro externalId cuando arranque el caso siguiente.
No hay adjuntos. El único contenido que acepta el POST de mensaje es texto en el campo message. No hay forma de mandar un archivo, una imagen ni un audio por esta vía.