Manejador externo

El manejador externo es un puente entre tu código y Telegram. Cuando la lógica del constructor no es suficiente, delegas una reacción concreta a tu servidor: este recibe los eventos y responde con comandos en formato Telegram Bot API, mientras GetMyBot se encarga del transporte, la entrega y todo lo relacionado con Telegram.

Qué es

Las reacciones en GetMyBot se crean visualmente: disparador → condiciones → acciones. La lógica compleja o personalizada — tu propia base de datos, cálculos, llamadas a sistemas externos, ML, escenarios con muchas ramas — es difícil de expresar en el constructor. El manejador externo elimina ese techo: conectas tu propio backend y le delegas reacciones completas para que las procese.

El servicio externo recibe de GetMyBot los eventos (se activó un disparador, el usuario respondió con texto, pulsó un botón) y a su vez ordena al bot qué enviar. GetMyBot ejecuta los comandos con su propio token de bot — por encima funcionan el rate-limit, los reintentos, la deduplicación y el registro del diálogo.

Modelo de funcionamiento

  • GetMyBot es el servidor WebSocket. Tu servicio se conecta a él y no necesita una dirección pública ni un endpoint de webhook. GetMyBot no realiza conexiones salientes a tu servicio, por lo que no hay superficie de SSRF.
  • Autenticación mediante token de integración. El servicio presenta el token al conectarse; GetMyBot lo compara con el hash y mantiene la conexión.
  • Eventos y comandos en formato Telegram Bot API. El servicio recibe un evento y responde con un array de llamadas del tipo «método + parámetros», exactamente igual que al trabajar con un bot normal.
  • Los comandos los ejecuta GetMyBot con su token. El servicio nunca se comunica directamente con Telegram: los comandos pasan por validación y entran en el outbox interno de GetMyBot, desde donde se envían con el token del bot con rate-limit, reintentos, deduplicación y registro.

Una sola conexión atiende todos los diálogos de una integración — los eventos de distintos usuarios se multiplexan y se distinguen por el campo session_id.

Configuración de la integración

  1. Abre la sección Integraciones en el menú izquierdo y pulsa Añadir integración.
  2. Selecciona el servicio Manejador externo y dale un nombre.
  3. Tras crear la integración, la tarjeta mostrará:
    • Dirección WebSocket — del tipo wss://api.mybot.app/ext/ws/<integrationID>, donde <integrationID> es el UUID de esta integración. Tu servicio se conecta a esta dirección. Sin el id en la ruta la conexión no se establece.
    • Token de conexión — se muestra una sola vez al crearse. Cópialo y guárdalo en un lugar seguro; no es posible verlo de nuevo (se almacena solo como hash).
    • Regenerar token — el botón genera un nuevo token e invalida el anterior de inmediato. Tras la regeneración, actualiza el token en tu servicio.
    • Indicador de estado online — muestra si tu servicio mantiene una conexión activa en este momento.

Parámetros adicionales de la integración que afectan al protocolo:

  • session_ttl_seconds — tiempo de vida de la sesión en segundos (por defecto 3600).
  • on_unavailable_reaction_id — reacción de respaldo que se lanza si el servicio está desconectado en el momento de la activación (ver Fiabilidad).

Vinculación a una reacción

El manejador externo no se conecta como un disparador independiente, sino como una acción. En el editor de la reacción, añade la acción Transferir al manejador externo y selecciona la integración correspondiente.

El diálogo puede abrirse con cualquier disparador existente de GetMyBot — comando, texto, pulsación de botón, parámetro desde un enlace, solicitud web entrante, programación. Cuando ese disparador se activa y llega a la acción, GetMyBot abre una sesión proxy y envía al servicio el evento session.open.

Si en ese momento el servicio no está conectado, GetMyBot lanzará la reacción de respaldo configurada en la integración (on_unavailable_reaction_id). Si no se ha definido ninguna reacción de respaldo, no ocurrirá nada (silenciosamente, sin error para el usuario).

Conexión y autorización

Endpoint: GET /ext/ws/{integrationID} sobre WebSocket (wss). integrationID es el UUID de la integración de tipo «Manejador externo». El límite de tamaño de trama es 256 KiB.

Puedes autenticarte de una de estas dos formas.

1. Cabecera en el handshake (preferido). Pasa el token en la cabecera Authorization:

Authorization: Bearer <token>

