Lelemon API
  • Guías
  • Referencia de la API
Crear un agenteConversar con un agenteConocimiento del agenteAcciones HTTPEscalamiento a un humanoMandar un mensaje a una conversaciónQue el agente escriba primeroAgendar en tu propia pantallaInventariar tus agentes para una auditoriaCatalogos y respuestas rapidas de un embudoDisponibilidad de una persona del equipoMetricas de las conversaciones
powered by Zudoku
Guías

Recibir avisos de Lelemon y resolver escalamientos desde otra aplicación

Documento para el equipo que va a escribir la integración. Describe las dos mitades —los avisos que les mandamos por webhook y cómo nos cuentan que resolvieron un escalamiento— y alcanza para implementar el lado de ustedes sin preguntarnos nada.

Base de la API de Lelemon: https://api.lelemon.cl

Qué resuelve esto

Cuando el agente de Lelemon está atendiendo a una persona por WhatsApp y esa conversación necesita a un humano —porque lo pidió, porque el caso es complejo o porque es un reclamo—, Lelemon deja de responder y le pega a un webhook de ustedes con el contacto, el motivo y el historial de la conversación. De ahí en adelante manda n8n: ustedes deciden el request type, el issue type y la tipificación, y arman el ticket de Jira. Lelemon no sabe nada de Jira ni quiere saber; sólo emite el aviso. Cuando el ticket se cierra, n8n le pega a un endpoint de Lelemon con una credencial, la resolución queda escrita en la conversación y el control vuelve al agente para que siga atendiendo.


1. Recibir el aviso de escalamiento

La URL la eligen ustedes y nosotros la damos de alta del lado de Lelemon, junto con el secreto de firma. Condiciones del destino:

  • Tiene que ser http o https. Recomendamos https.
  • Tiene que resolver a una dirección pública. Validamos el dominio antes de cada salida y rechazamos loopback, redes privadas, link-local y sufijos como .internal o .local.
  • Hay un destino por evento y por organización. El evento de este acuerdo es conversation.escalated.

Detalles de la salida:

  • Método POST, Content-Type: application/json.
  • Esperamos hasta 10 segundos por la respuesta. Si no contestan en ese plazo, para nosotros es un fallo.
  • No seguimos redirecciones. Un 301 o 302 cuenta como fallo, no como redirección a seguir. La URL que nos den tiene que responder directo.
  • Tomamos como éxito cualquier código entre 200 y 299. Todo lo demás es fallo y dispara reintento.

Cabeceras que mandamos

CabeceraValor
Content-Typeapplication/json
User-AgentLelemon-Webhooks/1.0
X-Lelemon-Eventconversation.escalated
X-Lelemon-Timestampmarca de tiempo Unix en segundos, como texto
X-Lelemon-SignatureHMAC-SHA256 en hexadecimal (ver sección 2)

El JSON que les llega

Ejemplo completo y realista de un contacto que pide hablar con una persona. Los datos son inventados a propósito.

Code
{ "eventId": "2f6c1c4e-9b3a-4a5e-8f21-7d0c5a1b9e44", "event": "conversation.escalated", "occurredAt": "2026-08-19T14:32:07.418Z", "conversation": { "dealId": "b81d3f77-2c4a-4f19-9a86-31ee6c0b7d52", "pipelineId": "5a0e2c31-8d47-4b62-b1c9-0f4a7e93d118", "channel": "whatsapp" }, "contact": { "id": "c4a9e610-7f38-4d21-90bb-2e5c8a417d63", "name": "Marcela Fuentes", "phone": "+56912345678", "email": "[email protected]" }, "escalation": { "reason": "user_request", "priority": "medium", "summary": "La contadora pregunta por un trámite que lleva cuatro días sin movimiento y pide hablar con una persona del equipo." }, "history": [ { "role": "user", "content": "Hola, subí los documentos el viernes y todavía no me aparece nada.", "at": "2026-08-19T14:28:55.102Z" }, { "role": "assistant", "content": "Hola Marcela. Veo la solicitud ingresada el viernes y sigue en revisión. ¿Quieres que te avise apenas cambie de estado?", "at": "2026-08-19T14:29:31.774Z" }, { "role": "user", "content": "No, prefiero que alguien lo revise ahora. ¿Me pueden comunicar con una persona?", "at": "2026-08-19T14:31:48.960Z" } ] }

