Paquetes publicados y sin publicar
Los canales En npm se instalan con
pnpm add y siguen semver (los marcados “desde la beta” se publicaron por primera vez con ella). @botmesh/tiktok lleva "private": true en su
package.json: está implementado y con tests, pero no se publica en npm y
su API todavía puede cambiar. Úsalo desde un clon del monorepo ("workspace:*"). Lo mismo vale para el
sandbox. El ingress de webhooks,
@botmesh/server, sí está en npm.
Elegir un canal
WhatsApp tiene varias opciones:@botmesh/baileys: vincula el bot como un dispositivo de WhatsApp Web (QR o código de emparejamiento) y abre su propio socket. Sin servidor público ni webhook, con grupos y encuestas, sin coste por mensaje. Es un cliente no oficial: automatizar una cuenta puede infringir los términos de WhatsApp y acabar en el bloqueo del número. Deja activado el rate limiter.@botmesh/meta: la WhatsApp Cloud API oficial. Botones, listas, plantillas y formularios con WhatsApp Flows nativos, sin riesgo de baneo. Necesita un webhook HTTPS público, no tiene grupos y los mensajes libres solo funcionan dentro de la ventana de atención de 24 horas.@botmesh/evolution/@botmesh/wppconnect: ya tienes desplegado un Evolution API o un WPPConnect Server. El bot habla con ese gateway por REST y recibe sus webhooks. Ambos gateways manejan WhatsApp Web, así que valen las mismas advertencias que con Baileys (no oficial, sin botones nativos fiables). Lo común (cliente REST, media, reconciliación LID/PN) vive en@botmesh/wa-gateway-kit, una dependencia pública de ambos; se instala el canal, no el kit (salvo que escribas un transporte para otro gateway).@botmesh/twilio/@botmesh/gupshup: envías WhatsApp a través de la cuenta de ese proveedor. Admiten plantillas; Twilio cubre además SMS/MMS con la misma clase.@botmesh/gohighlevel: el bot responde a los contactos del CRM HighLevel por el canal que usaron (SMS, WhatsApp, Email, Facebook, Instagram, Live chat…).
@botmesh/telegram (long polling por defecto, webhook
opcional), @botmesh/discord (socket del gateway), @botmesh/messenger e
@botmesh/instagram (webhooks de Meta), @botmesh/email (cada hilo de correo
es un chat) y @botmesh/tiktok (comentarios en tus propios videos y DMs).
Socket, polling o webhook
- Socket / polling (el provider abre la conexión): Baileys, Discord, Telegram en modo polling, Email (IMAP IDLE). No hay nada que exponer a internet.
- Webhook (
capabilities.pushIngress: true): Cloud API, Messenger, Instagram, Telegram en modo webhook, Evolution, WPPConnect, Twilio, HighLevel, Gupshup, TikTok. Tu servidor HTTP recibe el POST y le pasa el body crudo y las cabeceras aprovider.ingest(), que verifica la firma o el token y devuelve el status HTTP con el que responder.@botmesh/serverresuelve ese cableado (body crudo, límites de tamaño, timeouts, límite de concurrencia, endpoints de salud) paranode:http, Express, Fastify y Hono.
@botmesh/server (pnpm add @botmesh/server): una
ruta por provider, y el handshake GET y los POST firmados quedan resueltos:
ingest():
GET, Telegram no tiene ninguno, y los providers
cuya autenticación cubre la URL también la reciben (el
ingest(body, headers, url) asíncrono de WPPConnect,
ingestRequest({ url, body, headers }) en Twilio y Gupshup). ingest() no
espera a tus handlers, así que la plataforma recibe su respuesta enseguida.
El constructor de todo provider por webhook (Telegram, solo en modo webhook)
lanza un error si falta su credencial de webhook (app secret, auth token o token compartido), salvo que
pases allowUnsignedWebhooks: true, pensado solo para desarrollo local
aislado. HighLevel es la excepción: su clave pública de firma viene incluida.
Matriz de capacidades
Los valores salen de la declaracióncapabilities de cada provider. Una
función marcada “no” se degrada, no se rompe: los botones y las listas
pasan a un menú de texto numerado, ctx.edit envía un mensaje nuevo,
ctx.react resuelve { skipped: true } y ctx.typing no hace nada (ver
Mensajes y media).
Contenido. Lo que cada canal envía de forma nativa.
Entrada y presencia. Cómo llegan los eventos y qué indicadores de presencia funcionan.
- Enviar reacciones solo describe el envío. Varios providers reciben
igualmente reacciones como eventos
reaction(Messenger, Instagram, Gupshup, TikTok…). ctx.stream:borrador= vistas previas nativas en borrador y un mensaje final (chats privados de Telegram; ediciones en el resto),edición= un mensaje que se edita a medida que crece el texto,fragmentos= sin vista previa en vivo, mensajes del tamaño de una frase con indicador de escritura (streaming: 'none'). Ver Respuestas en streaming.- Formularios:
nativoenvíaflow().form()como un WhatsApp Flow;campo a campopregunta cada campo con mensajes de texto. textFormat(no es columna): el formato de texto que muestra el canal, para que quien genere Markdown, como@botmesh/ai, lo convierta.'whatsapp'(*negrita*,_cursiva_,~tachado~): Baileys, Cloud API, Evolution, WPPConnect, Gupshup.'plain'(sin formato): Telegram (se envía sinparse_mode), Messenger, Instagram, TikTok.'markdown': Discord. Sin declarar: Email. Los providers que sirven varios canales deciden por chat con el método opcionaltextFormatFor(chatId), que gana acapabilities.textFormaty llega a los handlers comoctx.textFormat: Twilio devuelve'whatsapp'en los chatswhatsapp:y'plain'en SMS; HighLevel sigue el canal de respuesta del contacto ('whatsapp'en WhatsApp,'plain'en SMS, Email, FB, IG, Live chat, GMB y custom).- Plantillas es el tipo de contenido
template: plantillas de la Cloud API, Content Templates de Twilio (ContentSid),templateIdde HighLevel, plantillas de Gupshup. El resto de providers lo rechaza conUnsupportedContentError. - Todos los providers salvo Baileys aceptan el tipo de contenido
native(ctx.sendNative('<id del provider>', payload)) como vía de escape a la API propia del canal, con un payload específico de cada provider. Baileys lo rechaza conUnsupportedContentError. Discord, Messenger, Instagram, Evolution, WPPConnect, Twilio, Gupshup, HighLevel, Email y TikTok también rechazan un payload dirigido a otro id de provider en lugar de enviarlo.
Contrato común de los canales
Las mismas reglas valen en todos los canales, así un bot se comporta igual donde corra. Quien escribe un provider las comprueba con los helpers de@botmesh/core/testing
(ver Testing).
- Los timestamps son milisegundos epoch.
ctx.timestamp,InboundMessage.timestamp,InboundInteraction.timestampyDeliveryStatus.timestampson ms epoch en todos los canales. Los transportes que informan segundos (WhatsApp, Telegram, la Cloud API) se multiplican por 1000; uno que no informa hora (Twilio) usa la hora de recepción, nunca0. Telegram no envía la hora de pulsación de un botón, así que un callback query lleva la hora de recepción. - El texto largo se divide. Cada canal declara su límite de texto y el core divide un
envío
textmás largo en varios mensajes (ver Mensajes;splitLongText: falselo desactiva). - Las operaciones no soportadas no existen. Una operación que el transporte no puede
hacer no se implementa: falta el método opcional (
typing,markRead,editMessage,deleteMessage) y su flag de capacidad esfalse, así quectx.typing(),ctx.markRead()yctx.delete()no hacen nada. Un método que existe hace la operación o rechaza; nunca es un sustituto silencioso. - La presencia es de mejor esfuerzo sin conexión.
typing()ymarkRead()resuelven sin tocar el transporte mientras el último estado de conexión del provider no es'ready'(antes de questart()conecte, durante una reconexión, después destop()): sin error, sin cola y sin reintento posterior. El bot aplica la misma regla antes de llamarlos, registra la señal omitida en debug y la cuenta enmetrics().presenceSkipped(botmesh_presence_skipped_totalentoPrometheus()). Ya conectado, un fallo del transporte sigue rechazando.assertPresenceWhileDisconnected()lo comprueba en un provider. - Demasiadas opciones rechazan. Un transporte que muestra menos botones, filas o
respuestas de encuesta de las que enviaste rechaza con
UnsupportedContentError(Cloud API, Messenger, Instagram, Discord), salvo que tenga una degradación documentada (texto numerado en Baileys, Evolution, WPPConnect, Gupshup, TikTok). - Una ruta local de media que no se puede leer rechaza con
UnsupportedContentError(el error de fs comocause), nunca con unENOENTcrudo. - Las esperas tienen tope. Cada llamada REST tiene timeout (
requestTimeoutMsen la Cloud API, Telegram, Discord, Messenger e Instagram) y unRateLimitedErrornunca pide más de 24 h. - Una edición conserva el id original. Donde el transporte informa de ediciones de
texto (Baileys,
edited_messagede Telegram,MESSAGE_UPDATEde Discord), la edición llega como mensaje con el texto nuevo y elref.idoriginal; cada uno de esos paquetes exportaisEditedMessage(ctx)y una opcióneditedMessages: 'ignore'. El dedupe del core deja pasar la edición porque su contenido cambió y descarta una re-entrega de la misma edición. - Los votos de encuesta tienen una sola forma. Baileys y Telegram emiten cada voto
como evento
'poll'(bot.on('poll', (vote) => …)):vote.refes el mensaje de la encuesta,vote.senderIdquien votó yvote.optionsel recuento actual de cada opción con susvoters. - Un solo esquema de nombres para los helpers de bajo nivel. El mapeador de errores
de cada provider es
to<Canal>Error(toTelegramError,toDiscordError,toTwilioError…; los canales de Meta compartentoGraphErrorde@botmesh/graph-core). Telegram, TikTok y la Cloud API conservan su antiguotoProviderError(y Email sutoEmailProviderError) como alias deprecados; los canales que se publican por primera vez con la beta solo tienen los nombres nuevos.ingest()devuelve el estado HTTP con el que responder (unnumber); WPPConnect lo devuelve comoPromise<number>(resuelve los LID desconocidos contra el gateway antes de emitir, sin esperar a tus handlers) y elingest()de email recibe un mensaje crudo, no un webhook, y devuelve unEmailIngestResult.
Providers
@botmesh/baileys
WhatsApp Web a través de Baileys.
Se vincula con un código QR (o un código de emparejamiento, pairingPhoneNumber) y guarda la sesión en
sessionsDir (./sessions/<session>): los arranques siguientes reconectan sin
QR. Depende de la versión exacta 7.0.0-rc14 de @whiskeysockets/baileys
(todavía no hay Baileys 7 estable); el README explica esa fijación y la
política de actualización.
- Socket propio del provider, con reconexión y backoff
(
reconnect: { maxAttempts, baseMs, capMs }lo ajusta,falselo desactiva). - Las formas LID y número de teléfono de un usuario se unen en un único
senderIdestable. - Sin botones ni listas nativos (WhatsApp Web los retiró): usa menús
choose()octx.poll(). - El rate limiting anti-baneo de envíos está activado por defecto
(
rateLimit). - Las reacciones y los borrados llegan como eventos, nunca como mensajes,
así que un handler comodín o un
askabierto no los toman como texto. Una edición de texto llega como mensaje con el texto nuevo y elref.idoriginal, como en Telegram:isEditedMessage(ctx)(de@botmesh/baileys) la distingue yeditedMessages: 'ignore'descarta las ediciones. Solo el autor de un mensaje que el proceso ha visto puede editarlo: la edición de un mensaje ajeno, de uno del bot o de un original que ya no está en caché se descarta. ctx.voice()yctx.sticker()aceptan URLs y streams (descargados con los mismos controles anti-SSRF que el resto de la media);ctx.poll()necesita de 2 a 12 opciones distintas.
@botmesh/meta (WhatsApp Cloud API)
La API oficial. Funciona por webhook: handshake GET con verifyChallenge()
y eventos POST con ingest() (ver el ejemplo de servidor
más arriba).
Configuración en Meta: crea una app con el producto WhatsApp en
developers.facebook.com, genera un token de usuario del sistema permanente
con whatsapp_business_messaging, copia el phone number id del número
emisor y, en WhatsApp → Configuración, pon la URL del webhook y un verify token
que elijas, y suscríbelo al campo messages (trae tanto los mensajes como
los estados de entrega).
- Botones de respuesta nativos (máximo 3: con más rechaza con
UnsupportedContentError, envía una lista), listas, plantillas y formularios con WhatsApp Flows. Un botón conurlsale como botón de enlacecta_url(uno por mensaje: varios enlaces, o un enlace mezclado con botones de respuesta, rechazan). Unacardconserva su imagen (como media o como cabecera del mensaje interactivo) y sus botones. Los límites de Graph se comprueban antes de enviar: sin botones, ids o títulos de botón repetidos (comparados tras el corte a 20 caracteres), una lista sin filas, con más de 10 filas en total o con ids de fila repetidos rechazan conUnsupportedContentError. Las etiquetas se cortan por grafemas, así que un emoji se conserva entero o se quita, nunca se parte. - Si falta
accessToken,phoneNumberIdoverifyToken, el constructor falla con un error que nombra el campo (phoneNumberIdtambién puede ser un entero seguro). Cada petición a Graph tiene un límite de tiempo (requestTimeoutMs, 60 s por defecto); las subidas de media tienen el suyo (uploadTimeoutMs, 5 min por defecto). - Una respuesta interactiva que no es la pulsación de un botón o una lista
(por ejemplo
call_permission_reply) llega como mensajeunknown, con el payload crudo enctx.ref.native. - Versión de la Graph API:
apiVersiontoma por defectoDEFAULT_GRAPH_API_VERSIONde@botmesh/graph-core(v26.0, publicada el 29-07-2026; Messenger e Instagram usan el mismo valor). Meta retira cada versión unos dos años después de publicarla, así que el valor por defecto sigue a la actual y cambia en una versión menor, tras revisar los payloads contra el changelog de Meta. FijaapiVersion(por ejemplo'v24.0') para quedarte en una versión hasta migrar. Un estadoplayed(se escuchó una nota de voz) llega comoreadconplatformData.played: true. - Los usuarios con username de WhatsApp pueden llegar con un BSUID en lugar
del teléfono; el provider mantiene un id estable por persona
(
identityMap). - Fuera de la ventana de 24 h los envíos libres rechazan con
OutsideSessionWindowError. El gestor opcionalsessionWindowreabre el chat con una plantilla y reintenta: ver Ventana de 24 h y costos. - La API no tiene grupos, indicador de escritura ni encuestas, ni borrado para
todos: el provider no tiene
typing()nideleteMessage(), así quectx.typing()yctx.delete()no hacen nada (ver el contrato común de los canales). - El core divide en varios mensajes el texto de más de 4096 caracteres.
- Con el bot parado,
ingest()responde503, así que Meta reenvía el webhook tras el reinicio. publishForm()fija en el Flow la etiqueta de envío por defecto de un formulario en ellangdel provider (por defecto'en'; ponle elBotConfig.langde tu bot), o enpublishForm(def, { lang })para un Flow. ElsubmitLabeldel propio formulario siempre gana.
@botmesh/telegram
Telegram Bot API. Long polling por defecto; polling: false cambia a modo
webhook, donde webhookSecret es obligatorio.
- Polling no necesita URL pública: úsalo en desarrollo y para un bot que
corre en un solo proceso. Solo puede haber un poller por token (un segundo
recibe
409y el provider se detiene). - Webhook encaja con serverless y despliegues con varias réplicas detrás de
HTTPS. Registra la URL una vez con
setWebhookde la Bot API, pasando el mismo secreto comosecret_token.
- Teclados inline, encuestas nativas, reacciones y edición de mensajes.
ctx.stream()usa borradores nativos en chats privados y ediciones en el resto.- En modo webhook
ingest(body, headers)acepta los bytes crudos, el string JSON o el objeto ya parseado, y comprueba la cabeceraX-Telegram-Bot-Api-Secret-Token(en cualquier capitalización, objeto plano oHeadersde Fetch). Con@botmesh/serverno hace falta ninguna opción. Trasstop()responde503y no emite nada, así que Telegram reenvía esos Updates cuando el bot vuelve. - Los comandos de grupo dirigidos al bot (
/menu@MiBot) llegan abot.command('menu'), también en el pie de una foto o un archivo: el provider quita el sufijo cuando nombra a este bot (usuario degetMe, o la opciónusernamesi pasasselfId). - Menciones al bot: un
@MiBoten el texto y un comando dirigido como/menu@MiBotañaden elselfIddel bot actx.mentions, así quegroupMode: 'mentions'los deja pasar. Las demás menciones@usuariono llevan id de usuario y no se listan; untext_mention(un usuario sin nombre de usuario) sí. - Cada petición a la Bot API tiene timeout de cliente (
requestTimeoutMs, 120 s por defecto; el long poll usapollTimeoutMs(30 s por defecto) + 15 s; las subidas de archivos,uploadTimeoutMs, 5 min por defecto), los errores nunca llevan el token del bot, unretry_afterde 429 se respeta hasta 24 h ybot.stop()corta al momento las esperas de reintento del bucle de polling. - Mensajes editados: llegan al bot como mensajes con el texto nuevo y el
ref.idoriginal (respuestas, reacciones y ediciones apuntan al mensaje editado).isEditedMessage(ctx)los distingue; el dedupe del core deja pasar la edición porque su contenido cambió.editedMessages: 'ignore'los desactiva. - Votos de encuesta: las encuestas se envían no anónimas
(
is_anonymous: false), así que cada voto llega como updatepoll_answery se convierte en un evento'poll'con el recuento, como en WhatsApp. Los miembros del chat ven quién votó;anonymousPolls: trueenvía encuestas anónimas, cuyos votos nunca llegan al bot. Se cuentan los votos de las encuestas que envió este proceso (en memoria, las últimas 500): un voto en una encuesta enviada antes de un reinicio se descarta. Los canales no admiten encuestas con votantes públicos: una encuesta a un canal se reintenta una vez como anónima (sin votos) y ese canal recibe encuestas anónimas desde entonces. - Servidor Bot API propio (self-hosted): apunta
baseUrla él. Si va por HTTP plano o en una red privada, las descargas de media necesitan ademásmediaDownload: { allowHttp: true, allowPrivateHosts: true }(y unmaxBytesmayor). Arráncalo sin--local: en ese modo las rutas de archivo no se sirven por HTTP.
@botmesh/messenger
Facebook Messenger a través del Send API de Graph, con la misma verificación
de webhook que la Cloud API.
- Botones y listas nativos; las reacciones llegan pero no se pueden enviar
(suscribe el webhook a
message_reactions;emojies el carácter yremovedse marca conunreact). - El texto se limita a 2000 caracteres (
maxTextLength): el texto más largo se divide en varios mensajes, y los trozos dectx.stream()caben en la Send API. - Más de 3 botones (plantilla de botones) o de 10 filas de lista (plantilla
genérica) rechazan con
UnsupportedContentErroren vez de descartar opciones. - Cada llamada a la Send API vence tras
requestTimeoutMs(60 s por defecto). - No se corta texto: una lista envía su texto antes de la plantilla genérica (que no tiene encabezado), y el texto de botones que pasa de los 640 caracteres de la plantilla sale antes como texto plano.
- Varios adjuntos en un mensaje llegan como un mensaje cada uno (
<mid>,<mid>#2…). Un mensaje editado (suscribemessage_edits) llega de nuevo con su id original;isEditedMessage(ctx)lo distingue. - Etiquetas de mensaje:
bot.send(psid, texto, { platformData: { tag: 'HUMAN_AGENT' } })envía fuera de la ventana de 24 horas (también aceptamessaging_type,notification_typeypersona_id). - Con el bot parado,
ingest()responde503, así que Meta reenvía el webhook tras el reinicio.
@botmesh/instagram
DMs de una cuenta profesional de Instagram conectada a una Página de
Facebook (Messenger API for Instagram).
- Los botones salen como quick replies (como mucho 13, títulos cortados a 20
caracteres, ids y títulos únicos, sobre un texto no vacío; lo demás rechaza
con
UnsupportedContentErroren vez de descartar opciones); las listas se degradan a texto numerado. Cada petición a la Send API caduca trasrequestTimeoutMs(60 s). - Solo DMs (sin grupos); las reacciones llegan pero no se pueden enviar.
- El texto se limita a 1000 bytes UTF-8 (
maxTextBytes, además demaxTextLength1000), así que el texto no ASCII cabe menos: el texto más largo se divide en varios mensajes por bytes, y los trozos dectx.stream()también caben. - Los adjuntos no llevan pie de foto: el caption de un medio sale como un mensaje de texto justo después del adjunto, y la imagen de una tarjeta después de su texto (una tarjeta solo con imagen envía la imagen; una sin texto ni imagen se rechaza).
- Un DM con varias fotos llega como un mensaje por foto (ids
<mid>,<mid>#2,<mid>#3…). - Con el bot parado,
ingest()responde503, así que Meta reenvía el webhook tras el reinicio.
@botmesh/discord
Gateway de Discord (WebSocket) para los eventos y REST para enviar.
- Botones, menús de selección, encuestas, reacciones y ediciones nativos;
ctx.stream()edita un mensaje a medida que crece el texto. - El bot mantiene una reacción por mensaje, como en WhatsApp/Telegram: un
nuevo
ctx.react(emoji)reemplaza la anterior yctx.react('')la quita. Discord borra reacciones por emoji, así que el provider recuerda el que puso (en memoria,reactionMemory, 1000 mensajes por defecto); si no lo conoce (se puso antes de reiniciar o salió del LRU) rechaza conUnsupportedContentError. - Las interacciones tienen un plazo estricto de confirmación
(
interactionsRequireAck), así que el core confirma cada una exactamente una vez por ti: una confirmación diferida antes del primer envío del handler, poco antes del plazo de ~3 s o al terminar el handler, lo que ocurra antes. Llama actx.acknowledge('texto')antes de responder para contestar con un texto inmediato; una vez enviada la confirmación automática,ctx.acknowledge('texto')envíatextocomo respuesta normal yctx.acknowledge()no hace nada. ctx.interaction.ides el id de la interacción a secas: su token no sale del provider. Como red de seguridad, el provider también difiere cualquier interacción que siga sin respuesta trasautoAcknowledgeMs(2000 ms por defecto;falselo desactiva). Unctx.acknowledge('texto')tardío, ya confirmada, sale como followup, conappIdo, si falta, con el id de aplicación que Discord manda en READY y en cada interacción; si no se conoce ninguno, la llamada rechaza conProviderErroren vez de descartar el texto.- Más de 25 botones (5 filas de 5), 25 opciones de menú o 10 respuestas de
encuesta rechazan con
UnsupportedContentErroren vez de descartar opciones; el texto de más de 2000 caracteres se divide en varios mensajes. - Cada llamada REST vence tras
requestTimeoutMs(60 s por defecto); las subidas de archivos, trasuploadTimeoutMs(5 min). - Los mensajes de sistema (entradas al servidor, fijados, boosts…) y los de
bots no se enrutan. Un mensaje sin texto ni archivo llega como
sticker(con la imagen del sticker como media) ounknown(solo embeds, o el intent Message Content desactivado), nunca como texto vacío. - Todo mensaje que envía el provider lleva
allowed_mentions(por defecto{ parse: ['users'] }, opciónallowedMentions), así que un texto que el bot repite no puede mencionar a@everyone,@hereni a un rol;SendOptions.mentions,platformData.allowed_mentionsy los payloadsnativelo sustituyen. Una respuesta citada sigue notificando al autor del mensaje citado (replied_user: true;allowedMentions.replied_user: falseresponde sin notificar). - Los ids van en las URLs REST codificados, y un id vacío,
.o..rechaza conProviderError: un id manipulado nunca llega a otra ruta. Los votos de las encuestas nativas no llegan como eventos. - Un mensaje reenviado trae el texto y la media reenviados (de su snapshot),
no una respuesta citada; si no tiene nada legible llega como
unknown. Una encuesta nativa llega comotype: 'poll'con la pregunta como cuerpo. - Mensajes editados (
MESSAGE_UPDATEconedited_timestamp) llegan como mensajes con el texto nuevo y elref.idoriginal;isEditedMessage(ctx)de@botmesh/discordlos distingue yeditedMessages: 'ignore'los descarta. Las demás actualizaciones (previsualizaciones de enlaces, fijados, flags) no se enrutan: el provider entrega una edición una sola vez poredited_timestamp(recuerda el último de los 1000 mensajes editados más recientes), así que fijar un mensaje editado no lo vuelve a entregar.
@botmesh/evolution
Evolution API v2, un
gateway REST de WhatsApp self-hosted construido sobre Baileys. Salida: REST
con la cabecera apikey. Entrada: el webhook de la instancia, autenticado con
un secreto compartido en cabecera o con el jwt_key de Evolution (JWT HS256).
start()comprueba la instancia; si no está vinculada, el QR o el código de emparejamiento llega en la actualizaciónconnection(authPayload).- Los gemelos LID/PN que envía Evolution se enlazan antes de emitir; pasa
identityPersistencepara mantener los ids estables entre reinicios. - Encuestas, reacciones, edición (texto), escribiendo y confirmaciones de
lectura; botones, listas y cards se degradan a texto numerado;
templatese rechaza, igual que una encuesta que WhatsApp no puede llevar (menos de 2 o más de 12 opciones, vacías o repetidas). - Si Evolution deja de ofrecer códigos QR antes de vincular el número
(
QRCODE_LIMIT), la actualizaciónconnectionllega comoclosedcon motivosessionInvalid: llama otra vez astart()para obtener un QR nuevo. - Deja desactivado
webhookByEventsen el webhook de la instancia: con él, Evolution añade el nombre del evento a la URL y una ruta de path exacto responde 404.
@botmesh/wppconnect
WPPConnect Server, un
gateway self-hosted sobre WhatsApp Web (WA-JS). REST con token Bearer;
webhooks autenticados con un token en la URL del webhook.
ingest()es asíncrono y recibe la URL de la petición como tercer argumento, porque wppconnect-server no puede añadir cabeceras: el token viaja en la query string.- Los LID desconocidos se resuelven a número de teléfono (
contact/pn-lid) antes de emitir. markReadmarca el chat entero como leído; botones y listas se degradan a texto numerado.- La conexión sigue los webhooks
status-finddel servidor:isLogged/qrReadSuccess→ready,notLoggedyqrcode→authenticating, y desvincular el dispositivo desde el teléfono (disconnectedMobile) →closedconsessionInvalid.bot.stop()no cierra la sesión en el servidor.
@botmesh/twilio
Twilio Programmable Messaging: WhatsApp y SMS/MMS con una sola clase.
Webhooks verificados con X-Twilio-Signature, que cubre la URL pública
exacta.
chatIdes elFromde Twilio tal cual (whatsapp:+57…o+57…): la misma persona por WhatsApp y por SMS son dos chats.- Texto de hasta 1600 caracteres; media solo desde URLs públicas; botones y
listas pasan a texto numerado;
templatese traduce aContentSid+ContentVariables. ingestRequest({ url, body, headers })para URLs que cambian por petición (multi-tenant). ConwebhookPath(la URL se reconstruye con las cabecerasHost/X-Forwarded-*), pasa la ruta y la query de la petición como tercer argumento deingest()(@botmesh/serverya lo hace): así la query con la que llamó Twilio entra en la URL firmada.- El error 63016 se traduce a
OutsideSessionWindowError. - Un mensaje con varios adjuntos (
NumMedia> 1, hasta 10) llega como un mensaje por adjunto, en orden: el primero conserva elMessageSidy el texto (Body); el adjuntoilleva el idMessageSid:iy cuerpo vacío (ids únicos y estables ante reintentos del webhook).
@botmesh/gohighlevel
HighLevel (GoHighLevel / LeadConnector) Conversations API. Entrada por el
webhook InboundMessage (Ed25519, X-GHL-Signature), salida con
POST /conversations/messages por el último canal que usó el contacto.
chatId=contactIdde HighLevel: un chat por contacto en todos los canales; el canal de cada mensaje está enctx.chat.platformData.channel.- La clave pública de HighLevel viene incluida (
GHL_WEBHOOK_PUBLIC_KEY), así que no hace falta secreto de webhook.ingest()necesita el body crudo. - Media solo desde URLs públicas;
templatese traduce atemplateId. - El cuerpo HTML de un Email llega como texto plano en
ctx.body;ctx.raw.bodyconserva el HTML yctx.chat.platformData.subjectel asunto.
@botmesh/gupshup
Gupshup WhatsApp API: endpoints de envío v1 y callbacks v2. Gupshup no firma
los callbacks, así que se autentican con un token compartido en la URL del
callback (o en una cabecera).
- Los botones salen como quick replies (hasta 3), después como lista (hasta
10) y después como texto numerado; las listas son nativas hasta 10 filas. El
botón de la lista de los botones degradados usa
listButtonText(por defecto'Options', máximo 20 caracteres;'Opciones'para un bot en español). Las etiquetas más largas se recortan a los límites de WhatsApp (títulos de 20/24 caracteres) en vez de ser rechazadas. - Con
@botmesh/server, monta el provider como una ruta: el ingress pasa la URL de la petición aingest(), así que el callback con?token=funciona tal cual. - Usa un
webhookTokenseguro para URL (openssl rand -hex 32). Un secreto base64 pegado tal cual en?token=también coincide (un+literal no se lee como espacio), igual que su forma codificada con%. chatId= los dígitos del teléfono del remitente (sin+), justo lo que espera la API de envío.- Media solo desde URLs públicas. Gupshup también documenta restringir por IP: haz ambas cosas en producción.
@botmesh/email
Email: IMAP IDLE para recibir y SMTP para responder. Cada hilo de
correo es un chat, así que los flujos con ask() continúan cuando el cliente
responde al email. imapflow y nodemailer son peer dependencies opcionales
que se cargan de forma lazy.
threading: 'thread'(por defecto, un chat por hilo) o'sender'(un chat por dirección); persiste los hilos conthreadStore: cursorThreadStore(store). Ese store guarda también el cursor de UID del buzón, así que tras un reinicio el bot sigue después del último mensaje que procesó y no vuelve a entregar correo (ni siquiera conmarkSeen: false).- Protección contra bucles de respuestas automáticas y listas de correo,
gestión de rebotes (eventos
status: 'failed', supresión de direcciones con rebote permanente). - El email no autentica
Frompor sí mismo: usarequireAuthentication: { authservId }para aceptar solo correo que tu MTA haya autenticado. Las cabeceras hostiles quedan contenidas: se descartan las direcciones con caracteres de control, se ignoran los Message-ID enormes y se acota la cadena deReferences. securees TLS implícito (por defecto salvo en el puerto IMAP 143, y solo en el puerto SMTP 465); contra un puerto en texto plano o con STARTTLS,start()/send()rechazan con un mensajeTLS handshake failedque dice qué opción cambiar.- Sin IMAP,
ingest(rawMime)acepta el MIME crudo de un webhook de correo entrante y resuelve{ status, accepted, reason? }. - El transporte creado desde
smtpse cierra enstop()y se reconstruye en el siguiente envío (también con pool); unsmtpTransportinyectado es tuyo ystop()nunca lo cierra.
@botmesh/tiktok (aún no en npm)
TikTok API for Business (v1.3): comentarios en los videos propios de la
cuenta y mensajes directos (Business Messaging API, requiere aprobación).
Webhooks verificados con TikTok-Signature.
- Chats:
dm:<conversation_id>(DM),comment:<video_id>:<comment_id>(hilo de un comentario, un chat de grupo) yhic:<comment_id>(comentario de alta intención).ctx.chat.platformData.surfacelos distingue. - DMs: texto (hasta 6000 caracteres), imágenes, tarjetas Q&A para los botones (de 1 a 3); comentarios: respuestas de texto de hasta 150 caracteres. Los botones o listas que no caben en una tarjeta Q&A (más de 3 opciones, botones con URL, títulos de más de 40 caracteres) y los menús enviados a un hilo de comentarios se degradan a un menú de texto numerado, como en Baileys.
- El bot no puede iniciar una conversación y TikTok limita los mensajes por
ventana de 48 h (
OutsideSessionWindowError).
Actualizar desde una alpha de 2.0
Los bots nuevos pueden saltarse esta sección. Si tenías un bot sobre una de las primeras alphas de 2.0, el contrato común de los canales cambió algunos comportamientos; el detalle por paquete está en Notas de actualización: canales y plugins y Migrar a la API estable.Qué cambió en todos los canales
Qué cambió en todos los canales
ctx.timestampes milisegundos epoch en todas partes. Baileys, la Cloud API yMockProviderinformaban segundos epoch. Quita cualquier* 1000(new Date(ctx.timestamp)ya es correcto). Lo mismo vale para los timestamps deInboundMessage,InboundInteractionyDeliveryStatusen hooks propios. Un provider de terceros que aún emite segundos sigue funcionando: el core convierte un valor menor que1e12paractx.timestampy el log de mensajes, pero actualízalo para emitir ms (assertInboundContractlo detecta).- Las pulsaciones de botones en Telegram llevan la hora de recepción, no la fecha del mensaje que tiene el teclado.
- Se quitaron métodos que no hacían nada:
CloudAPIProvider#typing()y#deleteMessage(),TelegramProvider#markRead()yDiscordProvider#markRead(). Usactx.typing()/ctx.delete()/ctx.markRead()(que ahí no hacen nada) oprovider.typing?.(…). - El texto largo se divide en varios mensajes en vez de rechazar con
PayloadTooLargeError.new Bot({ splitLongText: false })recupera el comportamiento anterior. Los hooksbefore('send')siguen corriendo una vez por mensaje (sobre el texto completo, antes de dividirlo); losafter('send'), una vez por parte. - Demasiadas opciones rechazan en Discord (más de 25 botones u opciones de menú, más
de 10 respuestas de encuesta), Messenger (más de 3 botones, más de 10 filas de lista) e
Instagram (más de 13 quick replies) con
UnsupportedContentError; antes descartaban las opciones sobrantes en silencio. - Discord difiere las interacciones sin responder tras 2 s.
autoAcknowledgeMs: falserecupera el comportamiento anterior. Unctx.acknowledge('texto')tardío sale entonces como followup; sinappIdni id de aplicación recibido de Discord, rechaza. MockProviderinformacapabilities.typingyreadReceiptscomofalsesalvo que exista un métodotyping()/markRead()(antes tampoco tenían efecto), y emite timestamps en ms epoch.RateLimitedError.retryAfterMstiene un tope de 24 h.- Las encuestas de Telegram se envían no anónimas para que sus votos lleguen al bot
como eventos
'poll'.new TelegramProvider({ anonymousPolls: true })mantiene las encuestas anónimas. - Las ediciones de Discord se entregan como mensajes con el
ref.idoriginal; un handler que no deba verlas compruebaisEditedMessage(ctx)o usaeditedMessages: 'ignore'. - Helpers renombrados (los nombres antiguos son alias deprecados hasta v3):
pollTimeoutde Telegram (segundos) →pollTimeoutMs;toProviderError→toTelegramError,toTikTokError;toEmailProviderError→toEmailError; eltoProviderErrorde@botmesh/meta→toGraphErrorde@botmesh/graph-core. Discord, Messenger e Instagram quitaron sutoProviderErrorde las alphas antes de su primera publicación en npm: usatoDiscordError,toMessengerErrorytoInstagramError.
Siguientes pasos
- Mensajes y media: qué hace cada helper en cada transporte.
- Sandbox de plataformas: ejecuta un bot sobre los providers reales contra simuladores locales, sin cuentas.
- Servidor de webhooks: monta los providers por webhook
con
@botmesh/server. - Ventana de 24 h y costos: ventana de sesión de la Cloud API y seguimiento de uso.
- Formularios: WhatsApp Flows nativos en la Cloud API, campo a campo en el resto.