2. Trama de autenticación. Si no se ha enviado la cabecera Bearer, envía como primera trama en los 10 segundos siguientes al establecimiento de la conexión:

{ "type": "auth", "token": "<token>" }

Si en 10 segundos no llega la trama de autenticación, la conexión se cierra con el código 4401. Un token incorrecto en cualquiera de los métodos también provoca el cierre 4401.

El token se genera al crear la integración, se muestra una sola vez y se almacena solo como hash (sha256); la verificación es en tiempo constante. Rotación del token:

POST /api/bots/{botID}/integrations/{integrationID}/rotate-token

El endpoint devuelve el nuevo token en texto plano; el anterior deja de funcionar de inmediato.

Eventos: bot → servicio

Cada evento es una trama JSON (EventEnvelope). El campo type es uno de: session.open, message, callback, session.cancel, session.expired.

Conjunto completo de campos del envelope:

{
  "type": "session.open",
  "session_id": "string",
  "bot_id": "string",
  "user": {
    "tg_user_id": 123456789,
    "first_name": "Ivan",
    "username": "ivan",
    "params": { "utm": "promo" }
  },
  "chat": { "id": 123456789, "type": "private" },
  "update": { "...": "raw Telegram Update (JSON sin procesar)" },
  "event": { "...": "evento normalizado de GetMyBot (JSON sin procesar)" }
}
  • session_id — identificador de la sesión (diálogo).
  • bot_id — identificador del bot.
  • user — datos del usuario: tg_user_id (int64), opcionales first_name, username, y params — parámetros del usuario (incluyendo los del enlace), mapa clave→valor.
  • chat — chat: id (int64) y type (por ejemplo "private").
  • update — raw Telegram Update (JSON sin procesar). Presente en session.open, message, callback.
  • event — evento normalizado de GetMyBot (JSON sin procesar).

El contenido varía según el tipo:

  • session.open — se activó el disparador, se abrió un nuevo diálogo. Incluye update (raw Update) y event (evento normalizado completo), user.params está relleno.
  • message — llega cuando el servicio «mantiene una espera» (expect = text/any) y el usuario envió un mensaje. El contenido es igual que el de session.open.
  • callback — el usuario pulsó un botón que el servicio envió anteriormente. update es el raw; en user solo está relleno tg_user_id; event contiene solo el callback_data original definido por el servicio en el botón:
{ "callback_data": "<valor original definido por el servicio en el botón>" }
  • session.cancel / session.expired — envelope mínimo: session_id, bot_id, user.tg_user_id, chat.id (en cancel también chat.type). Los campos update/event no están presentes. Estos eventos se entregan solo si el servicio está online — no se añaden a la cola offline.

Comandos: servicio → bot

Un comando es una trama JSON (CommandEnvelope). El campo type es uno de: execute, session.close, auth, ping, pong.

{
  "type": "execute",
  "session_id": "string",
  "methods": [
    { "method": "sendMessage", "params": { "text": "Hola" } }
  ],
  "expect": "none"
}
  • session_id — la sesión a la que pertenece el comando.
  • token — solo para type: "auth" (ver Conexión y autorización).
  • methods — array de objetos { "method": "<nombre del método de Telegram Bot API>", "params": { ... } }. El nombre del método es exactamente el de Telegram Bot API (por ejemplo sendMessage); params es el objeto de parámetros de ese método.
  • expect — si el servicio espera respuesta del usuario: "none", "text" o "any" (un valor vacío se interpreta como none).
  • session.close — cierra explícitamente el diálogo.

Los comandos no van directamente a Telegram: GetMyBot los valida y los coloca en su outbox, desde donde los envía con el token del bot (con rate-limit, reintentos, deduplicación y registro).

Cómo se procesa execute

El comando execute se descarta por completo si: session_id está vacío; la sesión no se encuentra o no está en estado open; la sesión pertenece a otra integración; la sesión ha expirado.

Después:

  • Rate-limit: hasta 60 execute por minuto por sesión (ventana deslizante). Por encima del límite se descartan.
  • Hasta 30 métodos en un solo execute; los sobrantes se descartan silenciosamente.
  • Cada método se verifica contra la lista blanca (ver Métodos permitidos) — un método que no esté en la lista se rechaza.
  • Chat-guard: si en params hay chat_id o from_chat_id con un valor distinto de 0 y distinto del chat_id de la sesión, todo el método se rechaza (no se puede enviar a un chat ajeno). chat_id puede omitirse: GetMyBot asignará forzosamente el chat de la sesión.
  • Botones inline con callback_data se tokenizan automáticamente (formato interno x:<token>); el tiempo de vida del botón es hasta que expire la sesión. Los botones url/webapp/switch_inline no se modifican.