Qué significa cada campo

Nivel raíz

  • eventId — UUID único de este aviso. Es la llave de idempotencia; ver sección 3.
  • event — siempre conversation.escalated para este acuerdo. Viene también en la cabecera X-Lelemon-Event.
  • occurredAt — cuándo armamos el aviso, en ISO 8601 UTC. No confundir con X-Lelemon-Timestamp, que va en segundos y es el que entra en la firma.

conversation

  • dealId — identificador de la conversación en Lelemon. Este es el id que después usan en la ruta para avisarnos que se resolvió. Guárdenlo en el ticket.
  • pipelineId — el flujo al que pertenece la conversación.
  • channel — por dónde venía conversando, por ejemplo whatsapp. Sale del contexto del agente y puede venir null.

contact

  • id — identificador del contacto en Lelemon.
  • name — nombre del contacto tal como lo tenemos guardado.
  • phone — teléfono del contacto. Si el contacto no lo tiene guardado, usamos el teléfono del contexto de la conversación. Puede venir null.
  • email — puede venir null.

escalation

  • reason — motivo que declaró el agente al escalar. Los valores que puede elegir son user_request, complex_issue, complaint, sales_opportunity y technical_support. Si el agente no manda ninguno, queda user_request. Puede venir null si el escalamiento se originó fuera de la herramienta del agente.
  • priority — low, medium, high o urgent. Si no viene, mandamos medium.
  • summary — resumen que escribió el agente para el humano que va a tomar el caso. Puede venir null.

history

Los últimos 20 mensajes de la conversación, ordenados del más viejo al más nuevo. Si la conversación tuvo menos, llegan todos.

  • role — user es el contacto, assistant es el agente IA, human es alguien del equipo que había tomado la conversación, system son eventos internos.
  • content — el texto del mensaje.
  • at — cuándo se escribió, en ISO 8601 UTC.

Una aclaración de diseño que les conviene saber: mandamos el historial y no una clasificación nuestra a propósito. Quien tipifica es el flujo de ustedes, y tipificar sobre un resumen nuestro sería tipificar sobre nuestra interpretación.


2. Verificar que el aviso es nuestro

Cada envío va firmado con el secreto que compartimos al dar de alta el webhook. El algoritmo es este, sin variantes:

  1. Tomen el valor de X-Lelemon-Timestamp (texto, segundos Unix).
  2. Tomen el cuerpo crudo de la petición, exactamente los bytes que llegaron.
  3. Armen la cadena <timestamp>.<cuerpo> — el timestamp, un punto, y el cuerpo.
  4. Calculen HMAC-SHA256 de esa cadena con el secreto compartido.
  5. Codifiquen en hexadecimal minúsculas y comparen contra X-Lelemon-Signature.

Por qué la marca de tiempo entra en lo firmado y no viaja sólo como cabecera informativa: si firmáramos únicamente el cuerpo, cualquiera que haya visto un aviso podría reenviarlo mañana tal cual y la firma seguiría dando válida. Al meter el timestamp adentro del HMAC, el aviso queda atado al momento en que salió, y un reenvío viejo se detecta comparando ese timestamp contra la hora actual. Recomendación nuestra: rechacen todo aviso cuyo X-Lelemon-Timestamp esté a más de 5 minutos de la hora de ustedes. Lelemon no impone esa ventana; la validación de frescura corre por cuenta del receptor.

Y una advertencia que es la causa número uno de firmas que no calzan: firmen sobre el cuerpo crudo, nunca sobre el JSON re-serializado. Si el nodo de n8n ya parseó el JSON y ustedes hacen JSON.stringify() para volver a armarlo, el texto resultante puede diferir del nuestro en el orden de las claves, los espacios o el escapado de caracteres, y el HMAC va a dar distinto aunque el aviso sea legítimo. En el nodo Webhook de n8n hay que habilitar la opción que entrega el cuerpo sin parsear (según la versión aparece como "Raw Body") y firmar sobre ese texto.

Código para un nodo de n8n

Code
const crypto = require('crypto'); // El secreto que les entregamos junto con el alta del webhook. const SECRETO = 'reemplazar-por-el-secreto-compartido'; // Tolerancia de frescura, en segundos. const VENTANA_SEGUNDOS = 300; /** * Devuelve true solo si el aviso viene de Lelemon y no es un reenvio viejo. * * @param {string} cuerpoCrudo El cuerpo tal como llego, sin parsear ni re-serializar. * @param {string} timestamp Valor de la cabecera X-Lelemon-Timestamp. * @param {string} firma Valor de la cabecera X-Lelemon-Signature. */ function avisoValido(cuerpoCrudo, timestamp, firma) { // 1. Frescura: un aviso viejo reenviado no pasa por nuevo. const ahora = Math.floor(Date.now() / 1000); const enviado = Number(timestamp); if (!Number.isFinite(enviado)) return false; if (Math.abs(ahora - enviado) > VENTANA_SEGUNDOS) return false; // 2. Firma: HMAC-SHA256 de "<timestamp>.<cuerpo crudo>" en hexadecimal. const esperada = crypto .createHmac('sha256', SECRETO) .update(`${timestamp}.${cuerpoCrudo}`) .digest('hex'); // 3. Comparacion en tiempo constante, para no filtrar en cuanto acertaron. const a = Buffer.from(esperada, 'utf8'); const b = Buffer.from(String(firma || ''), 'utf8'); if (a.length !== b.length) return false; return crypto.timingSafeEqual(a, b); } // Uso dentro de un nodo Code de n8n: const entrada = $input.first().json; const cuerpoCrudo = entrada.rawBody; // ajustar al nombre real del campo crudo const cabeceras = entrada.headers; if (!avisoValido(cuerpoCrudo, cabeceras['x-lelemon-timestamp'], cabeceras['x-lelemon-signature'])) { throw new Error('Firma invalida o aviso vencido'); } return [{ json: JSON.parse(cuerpoCrudo) }];

Los nombres de las cabeceras llegan en minúsculas cuando el runtime las normaliza; si su nodo las expone con la capitalización original, busquen X-Lelemon-Timestamp y X-Lelemon-Signature.


3. Reintentos e idempotencia

Cuando el envío no sale bien, reintentamos. Las reglas son fijas:

  • Tres intentos en total: el primero, y hasta dos reintentos.
  • Esperamos 1 segundo antes del segundo intento y 5 segundos antes del tercero.
  • Cuenta como fallo cualquier respuesta fuera del rango 2xx, que no contesten dentro de los 10 segundos, o que no podamos alcanzar el destino.
  • Después del tercer intento fallido dejamos de insistir. El evento queda registrado de nuestro lado como fallido, con el código HTTP y la duración, para que podamos responder si les avisamos o no. No lo reencolamos solos: si el destino estuvo caído, el aviso se perdió y hay que reponerlo a mano.

Por eso conviene que el webhook de ustedes conteste rápido con un 200 y haga el trabajo pesado después. Si n8n arma el ticket de Jira antes de responder y eso demora más de 10 segundos, nosotros vamos a dar el envío por caído y a reintentar, aunque el ticket se haya creado bien.

Ahí entra la idempotencia. El eventId es el mismo en los tres intentos del mismo aviso y cambia con cada escalamiento nuevo. Guárdenlo y descarten todo aviso cuyo eventId ya hayan procesado; con eso, un reintento nuestro no les crea un segundo ticket para la misma conversación. No usen el dealId como llave de deduplicación: una misma conversación puede escalar más de una vez a lo largo del tiempo, y esas sí son dos atenciones distintas.


4. Avisarnos que el ticket se resolvió

La llamada

Code
POST https://api.lelemon.cl/v1/external/conversations/{id}/resolve

Donde {id} es el conversation.dealId que les llegó en el aviso. Tiene que ser un UUID válido.

La credencial va en la cabecera Authorization:

Code
Authorization: Bearer lel_a1b2c3d4e5f6_<...>

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 cuerpo