Métodos permitidos

El servicio controla el bot mediante comandos, por lo que el conjunto de métodos está limitado por una lista blanca — solo envío y gestión de mensajes del diálogo actual. Los métodos que no están en la lista se ignoran silenciosamente (no es un error).

  • envío: sendMessage, sendPhoto, sendDocument, sendVideo, sendAudio, sendMediaGroup, sendAnimation, sendVoice, sendLocation, sendChatAction;
  • edición y eliminación: editMessageText, editMessageCaption, editMessageReplyMarkup, deleteMessage;
  • respuesta a pulsación: answerCallbackQuery;
  • reenvío y copia: forwardMessage, copyMessage;
  • fijación: pinChatMessage, unpinChatMessage.

Los métodos a nivel de cuenta (setWebhook, getUpdates, logOut, close, setMyCommands, etc.) no están permitidos.

Espera y ciclo de vida de la sesión

Tras cada execute, el destino de la sesión depende de expect y de si el comando contenía botones inline con callback_data:

  • expect = text o any — el siguiente mensaje del usuario se reenviará al servicio como evento message.
  • expect = none Y el comando NO tenía botones inline con callback — la sesión se cierra automáticamente. Este es un mensaje «terminal».
  • expect = none, PERO hay botones con callback — la sesión permanece abierta: las pulsaciones se capturan mientras vivan los tokens de los botones (hasta el TTL).

Importante: expect no afecta a la recepción de pulsaciones de botones. Las pulsaciones de botones tokenizados se capturan independientemente del valor de expect — mientras la sesión esté viva y el token del botón no haya expirado. expect solo controla si el servicio espera una respuesta de texto o arbitraria.

El contexto del diálogo vive exactamente mientras la sesión esté abierta:

  • Respuesta de texto con espera activa (expect = text/any) — va al servicio como evento message, en lugar de activar las reacciones normales del bot.
  • Pulsación de botón tokenizado — va al servicio como evento callback con el callback_data original. GetMyBot siempre confirma la pulsación por su cuenta (answerCallbackQuery); se verifica la pertenencia de la pulsación (bot + usuario + chat de la sesión). La pulsación de un botón no cierra la sesión.
  • /cancel (también /cancel@bot y «cancelar») con una sesión externa activa — la sesión se cierra, se envía session.cancel al servicio y se envía «Cancelado.» al usuario.
  • Un diálogo por usuario. Abrir una nueva sesión desplaza la sesión abierta anterior de ese usuario — a esta se le envía session.cancel.
  • Expiración del TTL. Cuando el temporizador expira, la sesión se marca como expired y (si el servicio está online) se envía session.expired al servicio.

Límites y tiempos de espera

  • Tamaño de trama: 256 KiB.
  • TTL de sesión: por defecto 3600 segundos (configurable con el parámetro session_ttl_seconds de la integración).
  • Frecuencia de comandos: 60 execute por minuto por sesión (ventana deslizante).
  • Métodos en un solo execute: hasta 30 (los sobrantes se descartan).
  • Sesiones abiertas por integración: hasta 1000.
  • Cola offline: hasta 100 eventos por sesión.
  • Heartbeat: ping cada 30 segundos; tiempo de espera por inactividad — 60 segundos.
  • Tiempo de espera de la trama de autenticación: 10 segundos.

Fiabilidad (heartbeat, offline, cola, fallback)

  • Heartbeat. El servidor envía una trama {"type":"ping"} cada 30 segundos. Si no hay actividad del cliente durante más de 60 segundos, la conexión se cierra. El cliente puede enviar sus propios ping/pong para mantener la actividad. Una desconexión no mata los diálogos: las sesiones viven en la base de datos hasta el TTL o la reconexión.
  • La reconexión es responsabilidad del servicio. Si la conexión se interrumpe, tu servicio debe reconectarse. Las sesiones abiertas en la base de datos sobreviven a los reinicios hasta que expire el TTL.
  • Fallback cuando está offline al inicio. Si en el momento de activarse la reacción el servicio no está conectado, se lanza la reacción de respaldo configurada en la integración (on_unavailable_reaction_id). Si no está definida, no ocurre nada (silenciosamente).
  • Cola offline. Los eventos message/callback que no se pudieron entregar por estar offline se almacenan en la cola offline (hasta 100 eventos, FIFO, se conserva hasta que expire la sesión) y se entregan al reconectarse. Los eventos session.open/session.cancel/session.expired no se almacenan en la cola.
  • Garantía de entrega — at-most-once. Si la cola se desborda o caduca, los eventos se descartan. Diseña tu lógica de forma que la pérdida de un evento concreto no rompa el escenario.