Todos los campos son opcionales. Un POST con {} es válido y sirve para el caso mínimo: cerrar el escalamiento y devolverle el control al agente.

CampoTipoLímiteQué hace
resolutiontexto2.000 caracteresQué se resolvió, en palabras. Queda escrito en la conversación como mensaje de sistema.
messagetexto4.000 caracteresSi viene, se le envía al contacto por su canal. Si se omite, la resolución sólo queda registrada.
externalReftexto120 caracteresEl identificador del ticket del lado de ustedes. Además de servir para cruzar, es lo que nos permite descartar un reintento: mándenlo siempre, ver abajo.
returnToAgentbooleano—Por defecto true: el control vuelve al agente IA. Manden false si quieren seguir atendiendo a mano.

Manden sólo estos campos: el endpoint valida contra esta lista y un campo de más no llega a nosotros.

Qué pasa de verdad cuando llaman

En orden:

  1. Buscamos la conversación dentro de la organización de la credencial. Si no existe, está borrada o es de otra organización, 404.
  2. Si mandaron externalRef y ya procesamos un aviso con esa misma referencia para esa conversación, cortamos ahí: no escribimos nada, no mandamos nada, no tocamos el control, y respondemos 200 con alreadyResolved: true.
  3. Revisamos que haya algo que resolver. Si la conversación no está escalada ni la tiene tomada alguien del equipo, devolvemos 409 y no escribimos nada. Esto es a propósito y conviene que lo tengan claro: sin esa puerta, una credencial con el alcance conversations:resolve sería en la práctica un permiso para mandarle un WhatsApp a cualquier contacto de la organización, porque bastaría con un id de conversación —que nosotros mismos mandamos en cada aviso saliente— y un message. Sólo se puede escribir sobre una conversación que efectivamente pidió un humano.
  4. Si mandaron message, resolvemos el canal de salida antes de escribir nada. Si esa conversación no tiene por dónde responderle al contacto —sin WhatsApp vinculado y sin origen en el copiloto in-app—, devolvemos 422 y no registramos nada. Preferimos el error a dejar la resolución escrita con el contacto sin enterarse.
  5. Escribimos un mensaje de sistema en la conversación con el texto Resuelto en sistema externo (<externalRef>): <resolution>. Queda visible en el historial y marcado como resolución externa.
  6. Si mandaron message, lo guardamos como mensaje del equipo con remitente sistema externo —no el nombre de una persona, para que quien lea la conversación después sepa quién habló— y lo despachamos por el canal del contacto.
  7. Salvo que hayan mandado returnToAgent: false, devolvemos el control al agente IA y la conversación vuelve a la etapa de entrada de su flujo. Con una excepción, ver abajo.

Cuando la conversación la tomó una persona del equipo

Si alguien de Lelemon tomó la conversación a mano antes de que ustedes avisaran, el control no vuelve al agente aunque no manden returnToAgent: false. Lo van a ver en la respuesta: el controlStatus sigue en human en vez de pasar a agent. No es un error ni un pedido que se ignoró. Devolver el control en ese caso le sacaría la conversación de las manos a la persona que la está atendiendo, y además la movería de etapa, sin que se entere. La resolución y el mensaje al contacto sí se registran y se envían con normalidad; lo único que no pasa es la devolución del control.

Por qué conviene mandar siempre externalRef

Adentro, un resolve son tres escrituras: el mensaje de sistema, el mensaje al contacto y la devolución del control. No están en una transacción. Si la última falla, ustedes reciben un 500 y —como hace n8n por defecto— reintentan; sin externalRef no tenemos con qué darnos cuenta, y el contacto recibe el mismo WhatsApp dos veces.

Con externalRef el segundo intento se corta al principio y responde 200 con alreadyResolved: true. Usen la llave del ticket, que ya es única del lado de ustedes.

La respuesta

200 OK con este cuerpo:

Code
{ "id": "b81d3f77-2c4a-4f19-9a86-31ee6c0b7d52", "controlStatus": "agent", "messageQueued": true, "alreadyResolved": false }
  • controlStatus — cómo quedó la conversación: agent (la maneja el agente IA), human (la tiene tomada alguien del equipo) o escalated (sigue esperando a un humano).
  • messageQueued — true si mandaron message y quedó encolado para salir por el canal del contacto. Se llama "encolado" y no "entregado" a propósito: en ese momento el mensaje está guardado y en camino, pero el envío por WhatsApp ocurre después. Si necesitan saber que llegó de verdad, mírenlo en la conversación, no en este campo.
  • alreadyResolved — true cuando este aviso ya lo habíamos procesado antes, reconocido por su externalRef. No es un error: significa que su reintento no hizo nada de más.

Códigos de error

Los errores llegan con este cuerpo:

Code
{ "statusCode": 404, "message": "Conversation b81d3f77-2c4a-4f19-9a86-31ee6c0b7d52 not found", "timestamp": "2026-08-19T15:02:11.883Z", "path": "/v1/external/conversations/b81d3f77-2c4a-4f19-9a86-31ee6c0b7d52/resolve" }
CódigoQué pasóQué hacer
400El {id} de la ruta no es un UUID válido, o el cuerpo no pasó la validación (por ejemplo, resolution de más de 2.000 caracteres). El mensaje de estos errores llega genérico, así que guíense por el campo que mandaron.Arreglar el pedido. Reintentar tal cual no sirve.
401Falta la credencial o no es válida. Falta la credencial. Mandala en el header Authorization: Bearer <clave>. significa que no mandaron ninguna; Credencial invalida. significa que la clave no existe, está revocada o no tiene la forma esperada. Es el mismo mensaje para los tres casos a propósito.Revisar la cabecera y la clave. Si sospechan que se revocó, escríbannos.
403La credencial es válida pero le falta el permiso. El mensaje dice cuál: A esta credencial le falta el permiso: conversations:resolve.Escríbannos para que le agreguemos el alcance.
404La conversación no existe, está borrada o pertenece a otra organización.Verificar que el id sea el dealId del aviso. No reintentar en loop.
409La conversación no está escalada ni la tiene tomada alguien del equipo, así que no hay nada que resolver. El mensaje es Esta conversación no está escalada: no hay nada que resolver. Pasa, por ejemplo, si el ticket se cierra dos veces, o si alguien ya resolvió el escalamiento por otra vía.No reintentar: el estado no va a cambiar solo. Si les extraña, consulten el estado con el GET antes de decidir.
422Mandaron message pero la conversación no tiene canal de salida. La resolución no se registró.Reintentar sin el campo message, para al menos dejar la resolución escrita y devolver el control.
429Pasaron un techo por minuto: 200 pedidos por dirección IP o 3.000 por credencial. El cuerpo dice Demasiadas solicitudes, intenta de nuevo en un momento.Esperar y reintentar. Es seguro reintentar.
500Error nuestro.Reintentar con espera. Si persiste, escríbannos: los 500 nos llegan como alerta.

Ejemplo con curl

TerminalCode
curl -X POST "https://api.lelemon.cl/v1/external/conversations/b81d3f77-2c4a-4f19-9a86-31ee6c0b7d52/resolve" \ -H "Authorization: Bearer lel_a1b2c3d4e5f6_<...>" \ -H "Content-Type: application/json" \ -d '{ "resolution": "Se reproceso la carga de documentos y el tramite quedo en revision final.", "message": "Hola Marcela, ya revisamos tu caso: reprocesamos los documentos y quedaron en revision final. Te avisamos apenas se cierre.", "externalRef": "PROC-4821", "returnToAgent": true }'

Ejemplo para un nodo HTTP Request de n8n

  • Method: POST
  • URL: https://api.lelemon.cl/v1/external/conversations/{{ $json.dealId }}/resolve
  • Authentication: ninguna de las predefinidas; la credencial va como cabecera.
  • Headers:
    • Authorization: Bearer {{ $env.LELEMON_API_KEY }} — la clave guardada como credencial o variable de entorno de n8n, nunca escrita adentro del nodo.
    • Content-Type: application/json
  • Body Content Type: JSON
  • Body:
Code
{ "resolution": "={{ $json.resolucion }}", "message": "={{ $json.mensajeAlContacto }}", "externalRef": "={{ $json.jiraKey }}", "returnToAgent": true }

Si no quieren mandarle nada al contacto, saquen message del cuerpo en vez de mandarlo vacío.

Consultar el estado de una conversación

Existe también, por si les sirve decidir sin adivinar si una conversación sigue escalada o ya la resolvieron:

Code
GET https://api.lelemon.cl/v1/external/conversations/{id}

Misma credencial, pero exige el alcance conversations:read. Devuelve 200 con los datos de la conversación: id, name, email, phone, status, controlStatus, controlledBy, assignedTo, escalatedAt, escalationReason, pipelineId, pipelineName, currentStageId, currentStageName, lastMessageAt y createdAt. Los mismos códigos de error que el endpoint anterior.

Ver las conversaciones anteriores de la misma persona

Cuando alguien vuelve a escribir, la conversación nueva es otra, pero la persona es la misma. Para ver qué pasó antes:

Code
GET https://api.lelemon.cl/v1/external/conversations/{id}/historial

Exige conversations:read. Devuelve 200 con conversationId (la que preguntaron) y conversations: las otras conversaciones de esa persona en su organización, de la más nueva a la más vieja, hasta 50. Cada una trae conversationId, pipelineId, pipelineName, status, outcome (por qué terminó), summary (un resumen breve escrito al cerrarse, o null), firstMessage (lo primero que escribió el cliente, hasta 160 caracteres, o null), messageCount, createdAt y lastMessageAt. Si no hay anteriores, conversations viene vacío.

Los mensajes de una conversación anterior se leen con GET /v1/external/conversations/{conversationId}/messages, el mismo de lelemon-api-agentes.md. Los códigos de error son los mismos que el endpoint anterior: 404 si la conversación no existe o no es de su organización.

Con esa misma credencial existe también la superficie para hablarle a un agente desde una app de ustedes: abrir una conversación, mandar mensajes y leer el hilo. Está documentada aparte en lelemon-api-agentes.md y exige los alcances agents:converse y conversations:read. No hace falta para recibir avisos ni para resolver; queda ahí por si quieren que el agente atienda algo que no entró por WhatsApp.

Si lo que necesitan es escribirle a la persona desde su propio sistema sin que corra el agente —una encuesta al cerrar un caso, o la respuesta de su mesa de soporte sobre un caso escalado—, hay una ruta aparte con el alcance conversations:send, documentada en lelemon-api-mensajes.md.


5. 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.

Lo que tienen que saber:

  • Se entrega una sola vez. De nuestro lado guardamos sólo un SHA-256 de la clave completa. Ni con la base de datos en la mano podemos volver a armarla: si la pierden, no la recuperamos, emitimos otra.
  • Es por organización. De la clave sale la organización, y de ningún otro lado: nunca leemos el identificador de organización del cuerpo ni de la URL. Con la clave de su organización sólo pueden ver y resolver conversaciones de su organización, aunque conozcan el id de una conversación ajena.
  • Se puede revocar en cualquier momento. Revocar es definitivo: esa clave no vuelve, se emite una nueva. La fila queda guardada para poder responder después qué clave hizo qué.
  • Alcances de la clave de ustedes: conversations:resolve para avisar que un escalamiento se resolvió, y conversations:read para consultar el estado de una conversación. No hay nada más habilitado. Un endpoint que no declare explícitamente su alcance se rechaza, así que la clave no se abre sola a superficie nueva.
  • Registramos la fecha del último uso, para saber cuándo una clave dejó de usarse.

No la manden por un canal que quede registrado en claro: nada de correo, chat corporativo, ticket de Jira, comentario de un issue ni archivo en un repositorio. Del lado de ustedes debería vivir como credencial o variable de entorno de n8n, no escrita adentro de un nodo. Si sospechan que quedó expuesta, avísennos y la revocamos: emitir una nueva toma un minuto.

Lo mismo vale para el secreto del webhook de la sección 2: es un secreto compartido, se maneja igual y también se puede reemplazar.


6. Si algo falla