Indicador de estado online

Para verificar si el servicio mantiene una conexión activa, usa el endpoint:

GET /api/bots/{botID}/integrations/{integrationID}/status

Respuesta:

{ "online": true }

El mismo indicador está disponible en la tarjeta de integración en el panel. El equivalente en MCP es la herramienta get_integration_status.

Seguridad

GetMyBot ejecuta los comandos de un servicio externo con su propio token, por lo que la protección es estricta.

  • Sin superficie de SSRF. GetMyBot actúa como servidor WebSocket y no realiza conexiones salientes al servicio — es el servicio quien se conecta a GetMyBot. No existe ninguna URL controlada externamente a la que la plataforma acceda.
  • El token se almacena como hash (sha256), la verificación es en tiempo constante y se admite la rotación.
  • Aislamiento por inquilino. Un comando está estrictamente vinculado a su integración; la sesión, al bot + usuario + chat. Los eventos de bots ajenos no llegan a tu socket.
  • Lista blanca de métodos + chat_id forzado impiden convertir el bot en un remitente masivo a chats arbitrarios.
  • El token del bot no se expone externamente — el servicio nunca se comunica directamente con Telegram.
  • Enmascaramiento de secretos. El token de integración y los valores sensibles se ocultan en el registro y en los logs.

Ejemplos

Manejador mínimo: al abrir el diálogo envía un mensaje y cierra la sesión (expect: "none"). Sustituye el id de integración y el token. La dirección debe contener obligatoriamente <integrationID> en la ruta.

Node.js

import WebSocket from "ws";

const URL = `wss://api.mybot.app/ext/ws/${process.env.MYBOT_INTEGRATION_ID}`;
const ws = new WebSocket(URL, {
  headers: { Authorization: `Bearer ${process.env.MYBOT_TOKEN}` },
});

ws.on("message", (raw) => {
  const ev = JSON.parse(raw);
  if (ev.type === "session.open") {
    ws.send(JSON.stringify({
      type: "execute",
      session_id: ev.session_id,
      expect: "none",
      methods: [{ method: "sendMessage", params: { text: "Hola desde el manejador externo!" } }],
    }));
  }
});

Python

import json, os, asyncio, websockets

async def main():
    url = f"wss://api.mybot.app/ext/ws/{os.environ['MYBOT_INTEGRATION_ID']}"
    headers = {"Authorization": f"Bearer {os.environ['MYBOT_TOKEN']}"}
    async with websockets.connect(url, additional_headers=headers) as ws:
        async for raw in ws:
            ev = json.loads(raw)
            if ev["type"] == "session.open":
                await ws.send(json.dumps({
                    "type": "execute",
                    "session_id": ev["session_id"],
                    "expect": "none",
                    "methods": [
                        {"method": "sendMessage", "params": {"text": "Hola desde el manejador externo!"}}
                    ],
                }))

asyncio.run(main())

Botón inline y manejo de callback

Para capturar una pulsación, envía un botón con callback_data (GetMyBot lo tokeniza automáticamente). Con expect: "none" y un botón, la sesión permanece abierta mientras viva el token del botón — la pulsación llegará como evento callback en el que event.callback_data es igual al valor original. No es necesario confirmar la pulsación (answerCallbackQuery) — GetMyBot lo hace por su cuenta.

if (ev.type === "session.open") {
  ws.send(JSON.stringify({
    type: "execute",
    session_id: ev.session_id,
    expect: "none",
    methods: [{
      method: "sendMessage",
      params: {
        text: "Pulsa el botón",
        reply_markup: { inline_keyboard: [[{ text: "Vamos", callback_data: "go" }]] },
      },
    }],
  }));
} else if (ev.type === "callback" && ev.event.callback_data === "go") {
  ws.send(JSON.stringify({
    type: "execute",
    session_id: ev.session_id,
    expect: "none",
    methods: [{ method: "sendMessage", params: { text: "¡Botón pulsado!" } }],
  }));
}

Qué sigue