No llega ningún aviso de escalamiento. Verifiquen primero que el destino responde desde afuera: no seguimos redirecciones, así que si la URL redirige a otra, para nosotros es un fallo. Después, que no esté demorando más de 10 segundos en contestar. Si de su lado no hay ni rastro del pedido, escríbannos: guardamos cada entrega, así que podemos ver si el envío salió, con qué código respondió el destino y cuántos intentos hicimos.

Llega el aviso pero la firma no calza. Casi siempre es el cuerpo re-serializado. Comprueben que están firmando el texto crudo y no el resultado de un JSON.stringify() sobre el objeto ya parseado. Después, que estén concatenando timestamp + . + cuerpo en ese orden, que el timestamp sea el de la cabecera y no la hora de ustedes, y que comparen en hexadecimal minúsculas.

Llegan tickets duplicados. Es un reintento nuestro sobre una respuesta que no fue 2xx o que demoró más de 10 segundos. Deduplicar por eventId lo resuelve; contestar rápido con 200 y hacer el trabajo después lo previene.

El POST de resolución devuelve 422. La conversación no tiene canal de salida y el message no se puede entregar. Reintenten sin message: la resolución queda escrita igual y el control vuelve al agente.

El POST de resolución devuelve 404 con un id que sí salió en el aviso. Revisen que estén usando conversation.dealId y no contact.id. Si es el correcto, escríbannos.

El POST de resolución devuelve 409. Esa conversación ya no está esperando a un humano. Lo más común es cerrar dos veces el mismo ticket, o que el escalamiento se haya resuelto antes por otra vía. Si necesitan confirmarlo, el GET les dice en qué estado quedó.

El POST devuelve 200 pero el controlStatus sigue en human. No es un bug: alguien del equipo tomó esa conversación a mano y por eso no le devolvimos el control al agente. La resolución quedó escrita igual.

El POST devuelve 401 de un día para el otro. La clave se revocó o se rotó. Escríbannos y emitimos una nueva.

Para cualquiera de estos casos podemos mirar de nuestro lado el registro de entregas —código HTTP, cantidad de intentos, duración y último error por webhook—, así que un mensaje con la fecha, la hora aproximada y el eventId o el dealId nos alcanza para responder qué pasó.


7. Lo que todavía no hacemos

Tres límites conocidos, para que no los descubran el día que los necesiten.

No hay rotación de credencial con dos llaves válidas a la vez. Para rotar hay que revocar la actual y emitir una nueva, en ese orden, así que entre una cosa y la otra hay un corte breve en el que los pedidos de ustedes van a recibir 401. Si tienen que rotar, coordinemos una ventana; son minutos, pero conviene que no los agarre en medio de un cierre de tickets.

No hay pantalla para que se emitan la credencial solos. Hoy la generamos nosotros y se la entregamos una vez. Lo mismo para revocarla o cambiar sus alcances: es un mensaje a nosotros.

No hay pantalla donde vean el registro de entregas. Guardamos cada intento con su código HTTP, cantidad de reintentos, duración y último error, pero por ahora eso lo miramos nosotros. Si necesitan saber si un aviso salió, escríbannos con la fecha y el eventId o el dealId.

Acciones HTTPMandar un mensaje a una conversación
On this page
  • Qué resuelve esto
  • 1. Recibir el aviso de escalamiento
    • Cabeceras que mandamos
    • El JSON que les llega
    • Qué significa cada campo
  • 2. Verificar que el aviso es nuestro
    • Código para un nodo de n8n
  • 3. Reintentos e idempotencia
  • 4. Avisarnos que el ticket se resolvió
    • La llamada
    • El cuerpo
    • Qué pasa de verdad cuando llaman
    • Cuando la conversación la tomó una persona del equipo
    • Por qué conviene mandar siempre externalRef
    • La respuesta
    • Códigos de error
    • Ejemplo con curl
    • Ejemplo para un nodo HTTP Request de n8n
    • Consultar el estado de una conversación
    • Ver las conversaciones anteriores de la misma persona
  • 5. La credencial
  • 6. Si algo falla
  • 7. Lo que todavía no hacemos
JSON
Javascript
JSON
JSON
JSON