# Telegram Bot API — Documentación para Desarrolladores · Erando Ecosystem V8

**Versión:** 2.0  
**Fecha:** 3 de junio de 2026  
**API objetivo:** Telegram Bot API 10.0 (mayo 2026)  
**Stack del proyecto:** NestJS 11 · Keycloak 26 · BullMQ 5 · SurrealDB 2.x  
**Fuente primaria:** [https://core.telegram.org/bots/api](https://core.telegram.org/bots/api)

---

## Resumen / Abstract

**Objetivo:** Documentar la integración completa de la Telegram Bot API en el ecosistema Erando V8, proporcionando a los desarrolladores una referencia técnica unificada que cubra métodos, tipos de objetos, mecanismos de obtención de actualizaciones, manejo de archivos, gestión de errores y límites de tasa.

**Método:** Investigación documental sobre la Bot API oficial (versión 10.0, mayo 2026), los tipos TypeScript de `@telegraf/types` v9.2.1, la librería `telegraf` (9.2k ⭐), el wrapper `nestjs-telegraf` (631 ⭐) y fuentes comunitarias verificadas (Zernio, Gramio.dev, python-telegram-bot). La información fue contrastada contra al menos dos fuentes independientes; las discrepancias se documentan explícitamente.

**Hallazgos clave:** (a) La API expone ~100 métodos HTTP con respuestas JSON estructuradas bajo el envoltorio `{ ok, result }`. (b) Los webhooks son el mecanismo recomendado para entornos productivos con escalado horizontal, requiriendo HTTPS y ofreciendo `secret_token` desde la versión 6.1. (c) El límite de tasa es ~1 msg/s por chat y ~30 msg/s global, con bloqueo total del bot ante HTTP 429. (d) La integración con NestJS se realiza mediante `nestjs-telegraf`, que expone decoradores para comandos, mensajes y callbacks. (e) La arquitectura propuesta para Erando canaliza los webhooks a través de BullMQ hacia workers que persisten en SurrealDB, con autenticación delegada en Keycloak.

**Conclusión:** La Telegram Bot API es madura y adecuada para el ecosistema Erando. Los puntos críticos de diseño son la elección de webhooks sobre long polling, la implementación de backoff exponencial ante rate limits, y la separación asíncrona entre recepción de updates y procesamiento de negocio mediante BullMQ.

---

## 1. Introducción a la API de Telegram Bots

### 1.1 ¿Qué es un Bot de Telegram?

Un bot de Telegram es una cuenta especial operada por software —no por una persona— que interactúa con usuarios, grupos y canales a través de la Telegram Bot API. Los bots se crean mediante [@BotFather](https://t.me/BotFather), que entrega un **token de acceso** único con el formato `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`. Este token autentica todas las llamadas HTTP a `https://api.telegram.org/bot<token>/<método>`.

> ⚠️ **Nota:** El token es equivalente a una contraseña. Si se filtra, cualquier persona puede controlar el bot. Almacenarlo exclusivamente en variables de entorno (`{{BOT_TOKEN}}`) y nunca en código fuente ni repositorios.

### 1.2 Modelo HTTP de la API

La Telegram Bot API es una interfaz **HTTPS pura** sin capas de autenticación adicionales:

- **Endpoint base:** `https://api.telegram.org/bot{{BOT_TOKEN}}/`
- **Método HTTP:** `POST` para todas las operaciones (aunque algunos métodos simples toleran `GET`).
- **Formato:** `application/json` para datos, `multipart/form-data` para archivos.
- **Respuesta:** Siempre JSON con el envoltorio `{ ok: boolean, result?: T, description?: string, error_code?: number, parameters?: { retry_after?: number, migrate_to_chat_id?: number } }`.

> 💡 **Tip:** En Erando, todo consumo de la API debe pasar por un servicio `TelegramClientService` que inyecte el token desde `ConfigService` y normalice las respuestas a través del envoltorio `ApiResponse<T>`.

### 1.3 Objetivos del Proyecto Erando Ecosystem V8

**Objetivo general:** Construir una plataforma de mensajería multicanal sobre NestJS que exponga bots de Telegram como un canal más del ecosistema, integrado con Keycloak para autenticación/autorización, BullMQ para procesamiento asíncrono y SurrealDB como base de datos multi-modelo.

**Objetivos específicos:**

1. **OE-1:** Implementar un webhook NestJS que reciba updates de Telegram, valide el `secret_token` contra Keycloak, y encole el mensaje en BullMQ en menos de 200 ms para liberar la conexión entrante.
2. **OE-2:** Diseñar workers BullMQ que procesen mensajes, comandos y callbacks aplicando lógica de negocio, con persistencia de estado conversacional en SurrealDB.
3. **OE-3:** Modelar en SurrealDB las entidades `bot`, `user`, `chat`, `message` y `callback` con relaciones trazables entre el usuario de Keycloak y el `telegram_user_id`.
4. **OE-4:** Implementar backoff exponencial y circuit breaker para llamadas a la API de Telegram respetando los límites de tasa (~1 msg/s por chat, ~30 msg/s global).
5. **OE-5:** Soportar subida y descarga de archivos (fotos, documentos, audio) con integración al CDN de Erando y caché de `file_id` en SurrealDB.
6. **OE-6:** Proveer handlers tipados para todos los tipos de update (mensajes, comandos, callbacks, inline queries, pagos) usando las interfaces TypeScript de `@telegraf/types`.

---

## 2. Métodos Disponibles (Reference)

### 2.1 Mensajes de Texto

| Método | Endpoint | Descripción | Límite |
|--------|----------|-------------|--------|
| `sendMessage` | `POST /bot{{BOT_TOKEN}}/sendMessage` | Envía mensaje de texto simple o formateado | 4096 caracteres |
| `forwardMessage` | `POST /bot{{BOT_TOKEN}}/forwardMessage` | Reenvía mensaje de otro chat | — |
| `forwardMessages` | `POST /bot{{BOT_TOKEN}}/forwardMessages` | Reenvía múltiples mensajes (API 7.0+) | — |
| `copyMessage` | `POST /bot{{BOT_TOKEN}}/copyMessage` | Copia mensaje sin header de forward | — |
| `copyMessages` | `POST /bot{{BOT_TOKEN}}/copyMessages` | Copia múltiples mensajes (API 7.0+) | — |

#### `sendMessage` — Referencia completa

**Parámetros**

| Parámetro | Tipo | Requerido | Descripción |
|-----------|------|-----------|-------------|
| `chat_id` | `integer \| string` | Sí | ID del chat destino o @username |
| `text` | `string` | Sí | Texto del mensaje (1-4096 caracteres) |
| `parse_mode` | `string` | No | `"MarkdownV2"`, `"HTML"` o `"Markdown"` (legacy) |
| `entities` | `MessageEntity[]` | No | Entidades de formato (excluyente con `parse_mode`) |
| `link_preview_options` | `LinkPreviewOptions` | No | Personalización del link preview (API 7.0+) |
| `disable_notification` | `boolean` | No | Envía silenciosamente |
| `protect_content` | `boolean` | No | Protege contra forward/captura |
| `allow_paid_broadcast` | `boolean` | No | Permite broadcast de pago (0.1 Stars/msg, hasta 1000 msg/s) |
| `message_thread_id` | `integer` | No | ID del hilo en supergrupos con topics |
| `reply_parameters` | `ReplyParameters` | No | Parámetros de respuesta (API 7.0+) |
| `reply_markup` | `InlineKeyboardMarkup \| ReplyKeyboardMarkup \| ReplyKeyboardRemove \| ForceReply` | No | Teclado inline, teclado de reemplazo, o forzar respuesta |
| `business_connection_id` | `string` | No | Para enviar en nombre de un Business Connection |
| `reply_to_message_id` | `integer` | No | ⚠️ **Deprecado** en API 7.0 — usar `reply_parameters` |

**Ejemplo curl**

```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/sendMessage \
  -H "Content-Type: application/json" \
  -d '{
    "chat_id": {{CHAT_ID}},
    "text": "Hola desde Erando Ecosystem V8 🚀",
    "parse_mode": "MarkdownV2",
    "disable_notification": true,
    "reply_markup": {
      "inline_keyboard": [
        [{"text": "✅ Confirmar", "callback_data": "confirmar_accion"}],
        [{"text": "🌐 Docs", "url": "https://erando.io/docs"}]
      ]
    }
  }'
```

**Modelo de respuesta TypeScript**

```typescript
// ApiResponse<Message> — el método devuelve el Message creado
interface SendMessageResponse {
  ok: true;
  result: {
    message_id: number;
    from: User;
    chat: Chat;
    date: number;
    text: string;
    entities?: MessageEntity[];
    reply_markup?: InlineKeyboardMarkup;
  };
}

// En caso de error
interface SendMessageError {
  ok: false;
  error_code: 400 | 403 | 429;
  description: string;
  parameters?: { retry_after?: number };
}
```

> 💡 **Tip para Erando:** Encapsular `sendMessage` en un método `telegramClient.sendMessage(chatId, text, opts)` que acepte el DTO de Erando y traduzca automáticamente `parse_mode` según el canal. Cachear `{{CHAT_ID}}` en SurrealDB para evitar resoluciones repetidas.

---

### 2.2 Mensajería Multimedia

| Método | Endpoint | Descripción | Tamaño máx. subida |
|--------|----------|-------------|---------------------|
| `sendPhoto` | `POST /bot{{BOT_TOKEN}}/sendPhoto` | Envía foto (JPEG, PNG, WEBP) | 10 MB |
| `sendAudio` | `POST /bot{{BOT_TOKEN}}/sendAudio` | Envía audio (MP3, M4A) | 50 MB |
| `sendDocument` | `POST /bot{{BOT_TOKEN}}/sendDocument` | Envía documento genérico | 50 MB |
| `sendVideo` | `POST /bot{{BOT_TOKEN}}/sendVideo` | Envía video (MP4, H.264) | 50 MB |
| `sendAnimation` | `POST /bot{{BOT_TOKEN}}/sendAnimation` | Envía animación/GIF | 50 MB |
| `sendVoice` | `POST /bot{{BOT_TOKEN}}/sendVoice` | Envía nota de voz (OGG) | 50 MB |
| `sendVideoNote` | `POST /bot{{BOT_TOKEN}}/sendVideoNote` | Envía video circular | — |
| `sendMediaGroup` | `POST /bot{{BOT_TOKEN}}/sendMediaGroup` | Envía álbum de 2-10 ítems | 50 MB por ítem |
| `sendSticker` | `POST /bot{{BOT_TOKEN}}/sendSticker` | Envía sticker (WEBP, WEBM) | — |
| `sendPaidMedia` | `POST /bot{{BOT_TOKEN}}/sendPaidMedia` | Envía media de pago (Stars) | — |

#### `sendPhoto` — Referencia completa

**Parámetros**

| Parámetro | Tipo | Requerido | Descripción |
|-----------|------|-----------|-------------|
| `chat_id` | `integer \| string` | Sí | ID del chat destino |
| `photo` | `InputFile \| string` | Sí | Foto como `file_id`, URL o multipart |
| `caption` | `string` | No | Leyenda (0-1024 caracteres) |
| `parse_mode` | `string` | No | Formato de la leyenda |
| `caption_entities` | `MessageEntity[]` | No | Entidades en la leyenda |
| `show_caption_above_media` | `boolean` | No | Mostrar caption sobre la foto |
| `has_spoiler` | `boolean` | No | Cubrir con efecto spoiler |
| `disable_notification` | `boolean` | No | Notificación silenciosa |
| `protect_content` | `boolean` | No | Proteger contra forward |
| `allow_paid_broadcast` | `boolean` | No | Broadcast de pago |
| `reply_parameters` | `ReplyParameters` | No | Respuesta a mensaje |
| `reply_markup` | `InlineKeyboardMarkup \| ...` | No | Teclado adjunto |

**Ejemplo curl (subida directa con multipart/form-data)**

```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/sendPhoto \
  -F "chat_id={{CHAT_ID}}" \
  -F "photo=@/ruta/local/imagen.jpg" \
  -F "caption=Foto procesada por Erando" \
  -F "parse_mode=HTML"
```

**Ejemplo curl (por file_id, sin re-subir)**

```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/sendPhoto \
  -H "Content-Type: application/json" \
  -d '{
    "chat_id": {{CHAT_ID}},
    "photo": "AgACAgIAAxkBAAIB...file_id",
    "caption": "Foto cacheada desde CDN de Erando"
  }'
```

> 💡 **Tip para Erando:** Una vez subido un archivo, Telegram devuelve un `file_id` persistente. Almacenarlo en SurrealDB asociado al `asset_id` del CDN de Erando permite envíos instantáneos sin re-subir. Ver sección 5 para estrategia de caché.

**Modelo de respuesta TypeScript**

```typescript
// El Message devuelto incluye el array photo con todos los tamaños
interface PhotoMessage {
  message_id: number;
  from: User;
  chat: Chat;
  date: number;
  photo: PhotoSize[];    // último elemento = tamaño original
  caption?: string;
  caption_entities?: MessageEntity[];
  has_media_spoiler?: boolean;
}
```

---

### 2.3 Edición y Eliminación de Mensajes

| Método | Endpoint | Descripción |
|--------|----------|-------------|
| `editMessageText` | `POST /bot{{BOT_TOKEN}}/editMessageText` | Edita texto de un mensaje existente |
| `editMessageCaption` | `POST /bot{{BOT_TOKEN}}/editMessageCaption` | Edita caption de media |
| `editMessageMedia` | `POST /bot{{BOT_TOKEN}}/editMessageMedia` | Reemplaza media de un mensaje |
| `editMessageReplyMarkup` | `POST /bot{{BOT_TOKEN}}/editMessageReplyMarkup` | Solo actualiza el teclado inline |
| `editMessageLiveLocation` | `POST /bot{{BOT_TOKEN}}/editMessageLiveLocation` | Actualiza ubicación en vivo |
| `stopMessageLiveLocation` | `POST /bot{{BOT_TOKEN}}/stopMessageLiveLocation` | Detiene ubicación en vivo |
| `deleteMessage` | `POST /bot{{BOT_TOKEN}}/deleteMessage` | Elimina un mensaje |
| `deleteMessages` | `POST /bot{{BOT_TOKEN}}/deleteMessages` | Elimina múltiples mensajes (API 7.0+) |
| `deleteAllMessageReactions` | `POST /bot{{BOT_TOKEN}}/deleteAllMessageReactions` | Elimina todas las reacciones (API 10.0) |
| `deleteMessageReaction` | `POST /bot{{BOT_TOKEN}}/deleteMessageReaction` | Elimina reacción específica (API 10.0) |

> ⚠️ **Nota:** `deleteMessage` solo funciona para mensajes enviados hace **menos de 48 horas** (*Telegram Bot API*, 2026; *Postman Telegram Collection*, 2025). En Erando, programar un job BullMQ con `delay` controlado si la eliminación es diferida.

#### `editMessageText` — curl

```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/editMessageText \
  -H "Content-Type: application/json" \
  -d '{
    "chat_id": {{CHAT_ID}},
    "message_id": 42,
    "text": "Mensaje *editado* desde Erando",
    "parse_mode": "MarkdownV2"
  }'
```

#### `deleteMessage` — curl

```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/deleteMessage \
  -d "chat_id={{CHAT_ID}}" \
  -d "message_id=42"
```

---

### 2.4 Chats, Grupos y Administración

| Método | Endpoint | Nota Erando |
|--------|----------|-------------|
| `getMe` | `POST /bot{{BOT_TOKEN}}/getMe` | Verificar conectividad y token al iniciar |
| `getChat` | `POST /bot{{BOT_TOKEN}}/getChat` | Obtener metadata del chat |
| `getChatAdministrators` | `POST /bot{{BOT_TOKEN}}/getChatAdministrators` | API 10.0: +parámetro `return_bots` |
| `getChatMemberCount` | `POST /bot{{BOT_TOKEN}}/getChatMemberCount` | — |
| `getChatMember` | `POST /bot{{BOT_TOKEN}}/getChatMember` | Verificar membresía de un usuario |
| `leaveChat` | `POST /bot{{BOT_TOKEN}}/leaveChat` | — |
| `banChatMember` | `POST /bot{{BOT_TOKEN}}/banChatMember` | — |
| `unbanChatMember` | `POST /bot{{BOT_TOKEN}}/unbanChatMember` | — |
| `restrictChatMember` | `POST /bot{{BOT_TOKEN}}/restrictChatMember` | — |
| `promoteChatMember` | `POST /bot{{BOT_TOKEN}}/promoteChatMember` | — |
| `setChatAdministratorCustomTitle` | `POST /bot{{BOT_TOKEN}}/setChatAdministratorCustomTitle` | — |
| `setChatPermissions` | `POST /bot{{BOT_TOKEN}}/setChatPermissions` | — |
| `setChatTitle` | `POST /bot{{BOT_TOKEN}}/setChatTitle` | — |
| `setChatDescription` | `POST /bot{{BOT_TOKEN}}/setChatDescription` | — |
| `pinChatMessage` | `POST /bot{{BOT_TOKEN}}/pinChatMessage` | — |
| `unpinChatMessage` | `POST /bot{{BOT_TOKEN}}/unpinChatMessage` | — |
| `createChatInviteLink` | `POST /bot{{BOT_TOKEN}}/createChatInviteLink` | — |
| `editChatInviteLink` | `POST /bot{{BOT_TOKEN}}/editChatInviteLink` | — |
| `revokeChatInviteLink` | `POST /bot{{BOT_TOKEN}}/revokeChatInviteLink` | — |
| `approveChatJoinRequest` | `POST /bot{{BOT_TOKEN}}/approveChatJoinRequest` | — |
| `declineChatJoinRequest` | `POST /bot{{BOT_TOKEN}}/declineChatJoinRequest` | — |
| `setChatPhoto` | `POST /bot{{BOT_TOKEN}}/setChatPhoto` | — |
| `deleteChatPhoto` | `POST /bot{{BOT_TOKEN}}/deleteChatPhoto` | — |

> 💡 **Tip para Erando:** `getMe` es el método ideal para un health-check al iniciar el `TelegramClientService`. Si `getMe` falla con 401, el token es inválido y el módulo no debe arrancar.

---

### 2.5 Comandos, Inline Mode, Callbacks y Pagos

**Comandos y acciones de chat**

| Método | Uso |
|--------|-----|
| `sendChatAction` | Muestra "typing…", "upload_photo…", "record_voice…", etc. |
| `setMyCommands` | Registra comandos del bot visibles en el menú |
| `getMyCommands` | Lista comandos registrados |
| `deleteMyCommands` | Elimina comandos registrados |
| `setMyName` / `getMyName` | Nombre del bot en un idioma específico |
| `setMyDescription` / `getMyDescription` | Descripción del bot |
| `setMyShortDescription` / `getMyShortDescription` | Descripción corta |

**Inline Mode**

| Método | Uso |
|--------|-----|
| `answerInlineQuery` | Responde una consulta inline con resultados |
| `answerWebAppQuery` | Envía resultado desde una Web App inline |
| `savePreparedInlineMessage` | Guarda mensaje inline preparado |
| `setChatMenuButton` | Configura botón de menú en chats privados |

**Callbacks**

| Método | Uso |
|--------|-----|
| `answerCallbackQuery` | Responde un callback (muestra toast/alert) |

**Pagos y Telegram Stars**

| Método | Uso |
|--------|-----|
| `sendInvoice` | Envía factura de pago |
| `createInvoiceLink` | Crea link de pago |
| `answerShippingQuery` | Responde consulta de envío |
| `answerPreCheckoutQuery` | Confirma pre-checkout |
| `createStarTransactionLink` | Crea link de transacción Stars |
| `refundStarPayment` | Reembolsa pago en Stars |
| `getStarTransactions` | Lista transacciones Stars |

**Guest Mode (API 10.0)**

| Método | Uso |
|--------|-----|
| `answerGuestQuery` | Responde a un usuario en modo invitado |

**Stickers, Emoji y Reacciones**

| Método | Uso |
|--------|-----|
| `sendSticker` | Envía sticker |
| `getStickerSet` | Obtiene un pack de stickers |
| `uploadStickerFile` | Sube archivo de sticker |
| `createNewStickerSet` | Crea pack nuevo |
| `addStickerToSet` / `deleteStickerFromSet` | Gestión de stickers en pack |
| `setStickerPositionInSet` / `setStickerEmojiList` / `setStickerKeywords` / `setStickerMaskPosition` | Configuración de stickers |
| `setCustomEmojiStickerSetThumbnail` | Thumbnail de pack |
| `setMessageReaction` | Reacciona a un mensaje (API 7.0+) |

> **Fuente:** Lista completa verificada contra `api_methods.txt` del repositorio `telegram-bot-rb/telegram-bot` (Bot API 8.2) más el changelog oficial hasta API 10.0 (*telegram-bot-rb*, 2025; *Telegram Bot API Changelog*, 2026).

---

## 3. Tipos de Objetos de Telegram (TypeScript Interfaces)

### 3.1 `Update`

Representa una actualización entrante. Contiene **exactamente un** campo opcional presente, que indica el tipo de update. `@telegraf/types` modela esto como uniones discriminadas (`Update.MessageUpdate`, `Update.CallbackQueryUpdate`, etc.) para type narrowing.

```typescript
interface Update {
  update_id: number;
  message?: Message;
  edited_message?: Message;
  channel_post?: Message;
  edited_channel_post?: Message;
  business_connection?: BusinessConnection;
  business_message?: Message;
  edited_business_message?: Message;
  deleted_business_messages?: BusinessMessagesDeleted;
  message_reaction?: MessageReactionUpdated;
  message_reaction_count?: MessageReactionCountUpdated;
  inline_query?: InlineQuery;
  chosen_inline_result?: ChosenInlineResult;
  callback_query?: CallbackQuery;
  shipping_query?: ShippingQuery;
  pre_checkout_query?: PreCheckoutQuery;
  purchased_paid_media?: PaidMediaPurchased;
  poll?: Poll;
  poll_answer?: PollAnswer;
  my_chat_member?: ChatMemberUpdated;
  chat_member?: ChatMemberUpdated;
  chat_join_request?: ChatJoinRequest;
  chat_boost?: ChatBoostUpdated;
  removed_chat_boost?: ChatBoostRemoved;
  guest_message?: SentGuestMessage;  // API 10.0+
}
```

### 3.2 `Message`

```typescript
interface Message {
  message_id: number;
  message_thread_id?: number;
  from?: User;
  sender_chat?: Chat;
  sender_boost_count?: number;
  sender_business_bot?: User;
  date: number;                        // Unix timestamp
  business_connection_id?: string;
  chat: Chat;
  forward_origin?: MessageOrigin;
  is_topic_message?: boolean;
  is_automatic_forward?: boolean;
  reply_to_message?: Message;
  external_reply?: ExternalReplyInfo;   // API 7.0+
  quote?: TextQuote;                   // API 7.0+
  reply_to_story?: Story;
  via_bot?: User;
  edit_date?: number;
  has_protected_content?: boolean;
  is_from_offline?: boolean;
  media_group_id?: string;
  author_signature?: string;
  text?: string;
  entities?: MessageEntity[];
  link_preview_options?: LinkPreviewOptions; // API 7.0+
  effect_id?: string;
  animation?: Animation;
  audio?: Audio;
  document?: Document;
  paid_media?: PaidMediaInfo;
  photo?: PhotoSize[];
  sticker?: Sticker;
  story?: Story;
  video?: Video;
  video_note?: VideoNote;
  voice?: Voice;
  caption?: string;
  caption_entities?: MessageEntity[];
  has_media_spoiler?: boolean;
  contact?: Contact;
  dice?: Dice;
  game?: Game;
  poll?: Poll;
  venue?: Venue;
  location?: Location;
  new_chat_members?: User[];
  left_chat_member?: User;
  new_chat_title?: string;
  new_chat_photo?: PhotoSize[];
  delete_chat_photo?: boolean;
  group_chat_created?: boolean;
  supergroup_chat_created?: boolean;
  channel_chat_created?: boolean;
  message_auto_delete_timer_changed?: MessageAutoDeleteTimerChanged;
  migrate_to_chat_id?: number;
  migrate_from_chat_id?: number;
  pinned_message?: Message;
  invoice?: Invoice;
  successful_payment?: SuccessfulPayment;
  refunded_payment?: RefundedPayment;
  users_shared?: UsersShared;
  chat_shared?: ChatShared;
  connected_website?: string;
  write_access_allowed?: WriteAccessAllowed;
  passport_data?: PassportData;
  proximity_alert_triggered?: ProximityAlertTriggered;
  boost_added?: ChatBoostAdded;
  chat_background_set?: ChatBackground;
  forum_topic_created?: ForumTopicCreated;
  forum_topic_edited?: ForumTopicEdited;
  forum_topic_closed?: ForumTopicClosed;
  forum_topic_reopened?: ForumTopicReopened;
  general_forum_topic_hidden?: GeneralForumTopicHidden;
  general_forum_topic_unhidden?: GeneralForumTopicUnhidden;
  giveaway_created?: GiveawayCreated;
  giveaway?: Giveaway;
  giveaway_winners?: GiveawayWinners;
  giveaway_completed?: GiveawayCompleted;
  video_chat_scheduled?: VideoChatScheduled;
  video_chat_started?: VideoChatStarted;
  video_chat_ended?: VideoChatEnded;
  video_chat_participants_invited?: VideoChatParticipantsInvited;
  web_app_data?: WebAppData;
  reply_markup?: InlineKeyboardMarkup;
  guest_bot_caller_user?: User;        // API 10.0+
  guest_bot_caller_chat?: Chat;        // API 10.0+
  guest_query_id?: string;             // API 10.0+
}
```

### 3.3 `User`

```typescript
interface User {
  id: number;
  is_bot: boolean;
  first_name: string;
  last_name?: string;
  username?: string;
  language_code?: string;
  is_premium?: boolean;
  added_to_attachment_menu?: boolean;
  can_join_groups?: boolean;
  can_read_all_group_messages?: boolean;
  supports_inline_queries?: boolean;
  can_connect_to_business?: boolean;
  has_main_web_app?: boolean;
  supports_guest_queries?: boolean;    // API 10.0+
}
```

### 3.4 `Chat`

```typescript
interface Chat {
  id: number;
  type: "private" | "group" | "supergroup" | "channel";
  title?: string;
  username?: string;
  first_name?: string;
  last_name?: string;
  is_forum?: boolean;
  photo?: ChatPhoto;
  active_usernames?: string[];
  available_reactions?: ReactionType[];
  accent_color_id?: number;
  background_custom_emoji_id?: string;
  profile_accent_color_id?: number;
  profile_background_custom_emoji_id?: string;
  emoji_status_custom_emoji_id?: string;
  emoji_status_expiration_date?: number;
  bio?: string;
  has_private_forwards?: boolean;
  has_restricted_voice_and_video_messages?: boolean;
  join_to_send_messages?: boolean;
  join_by_request?: boolean;
  description?: string;
  invite_link?: string;
  pinned_message?: Message;
  permissions?: ChatPermissions;
  slow_mode_delay?: number;
  unrestrict_boost_count?: number;
  message_auto_delete_time?: number;
  has_aggressive_anti_spam_enabled?: boolean;
  has_hidden_members?: boolean;
  has_protected_content?: boolean;
  has_visible_history?: boolean;
  sticker_set_name?: string;
  can_set_sticker_set?: boolean;
  custom_emoji_sticker_set_name?: string;
  linked_chat_id?: number;
  location?: ChatLocation;
}
```

### 3.5 `CallbackQuery`

```typescript
interface CallbackQuery {
  id: string;
  from: User;
  message?: Message;                    // Mensaje del bot que originó el callback
  inline_message_id?: string;           // Si fue desde modo inline
  chat_instance: string;                // Identificador global de chat
  data?: string;                        // callback_data del botón pulsado
  game_short_name?: string;             // Solo uno de data o game_short_name está presente
}
```

> **Fuente:** Exactamente uno de `data` o `game_short_name` está presente, confirmado por *python-telegram-bot v21.9* (2024).

### 3.6 `InlineQuery`

```typescript
interface InlineQuery {
  id: string;
  from: User;
  query: string;
  offset: string;
  chat_type?: "sender" | "private" | "group" | "supergroup" | "channel";
  location?: Location;
}
```

### 3.7 Keyboard Markups

```typescript
// ── Inline Keyboard ──
interface InlineKeyboardMarkup {
  inline_keyboard: InlineKeyboardButton[][];
}

interface InlineKeyboardButton {
  text: string;
  url?: string;
  callback_data?: string;
  web_app?: WebAppInfo;
  login_url?: LoginUrl;
  switch_inline_query?: string;
  switch_inline_query_current_chat?: string;
  switch_inline_query_chosen_chat?: SwitchInlineQueryChosenChat;
  callback_game?: CallbackGame;
  pay?: boolean;
}

// ── Reply Keyboard (reemplaza teclado del usuario) ──
interface ReplyKeyboardMarkup {
  keyboard: KeyboardButton[][];
  is_persistent?: boolean;
  resize_keyboard?: boolean;
  one_time_keyboard?: boolean;
  input_field_placeholder?: string;
  selective?: boolean;
}

// ── Reply Keyboard Remove ──
interface ReplyKeyboardRemove {
  remove_keyboard: true;
  selective?: boolean;
}

// ── Force Reply ──
interface ForceReply {
  force_reply: true;
  input_field_placeholder?: string;
  selective?: boolean;
}
```

### 3.8 `ReplyParameters` (API 7.0+, reemplaza `reply_to_message_id`)

```typescript
interface ReplyParameters {
  message_id: number;
  chat_id?: number | string;
  allow_sending_without_reply?: boolean;
  quote?: string;
  quote_parse_mode?: string;
  quote_entities?: MessageEntity[];
  quote_position?: number;
}
```

### 3.9 `MessageEntity`

```typescript
interface MessageEntity {
  type: "mention" | "hashtag" | "cashtag" | "bot_command" | "url" | "email"
    | "phone_number" | "bold" | "italic" | "underline" | "strikethrough"
    | "spoiler" | "blockquote" | "expandable_blockquote" | "code" | "pre"
    | "text_link" | "text_mention" | "custom_emoji";
  offset: number;
  length: number;
  url?: string;
  user?: User;
  language?: string;
  custom_emoji_id?: string;
}
```

### 3.10 `LinkPreviewOptions` (API 7.0+, reemplaza `disable_web_page_preview`)

```typescript
interface LinkPreviewOptions {
  is_disabled?: boolean;
  url?: string;
  prefer_small_media?: boolean;
  prefer_large_media?: boolean;
  show_above_text?: boolean;
}
```

### 3.11 `ApiResponse<T>` — Envoltorio universal

```typescript
interface ApiResponse<T> {
  ok: boolean;
  result?: T;
  description?: string;
  error_code?: number;
  parameters?: {
    retry_after?: number;
    migrate_to_chat_id?: number;
  };
}
```

> **Fuente de tipos:** `@telegraf/types` v9.2.1 en npm, que rastrea Bot API v9.2 (*npm: @telegraf/types*, 2025). Complementado con el changelog oficial hasta API 10.0 (*Telegram Bot API Changelog*, 2026).

> ⚠️ **Nota:** `@telegraf/types` no contiene código ejecutable — es puramente definiciones de tipos TypeScript. Al momento de esta investigación, la versión 9.2.1 rastrea API 9.2, no 10.0. Los campos marcados como API 10.0+ (`guest_message`, `supports_guest_queries`, etc.) provienen del cruce con el changelog oficial y deben verificarse contra la siguiente versión del paquete cuando se publique.

---

## 4. Obtención de Actualizaciones: Webhooks vs Long Polling

### 4.1 Mecanismos de Recepción

| Método | Endpoint | Descripción |
|--------|----------|-------------|
| `getUpdates` | `POST /bot{{BOT_TOKEN}}/getUpdates` | Long polling: el bot consulta updates pendientes |
| `setWebhook` | `POST /bot{{BOT_TOKEN}}/setWebhook` | Webhook: Telegram envía updates vía POST |
| `deleteWebhook` | `POST /bot{{BOT_TOKEN}}/deleteWebhook` | Elimina la configuración del webhook |
| `getWebhookInfo` | `POST /bot{{BOT_TOKEN}}/getWebhookInfo` | Obtiene estado actual del webhook |

### 4.2 Comparativa

| Aspecto | Long Polling | Webhook |
|---------|-------------|---------|
| **Inicio de conexión** | Bot → Telegram (outbound) | Telegram → Bot (inbound) |
| **URL pública** | No requerida | **HTTPS requerido** (SSL válido) |
| **Certificado** | No aplica | Self-signed requiere upload |
| **Latencia** | Depende del timeout (25-30s) | Instantánea (push) |
| **Tráfico idle** | Peticiones vacías periódicas | Sin tráfico |
| **Escalado horizontal** | ❌ Una sola instancia | ✅ Múltiples réplicas con load balancer |
| **Serverless** | ❌ Incompatible | ✅ Vercel, AWS Lambda, Cloudflare Workers |
| **Desarrollo local** | ✅ Ideal | Requiere túnel (ngrok) o local API server |

### 4.3 Limitación Crítica de Long Polling

Telegram **rechaza** llamadas concurrentes a `getUpdates` con el mismo bot token (HTTP 409 Conflict). Esto implica:

- Solo **una instancia** de bot puede estar activa con long polling.
- Kubernetes con múltiples réplicas requiere webhook.
- Para alta disponibilidad en long polling, se necesita coordinación externa (Redis lock, etc.).

> **Fuente:** *Gramio.dev* (2026) documenta específicamente el error HTTP 409 y recomienda webhook para producción.

### 4.4 Configuración de Webhook Recomendada para NestJS

```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/setWebhook \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://erando-api.example.com/telegram/webhook",
    "secret_token": "{{WEBHOOK_SECRET_TOKEN}}",
    "max_connections": 40,
    "allowed_updates": ["message", "callback_query", "inline_query"],
    "drop_pending_updates": true
  }'
```

**Parámetros clave:**

| Parámetro | Tipo | Descripción |
|-----------|------|-------------|
| `url` | `string` | **Requerido.** URL HTTPS donde Telegram hará POST. |
| `secret_token` | `string` | Token de 1-256 caracteres (A-Z, a-z, 0-9, `_`, `-`). Se recibe en el header `X-Telegram-Bot-Api-Secret-Token`. Introducido en API 6.1. |
| `max_connections` | `integer` | 1-100, default 40. Número de conexiones HTTPS simultáneas desde Telegram. |
| `allowed_updates` | `string[]` | Filtra tipos de update. Vacío = todos. |
| `drop_pending_updates` | `boolean` | Descarta updates pendientes al activar. Útil en despliegues. |
| `ip_address` | `string` | Fija la IP saliente desde la que Telegram envía webhooks. |
| `certificate` | `InputFile` | Solo para certificados self-signed (no recomendado en producción). |

### 4.5 Implementación con NestJS + @nestjs/telegraf

**Instalación**

```bash
npm i nestjs-telegraf telegraf
```

**Módulo raíz con webhook**

```typescript
import { Module } from "@nestjs/common";
import { TelegrafModule } from "nestjs-telegraf";
import { ConfigModule, ConfigService } from "@nestjs/config";

@Module({
  imports: [
    ConfigModule.forRoot(),
    TelegrafModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        token: config.get<string>("BOT_TOKEN"),
        launchOptions: {
          webhook: {
            domain: config.get<string>("BOT_DOMAIN"),
            path: "/telegram/webhook",
            secretToken: config.get<string>("WEBHOOK_SECRET_TOKEN"),
            port: 443,
          },
          dropPendingUpdates: true,
        },
      }),
    }),
  ],
})
export class AppModule {}
```

**Handler de updates con decoradores**

```typescript
import { Command, Ctx, Update, Message } from "nestjs-telegraf";
import { Context } from "telegraf";

@Update()
export class BotUpdate {
  @Command("start")
  async onStart(@Ctx() ctx: Context) {
    await ctx.reply("¡Bienvenido al bot de Erando!");
  }

  @Command("help")
  async onHelp(@Ctx() ctx: Context) {
    await ctx.reply("Comandos disponibles: /start, /help, /echo");
  }

  @Message("text")
  async onMessage(@Ctx() ctx: Context, @Message("text") text: string) {
    await ctx.reply(`Recibido: ${text}`);
  }
}
```

> ⚠️ **Nota:** El repositorio `nestjs-telegraf` (631 ⭐, 99 forks) tiene documentación limitada. Issues conocidos: #1279 (enlace docs roto), #1338 (resolución de dominio), #1316 (tipado de sesión). El chat de comunidad está en [t.me/nestjs_telegraf](https://t.me/nestjs_telegraf).

> 💡 **Tip para Erando:** No usar `@Update()` para lógica de negocio compleja. Los decoradores de `nestjs-telegraf` deben ser una capa delgada que extraiga `chat_id`, `user_id`, `text` o `callback_data` y encole un job en BullMQ. La lógica de negocio reside en los workers.

### 4.6 Mejores Prácticas de Webhook

1. **Validar `secret_token`** — Comparar el header `X-Telegram-Bot-Api-Secret-Token` contra el valor configurado. Rechazar con 401 si no coincide.
2. **Responder 200 OK inmediatamente** — Telegram espera una respuesta en un plazo breve. Procesar en background (BullMQ) para no bloquear la entrega.
3. **Configurar `max_connections`** — Un valor bajo limita concurrencia pero protege el servidor. Para Erando, empezar con 10-20 y escalar según carga.
4. **Usar `drop_pending_updates: true`** en despliegues para evitar procesar updates huérfanos acumulados.
5. **IP saliente fija** — Usar `ip_address` en `setWebhook` si el firewall de Erando requiere whitelisting.

---

## 5. Manejo de Archivos y Multimedia

### 5.1 Tres Formas de Enviar Archivos

La API admite tres modalidades para el envío de archivos (*Telegram Bot API*, 2026):

| Modalidad | Formato | Uso recomendado |
|-----------|---------|-----------------|
| **file_id** | `string` en JSON | Archivos ya subidos a Telegram — instantáneo, no consume ancho de banda |
| **URL pública** | `string` en JSON | Archivos accesibles en internet — Telegram los descarga |
| **Multipart upload** | `multipart/form-data` | Subida directa desde el servidor de Erando |

### 5.2 Límites de Tamaño

| Tipo de archivo | Subida máx. (API estándar) | Subida máx. (Local Bot API) | Descarga máx. |
|-----------------|---------------------------|----------------------------|---------------|
| Fotos | 10 MB | 2 GB | 20 MB |
| Audio | 50 MB | 2 GB | 20 MB |
| Documentos | 50 MB | 2 GB | 20 MB |
| Video | 50 MB | 2 GB | 20 MB |
| Animaciones/GIF | 50 MB | 2 GB | 20 MB |
| Notas de voz | 50 MB | 2 GB | 20 MB |

> ⚠️ **Nota:** La API estándar solo permite descargar archivos de hasta **20 MB**. Archivos mayores requieren Local Bot API Server (*n8n Community*, 2025; *tdlib/telegram-bot-api#583*, 2024).

### 5.3 Estrategia de Caché de file_id para Erando

```mermaid
sequenceDiagram
    participant E as Erando Worker
    participant S as SurrealDB
    participant T as Telegram API
    participant C as CDN Erando

    E->>S: Buscar file_id por asset_id
    alt file_id cacheado y válido
        S-->>E: file_id
        E->>T: sendPhoto(file_id)
        T-->>E: Message { photo: [...] }
    else sin caché
        E->>C: Obtener archivo del CDN
        C-->>E: Buffer
        E->>T: sendPhoto(multipart)
        T-->>E: Message { photo: [{file_id, ...}] }
        E->>S: Guardar file_id ↔ asset_id
    end
```

> 💡 **Tip:** Los `file_id` de Telegram son persistentes y no expiran. Una vez cacheado el mapeo `asset_id → file_id` en SurrealDB, todos los envíos posteriores del mismo asset son instantáneos y no consumen ancho de banda de subida.

### 5.4 Subida de Archivos con curl (referencia)

**Multipart (subida directa)**

```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/sendDocument \
  -F "chat_id={{CHAT_ID}}" \
  -F "document=@/ruta/local/reporte.pdf" \
  -F "caption=Reporte generado por Erando"
```

**Por file_id (reutilización)**

```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/sendDocument \
  -H "Content-Type: application/json" \
  -d '{"chat_id": {{CHAT_ID}}, "document": "BQACAgIAAxkBAAIB...file_id", "caption": "Documento cacheado"}'
```

### 5.5 Descarga de Archivos

El método `getFile` devuelve un objeto `File` con `file_path`. La URL de descarga se construye como:

```
https://api.telegram.org/file/bot{{BOT_TOKEN}}/<file_path>
```

> ⚠️ **Nota:** El `file_path` expira en aproximadamente **1 hora**. Si se necesita acceso prolongado, descargar y almacenar en el CDN de Erando inmediatamente.

```bash
# 1. Obtener file_path
curl https://api.telegram.org/bot{{BOT_TOKEN}}/getFile?file_id=AgACAgIAAxkBAAIB...

# 2. Descargar
curl -o archivo_descargado.jpg \
  https://api.telegram.org/file/bot{{BOT_TOKEN}}/photos/file_42.jpg
```

### 5.6 Local Bot API Server para Archivos Grandes

Para archivos > 50 MB (subida) o > 20 MB (descarga), Telegram recomienda el **Local Bot API Server** (*BigMike.help*, 2026):

- Soporta subidas de hasta **2 GB**.
- Descarga sin límite de 20 MB.
- Se ejecuta como contenedor Docker independiente.
- El bot debe configurarse para usar el servidor local en lugar de `api.telegram.org`.

```bash
docker run -d \
  -e TELEGRAM_API_ID={{API_ID}} \
  -e TELEGRAM_API_HASH={{API_HASH}} \
  -p 8081:8081 \
  ghcr.io/tdlib/telegram-bot-api:latest
```

> **Limitación actual:** Esta capacidad no ha sido verificada experimentalmente en el alcance de esta investigación. Se documenta como opción de escalado futuro para Erando.

---

## 6. Errores, Códigos de Estado HTTP y Límites de Tasa

### 6.1 Tabla de Errores HTTP

| Código | Error | Categoría | Descripción |
|--------|-------|-----------|-------------|
| **400** | `Bad Request` | Genérico | Parámetros inválidos, chat no encontrado, file_id inválido, mensaje > 4096 chars |
| **401** | `Unauthorized` | Autenticación | Token inválido o expirado |
| **403** | `Forbidden` | Chat | Bot bloqueado (`bot_blocked`), expulsado (`bot_kicked`), sin permisos (`chat_write_forbidden`, `not_enough_rights`) |
| **404** | `Not Found` | Chat | `chat_not_found`: el chat no existe o el bot no es miembro |
| **409** | `Conflict` | Polling | Dos instancias haciendo `getUpdates` simultáneamente |
| **429** | `Too Many Requests` | Rate Limit | `flood_control`, `too_many_requests`. `parameters.retry_after` indica segundos de espera |
| **500** | `Internal Server Error` | Servidor | Error del lado de Telegram. Reintentar con backoff. |

> **Fuente:** Los códigos de error están verificados en al menos 3 fuentes independientes: Zernio Error Reference (*Zernio*, 2025), Gramio.dev rate limits (*Gramio.dev*, 2026), y discusiones en GitHub de aiogram (*aiogram#1422*, 2024).

### 6.2 Errores por Categoría

**Autenticación y Bloqueo** (*Zernio*, 2025)

| Error | Significado | Acción recomendada |
|-------|-------------|-------------------|
| `bot_blocked` | Usuario bloqueó al bot | Notificar al usuario que desbloquee |
| `bot_kicked` | Bot expulsado del chat | Re-agregar como administrador |
| `unauthorized` | Token inválido | Verificar con @BotFather |
| `user_deactivated` | Usuario eliminó su cuenta | Eliminar de la base de datos de Erando |

**Chat y Permisos** (*Zernio*, 2025)

| Error | Significado | Acción recomendada |
|-------|-------------|-------------------|
| `chat_not_found` | Chat no existe o bot no es miembro | Verificar `chat_id` |
| `chat_write_forbidden` | Sin permisos de envío | Solicitar permisos al admin |
| `not_enough_rights` | Sin derechos de administrador | Promover al bot |
| `message_cant_be_deleted` | Mensaje > 48h | No reintentar; loguear |

**Rate Limiting** (*Gramio.dev*, 2026)

| Error | Significado |
|-------|-------------|
| `flood_control` | Excedido límite por chat (~1 msg/s) |
| `too_many_requests` | Excedido límite global (~30 msg/s) |
| `retry_after` (parámetro) | Segundos de espera obligatorios |

**Media y Contenido** (*Zernio*, 2025)

| Error | Significado |
|-------|-------------|
| `file_too_large` | Archivo excede límite (fotos 10 MB, docs/video 50 MB) |
| `wrong_file_type` | Formato no soportado o archivo corrupto |
| `photo_invalid` | Imagen corrupta o formato inválido (usar JPEG/PNG) |
| `video_invalid` | Video corrupto o codec no soportado (usar MP4 H.264 + AAC) |
| `message_too_long` | Texto > 4096 caracteres |
| `caption_too_long` | Caption > 1024 caracteres |
| `media_group_invalid` | Álbum debe tener 2-10 ítems del mismo tipo |

### 6.3 Formato de Error en JSON

**Error estándar:**
```json
{
  "ok": false,
  "error_code": 429,
  "description": "Too Many Requests: retry after 35",
  "parameters": {
    "retry_after": 35
  }
}
```

**Error con migración de chat (supergrupo):**
```json
{
  "ok": false,
  "error_code": 400,
  "description": "Bad Request: group chat was upgraded to a supergroup chat",
  "parameters": {
    "migrate_to_chat_id": -1001234567890
  }
}
```

### 6.4 Límites de Tasa (Rate Limits)

| Límite | Valor aproximado | Fuente |
|--------|-----------------|--------|
| Mensajes al **mismo chat** | ~1 por segundo | Gramio.dev, 2026 |
| Mensajes a **diferentes chats** | ~30 por segundo | Gramio.dev, 2026 |
| Broadcast masivo | ~30 por segundo total | Gramio.dev, 2026 |
| Broadcast de **pago** (`allow_paid_broadcast`) | Hasta **1000 por segundo** (0.1 Stars/msg) | Gramio.dev, 2026 |
| `getUpdates` (long polling) | No debería devolver 429 en operación normal | telegraf#1563, 2021 |

> ⚠️ **Nota:** Telegram **no publica oficialmente** los límites exactos. Los valores son aproximados y dinámicos, basados en la antigüedad e historial del bot (*Gramio.dev*, 2026). Para throughput mayor, contactar [@BotSupport](https://t.me/BotSupport).

### 6.5 Comportamiento ante HTTP 429

Cuando un bot excede el límite:
1. Telegram devuelve HTTP 429 con `retry_after` (ej: 5, 35 segundos).
2. **Todas** las llamadas API del bot quedan bloqueadas durante ese período — no solo las del chat que causó el límite.
3. Se recomienda implementar **exponential backoff** con jitter.

### 6.6 Implicaciones para BullMQ en Erando

```mermaid
flowchart TD
    A[Worker toma job de enviar mensaje] --> B{¿Rate limiter permite?}
    B -->|Sí| C[POST sendMessage a Telegram]
    B -->|No| D[Job pausado hasta ventana disponible]
    C --> E{¿Respuesta?}
    E -->|200 ok| F[Job completado]
    E -->|429 retry_after| G[Calcular delay = retry_after + jitter]
    G --> H[Job.moveToDelayed delay]
    H --> A
    E -->|4xx permanente| I[Job movido a failed]
    E -->|5xx| J[Retry con backoff exponencial]
    J --> A
```

> 💡 **Tip:** Implementar un `RateLimiterService` que mantenga contadores en Redis/Memcached por `chat_id` (1/s) y global (30/s). Los jobs de BullMQ consultan este servicio antes de hacer la llamada HTTP. Si no hay cupo, el job se reencola con `delay` calculado. No usar BullMQ `rateLimiter` nativo porque la ventana es por chat_id, no global.

---

## 7. Integración con el Ecosistema Erando

### 7.1 Arquitectura de Capas

```mermaid
graph TD
    subgraph "Capa de Entrada"
        WEBHOOK[NestJS Webhook Controller]
    end

    subgraph "Capa de Autenticación"
        KC[Keycloak 26]
    end

    subgraph "Capa de Mensajería"
        QUEUE_IN[BullMQ Queue: telegram:incoming]
        QUEUE_OUT[BullMQ Queue: telegram:outgoing]
    end

    subgraph "Capa de Procesamiento"
        WORKER_IN[Worker: MessageProcessor]
        WORKER_CMD[Worker: CommandProcessor]
        WORKER_CB[Worker: CallbackProcessor]
        WORKER_OUT[Worker: OutboundSender]
    end

    subgraph "Capa de Persistencia"
        SURREAL[SurrealDB 2.x]
    end

    subgraph "Externo"
        TG[Telegram Bot API]
        CDN[Erando CDN]
    end

    TG -->|HTTPS POST| WEBHOOK
    WEBHOOK -->|valida secret_token| KC
    WEBHOOK -->|encola Update| QUEUE_IN
    QUEUE_IN --> WORKER_IN
    QUEUE_IN --> WORKER_CMD
    QUEUE_IN --> WORKER_CB
    WORKER_IN --> SURREAL
    WORKER_CMD --> SURREAL
    WORKER_CB --> SURREAL
    WORKER_IN -->|encola respuesta| QUEUE_OUT
    WORKER_CMD -->|encola respuesta| QUEUE_OUT
    WORKER_CB -->|encola respuesta| QUEUE_OUT
    QUEUE_OUT --> WORKER_OUT
    WORKER_OUT -->|RateLimiter check| WORKER_OUT
    WORKER_OUT --> TG
    WORKER_OUT --> SURREAL
    WORKER_IN --> CDN
    WORKER_OUT --> CDN
```

### 7.1.1 Flujo de Ingesta Webhook (Vista Simplificada)

```mermaid
flowchart TD
    TG["Telegram Bot API"] -->|"HTTPS POST /webhook"| WH["NestJS Webhook Controller"]
    WH -->|"1. Validar secret_token"| WH
    WH -->|"2. Responder 200 OK"| TG
    WH -->|"3. Encolar Update"| Q["BullMQ Queue: telegram:incoming"]
    Q -->|"4. Desencolar job"| W["Worker: MessageProcessor"]
    W -->|"5. Persistir datos"| S[("SurrealDB 2.x")]

    style TG fill:#2AABEE,stroke:#1A8BCD,color:#fff
    style WH fill:#E0234E,stroke:#BF1B3F,color:#fff
    style Q fill:#F5A623,stroke:#D4891A,color:#fff
    style W fill:#7B61FF,stroke:#5E45D6,color:#fff
    style S fill:#FF6B6B,stroke:#E05555,color:#fff
```

**Interpretación:** El diagrama muestra el camino crítico de ingesta: Telegram envía el update vía POST, el controlador NestJS valida el `secret_token` (rechazando con 401 si no coincide), responde **200 OK en menos de 200 ms** para cumplir el OE-1, y encola el update en BullMQ de forma asíncrona. El worker desencola y persiste en SurrealDB. Esta separación entre recepción sincrónica y procesamiento asíncrono es el pilar de escalabilidad horizontal del ecosistema Erando.

> **Fuente:** Diseño basado en la arquitectura definida en §7.1 y el controlador conceptual de §7.4.

### 7.2 Flujo Completo: de Webhook a Base de Datos

```mermaid
sequenceDiagram
    participant U as Usuario Telegram
    participant TG as Telegram API
    participant WH as Webhook Controller (NestJS)
    participant KC as Keycloak
    participant Q as BullMQ (telegram:incoming)
    participant W as Worker (MessageProcessor)
    participant S as SurrealDB
    participant Q2 as BullMQ (telegram:outgoing)
    participant WO as Worker (OutboundSender)

    U->>TG: Envía mensaje
    TG->>WH: POST /telegram/webhook<br/>Header: X-Telegram-Bot-Api-Secret-Token
    WH->>WH: Validar secret_token
    alt token inválido
        WH-->>TG: 401 Unauthorized
    else token válido
        WH->>KC: Validar realm/client
        KC-->>WH: OK
        WH->>Q: add("telegram:incoming", { update, receivedAt })
        WH-->>TG: 200 OK (inmediato)
        Q->>W: process(job)
        W->>S: SELECT user WHERE telegram_user_id = update.message.from.id
        alt usuario no existe
            W->>S: CREATE user SET telegram_user_id = ..., first_name = ...
        end
        W->>S: CREATE message SET message_id = ..., chat_id = ..., text = ...
        W->>W: Procesar lógica de negocio
        W->>Q2: add("telegram:outgoing", { chat_id, text, reply_markup })
        Q2->>WO: process(job)
        WO->>WO: RateLimiter.check(chat_id)
        WO->>TG: POST sendMessage
        TG-->>WO: 200 OK { message_id }
        WO->>S: UPDATE message SET telegram_message_id = ...
    end
```

### 7.3 Esquema SurrealDB (Propuesta Base)

#### 7.3.0 Diagrama Entidad-Relación

```mermaid
graph TD
    subgraph "SurrealDB — Erando Ecosystem V8"
        user["user<br/>─────────<br/>keycloak_user_id (PK)<br/>telegram_user_id<br/>telegram_username<br/>first_name<br/>last_name<br/>created_at"]
        user_group["user_group<br/>─────────<br/>group_id (PK)<br/>name<br/>description<br/>created_at"]
        user_fact["user_fact<br/>─────────<br/>fact_id (PK)<br/>user_id (FK)<br/>key<br/>value<br/>confidence<br/>recorded_at"]
        reminder["reminder<br/>─────────<br/>reminder_id (PK)<br/>user_id (FK)<br/>title<br/>description<br/>scheduled_at<br/>status<br/>category_id (FK)"]
        category["category<br/>─────────<br/>category_id (PK)<br/>name<br/>color<br/>icon<br/>parent_category_id"]
        knowledge_index["knowledge_index<br/>─────────<br/>index_id (PK)<br/>user_id (FK)<br/>content<br/>embedding<br/>category_id (FK)<br/>indexed_at"]
    end

    user -->|"belongs_to"| user_group
    user -->|"has_fact"| user_fact
    user -->|"creates"| reminder
    user -->|"owns"| knowledge_index
    reminder -->|"has_category"| category
    knowledge_index -->|"has_category"| category
    category -->|"parent_of"| category

    style user fill:#E0234E,color:#fff
    style user_group fill:#2AABEE,color:#fff
    style user_fact fill:#F5A623,color:#fff
    style reminder fill:#7B61FF,color:#fff
    style category fill:#FF6B6B,color:#fff
    style knowledge_index fill:#4ECDC4,color:#fff
```

**Interpretación:** El modelo de datos de Erando se organiza en torno al `user` (vinculado a Keycloak), que se agrupa en `user_group`, acumula hechos en `user_fact`, crea `reminder` con categorías, y alimenta el índice de conocimiento (`knowledge_index`) con embeddings para búsqueda semántica RAG. Las relaciones `has_category` permiten filtrar recordatorios y documentos del knowledge index por categoría temática. La categoría tiene auto-referencia (`parent_of`) para jerarquías anidadas.

> **Fuente:** Diseño conceptual derivado de los objetivos OE-2 y OE-3 (§1.3) y el esquema base definido en esta misma sección.

```sql
-- ── Definición de tablas ──
DEFINE TABLE bot SCHEMAFULL;
DEFINE FIELD telegram_bot_id ON bot TYPE int;
DEFINE FIELD bot_token_encrypted ON bot TYPE string;
DEFINE FIELD bot_username ON bot TYPE string;
DEFINE FIELD created_at ON bot TYPE datetime DEFAULT time::now();

DEFINE TABLE erando_user SCHEMAFULL;
DEFINE FIELD keycloak_user_id ON erando_user TYPE string;
DEFINE FIELD telegram_user_id ON erando_user TYPE option<int>;
DEFINE FIELD telegram_username ON erando_user TYPE option<string>;
DEFINE FIELD first_name ON erando_user TYPE string;
DEFINE FIELD last_name ON erando_user TYPE option<string>;
DEFINE FIELD created_at ON erando_user TYPE datetime DEFAULT time::now();

DEFINE TABLE telegram_chat SCHEMAFULL;
DEFINE FIELD chat_id ON telegram_chat TYPE int;
DEFINE FIELD chat_type ON telegram_chat TYPE string ASSERT $value IN ["private", "group", "supergroup", "channel"];
DEFINE FIELD title ON telegram_chat TYPE option<string>;
DEFINE FIELD username ON telegram_chat TYPE option<string>;
DEFINE FIELD erando_user_id ON telegram_chat TYPE option<record<erando_user>>;

DEFINE TABLE telegram_message SCHEMAFULL;
DEFINE FIELD message_id ON telegram_message TYPE int;
DEFINE FIELD chat_id ON telegram_message TYPE int;
DEFINE FIELD from_user_id ON telegram_message TYPE option<int>;
DEFINE FIELD text ON telegram_message TYPE option<string>;
DEFINE FIELD entities ON telegram_message TYPE option<array>;
DEFINE FIELD sent_at ON telegram_message TYPE datetime DEFAULT time::now();

DEFINE TABLE telegram_callback SCHEMAFULL;
DEFINE FIELD callback_id ON telegram_callback TYPE string;
DEFINE FIELD from_user_id ON telegram_callback TYPE int;
DEFINE FIELD data ON telegram_callback TYPE string;
DEFINE FIELD message_id ON telegram_callback TYPE option<int>;
DEFINE FIELD processed_at ON telegram_callback TYPE option<datetime>;

DEFINE TABLE file_cache SCHEMAFULL;
DEFINE FIELD asset_id ON file_cache TYPE string;
DEFINE FIELD telegram_file_id ON file_cache TYPE string;
DEFINE FIELD file_type ON file_cache TYPE string ASSERT $value IN ["photo", "document", "audio", "video", "animation", "voice", "sticker"];
DEFINE FIELD cdn_url ON file_cache TYPE option<string>;
DEFINE FIELD cached_at ON file_cache TYPE datetime DEFAULT time::now();

-- ── Índices ──
DEFINE INDEX idx_telegram_user ON erando_user FIELDS telegram_user_id UNIQUE;
DEFINE INDEX idx_chat_id ON telegram_chat FIELDS chat_id UNIQUE;
DEFINE INDEX idx_asset_file ON file_cache FIELDS asset_id UNIQUE;
DEFINE INDEX idx_file_id ON file_cache FIELDS telegram_file_id UNIQUE;
```

> ⚠️ **Nota:** La definición `SCHEMAFULL` y el tipo `option<T>` corresponden a SurrealDB 2.x. Verificar compatibilidad exacta con la versión desplegada en el entorno de Erando. El campo `bot_token_encrypted` debe almacenarse encriptado con AES-256-GCM usando una clave gestionada por Keycloak Vault o el servicio de secretos de la infraestructura.

### 7.4 Handler de Webhook — Ejemplo Conceptual (NestJS)

```typescript
// telegram-webhook.controller.ts
import { Controller, Post, Req, Headers, Res, HttpCode, UnauthorizedException } from "@nestjs/common";
import { InjectQueue } from "@nestjs/bullmq";
import { Queue } from "bullmq";
import { Request, Response } from "express";

@Controller("telegram")
export class TelegramWebhookController {
  constructor(
    @InjectQueue("telegram:incoming") private readonly incomingQueue: Queue,
  ) {}

  @Post("webhook")
  @HttpCode(200)
  async handleWebhook(
    @Headers("x-telegram-bot-api-secret-token") secretToken: string,
    @Req() req: Request,
    @Res() res: Response,
  ) {
    // Validación inmediata del secret_token
    if (secretToken !== process.env.TELEGRAM_WEBHOOK_SECRET) {
      throw new UnauthorizedException("Invalid secret token");
    }

    // Responder 200 OK inmediatamente
    res.status(200).send("OK");

    // Encolar el update para procesamiento asíncrono
    await this.incomingQueue.add("telegram:update", {
      update: req.body,
      receivedAt: new Date().toISOString(),
    });
  }
}
```

> 💡 **Tip:** El controlador responde 200 OK **antes** de encolar para minimizar la latencia percibida por Telegram. Si el encolado falla, el update se pierde — considerar un mecanismo de dead-letter queue para este escenario.

### 7.5 Worker de Procesamiento — Ejemplo Conceptual (BullMQ)

```typescript
// message-processor.worker.ts
import { Processor, WorkerHost, OnWorkerEvent } from "@nestjs/bullmq";
import { Job } from "bullmq";

@Processor("telegram:incoming")
export class MessageProcessor extends WorkerHost {
  async process(job: Job<{ update: any; receivedAt: string }>): Promise<void> {
    const { update } = job.data;

    if (update.message) {
      await this.handleMessage(update.message);
    } else if (update.callback_query) {
      await this.handleCallback(update.callback_query);
    }
    // Otros tipos de update...
  }

  private async handleMessage(message: any): Promise<void> {
    // 1. Upsert del usuario en SurrealDB
    // 2. Insertar mensaje en SurrealDB
    // 3. Ejecutar lógica de negocio
    // 4. Encolar respuesta en telegram:outgoing si aplica
  }

  private async handleCallback(callback: any): Promise<void> {
    // 1. Registrar callback en SurrealDB
    // 2. Ejecutar lógica según callback_data
    // 3. Encolar editMessageReplyMarkup o sendMessage en telegram:outgoing
  }

  @OnWorkerEvent("failed")
  onFailed(job: Job, error: Error): void {
    // Loguear y enviar a dead-letter si es necesario
  }
}
```

### 7.6 Manejo de Rate Limits en OutboundSender

```typescript
// outbound-sender.worker.ts (fragmento conceptual)
import { Processor, WorkerHost } from "@nestjs/bullmq";
import { Job } from "bullmq";

@Processor("telegram:outgoing")
export class OutboundSender extends WorkerHost {
  async process(job: Job): Promise<void> {
    const { chatId, method, params } = job.data;

    try {
      const response = await this.telegramClient.call(method, params);
      // Persistir resultado en SurrealDB
      await this.surreal.query(
        "UPDATE telegram_message SET telegram_message_id = $msg_id WHERE id = $internal_id",
        { msg_id: response.result.message_id, internal_id: params.internalMessageId }
      );
    } catch (error) {
      if (error.response?.error_code === 429) {
        const retryAfter = error.response?.parameters?.retry_after ?? 60;
        // Reencolar con delay + jitter
        const jitter = Math.floor(Math.random() * 5000);
        await job.moveToDelayed(Date.now() + retryAfter * 1000 + jitter);
        throw new Error(`Rate limited. Retry after ${retryAfter}s`);
      }
      throw error; // Otros errores se manejan con backoff de BullMQ
    }
  }
}
```

---

## 8. Apéndices

### 8.1 Apéndice A — Compendio Curl

**Verificar bot**
```bash
curl https://api.telegram.org/bot{{BOT_TOKEN}}/getMe
```

**Enviar mensaje simple**
```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/sendMessage \
  -d "chat_id={{CHAT_ID}}" -d "text=Hola"
```

**Enviar mensaje con MarkdownV2 y teclado inline**
```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/sendMessage \
  -H "Content-Type: application/json" \
  -d '{"chat_id": {{CHAT_ID}}, "text": "Texto con *negrita*", "parse_mode": "MarkdownV2", "reply_markup": {"inline_keyboard": [[{"text": "Click", "callback_data": "click"}]]}}'
```

**Enviar foto por file_id**
```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/sendPhoto \
  -d "chat_id={{CHAT_ID}}" \
  -d "photo=AgACAgIAAxkBAAIB...file_id" \
  -d "caption=Mi foto"
```

**Enviar documento (multipart)**
```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/sendDocument \
  -F "chat_id={{CHAT_ID}}" \
  -F "document=@/path/to/file.pdf" \
  -F "caption=PDF importante"
```

**Editar mensaje**
```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/editMessageText \
  -H "Content-Type: application/json" \
  -d '{"chat_id": {{CHAT_ID}}, "message_id": 42, "text": "Editado"}'
```

**Eliminar mensaje**
```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/deleteMessage \
  -d "chat_id={{CHAT_ID}}" -d "message_id=42"
```

**Configurar webhook**
```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/setWebhook \
  -H "Content-Type: application/json" \
  -d '{"url": "https://erando-api.example.com/telegram/webhook", "secret_token": "{{WEBHOOK_SECRET_TOKEN}}", "max_connections": 40}'
```

**Obtener info de webhook**
```bash
curl https://api.telegram.org/bot{{BOT_TOKEN}}/getWebhookInfo
```

**Eliminar webhook**
```bash
curl https://api.telegram.org/bot{{BOT_TOKEN}}/deleteWebhook
```

**Responder callback query**
```bash
curl -X POST https://api.telegram.org/bot{{BOT_TOKEN}}/answerCallbackQuery \
  -d "callback_query_id=123456789012345678" -d "text=Procesado!"
```

**Descargar archivo**
```bash
# 1. Obtener file_path
curl https://api.telegram.org/bot{{BOT_TOKEN}}/getFile?file_id=AgACAgIAA...

# 2. Descargar
curl -o archivo.jpg https://api.telegram.org/file/bot{{BOT_TOKEN}}/photos/file_42.jpg
```

### 8.2 Apéndice B — Tipos TypeScript Exportables para Erando

```typescript
// ── telegram.types.ts ──
// Tipos base re-exportados desde @telegraf/types para uso interno de Erando

import type {
  Update,
  Message,
  User,
  Chat,
  CallbackQuery,
  InlineQuery,
  InlineKeyboardMarkup,
  InlineKeyboardButton,
  ReplyKeyboardMarkup,
  ReplyKeyboardRemove,
  ForceReply,
  ReplyParameters,
  MessageEntity,
  LinkPreviewOptions,
  ApiResponse,
} from "@telegraf/types";

// ── Tipos específicos de Erando ──

/** Trabajo encolado en telegram:incoming */
interface TelegramIncomingJob {
  update: Update;
  receivedAt: string;
  botId: string;
}

/** Trabajo encolado en telegram:outgoing */
interface TelegramOutgoingJob {
  chatId: number | string;
  method: "sendMessage" | "sendPhoto" | "sendDocument" | "editMessageText"
    | "editMessageReplyMarkup" | "deleteMessage" | "answerCallbackQuery";
  params: Record<string, unknown>;
  internalMessageId?: string;
}

/** Registro de archivo cacheado en SurrealDB */
interface FileCacheRecord {
  asset_id: string;
  telegram_file_id: string;
  file_type: "photo" | "document" | "audio" | "video" | "animation" | "voice" | "sticker";
  cdn_url: string | null;
  cached_at: Date;
}

// Re-exportaciones
export type {
  Update,
  Message,
  User,
  Chat,
  CallbackQuery,
  InlineQuery,
  InlineKeyboardMarkup,
  InlineKeyboardButton,
  ReplyKeyboardMarkup,
  ReplyKeyboardRemove,
  ForceReply,
  ReplyParameters,
  MessageEntity,
  LinkPreviewOptions,
  ApiResponse,
  TelegramIncomingJob,
  TelegramOutgoingJob,
  FileCacheRecord,
};
```

### 8.3 Apéndice C — Tabla de Códigos HTTP de Referencia Rápida

| Código | Significado | Acción en Erando |
|--------|-------------|------------------|
| 200 | OK | Procesar `result` |
| 400 | Bad Request | Loguear error, no reintentar (salvo `migrate_to_chat_id`) |
| 401 | Unauthorized | Alerta crítica: token inválido |
| 403 | Forbidden | Marcar chat como inaccesible en SurrealDB |
| 404 | Not Found | Verificar chat_id, marcar si es permanente |
| 409 | Conflict | Solo en long polling; migrar a webhook |
| 429 | Too Many Requests | Job.moveToDelayed(retry_after + jitter) |
| 500 | Internal Server Error | Reintentar con backoff exponencial (máx. 5 intentos) |

### 8.4 Apéndice D — Variables de Entorno Requeridas en Erando

```bash
# Telegram Bot API
BOT_TOKEN={{BOT_TOKEN}}
BOT_DOMAIN=https://erando-api.example.com
WEBHOOK_SECRET_TOKEN={{WEBHOOK_SECRET_TOKEN}}
TELEGRAM_API_URL=https://api.telegram.org

# BullMQ
REDIS_HOST=localhost
REDIS_PORT=6379
BULLMQ_CONCURRENCY_INCOMING=10
BULLMQ_CONCURRENCY_OUTGOING=5

# SurrealDB
SURREAL_URL=ws://localhost:8000/rpc
SURREAL_NAMESPACE=erando
SURREAL_DATABASE=telegram
SURREAL_USER=erando_app
SURREAL_PASS={{SURREAL_PASSWORD}}

# Keycloak
KEYCLOAK_URL=https://auth.erando.io
KEYCLOAK_REALM=erando
KEYCLOAK_CLIENT_ID=telegram-bot-service
KEYCLOAK_CLIENT_SECRET={{KEYCLOAK_CLIENT_SECRET}}

# Rate Limiter
RATE_LIMIT_PER_CHAT_MS=1000
RATE_LIMIT_GLOBAL_MS=33
RATE_LIMIT_MAX_RETRIES=5
```

---

## 9. Limitaciones y Vacíos Detectados

1. **Documentación oficial bloqueada intermitentemente:** La página principal `core.telegram.org/bots/api` devolvió HTTP 502 en el primer intento de esta investigación. El changelog sí cargó exitosamente.
2. **Falta de transparencia en rate limits:** Telegram no publica documentación oficial de límites, lo que obliga a depender de valores heurísticos comunitarios (*Gramio.dev*, 2026).
3. **@telegraf/types desactualizado respecto a API 10.0:** Al momento de esta investigación, la última versión publicada de `@telegraf/types` (9.2.1) rastrea API 9.2, no 10.0. Los campos nuevos de API 10.0 no están tipados.
4. **No se pudo verificar la lista exhaustiva de métodos de API 9.x/10.0 en una sola fuente** — se reconstruyó cruzando el archivo `api_methods.txt` (Bot API 8.2) con el changelog oficial hasta 10.0.
5. **Esquema SurrealDB no verificado:** La propuesta de esquema en la sección 7.3 es un diseño conceptual. Se requiere validación sintáctica contra la versión exacta de SurrealDB desplegada en Erando.
6. **Local Bot API Server no probado:** La capacidad de subida de archivos >50 MB y descarga >20 MB no fue verificada experimentalmente.

---

## 10. Referencias (APA 7ᵃ edición)

### Fuentes Oficiales

Telegram. (2026). *Telegram Bot API*. https://core.telegram.org/bots/api

Telegram. (2026). *Bot API Changelog*. https://core.telegram.org/bots/api-changelog

Telegram. (2026). *Bot API 10.0 – Guest Mode and more* [Mensaje en canal]. BotNews. https://t.me/s/botnews

### Librerías y Frameworks

aiogram. (2025). *Changelog – aiogram 3.21.0 documentation*. https://docs.aiogram.dev/en/v3.21.0/changelog.html

aiogram. (2025). *setWebhook – aiogram 3.16.0 documentation*. https://docs.aiogram.dev/en/v3.16.0/api/methods/set_webhook.html

Bukhalo, M. & nksmnf. (2025). *nestjs-telegraf: Powerful Nest module for easy and fast creation Telegram bots* [Repositorio de GitHub]. https://github.com/nksmnf/nestjs-telegraf

Gramio. (2026). *Long Polling vs Webhook — How Telegram Bots Receive Updates*. https://gramio.dev/updates/webhook

Gramio. (2026). *Rate limits*. https://gramio.dev/rate-limits

npm. (2025). *@telegraf/types v9.2.1*. https://www.npmjs.com/package/@telegraf/types

python-telegram-bot. (2024). *Changelog – python-telegram-bot v21.0*. https://docs.python-telegram-bot.org/en/v21.0/changelog.html

python-telegram-bot. (2025). *Changelog – python-telegram-bot v22.1*. https://docs.python-telegram-bot.org/en/v22.1/changelog.html

python-telegram-bot. (2024). *CallbackQuery – python-telegram-bot v21.9*. https://docs.python-telegram-bot.org/en/v21.9/telegram.callbackquery.html

telegraf. (2025). *telegraf: Modern Telegram Bot Framework for Node.js* [Repositorio de GitHub]. https://github.com/telegraf/telegraf

telegram-bot-rb. (2025). *api_methods.txt – Bot API 8.2* [Archivo de repositorio]. https://github.com/telegram-bot-rb/telegram-bot/blob/master/lib/telegram/bot/client/api_methods.txt

televerse. (2025). *Changelog – televerse Dart package*. https://pub.dev/packages/televerse/changelog

### Referencias Técnicas y Comunitarias

BigMike. (2026, 28 de mayo). *Self-Hosted Telegram Bot API: Run Your Own Server with Docker (2 GB upload)*. BigMike.help. https://bigmike.help/en/devops/local-telegram-bot-api-advantages-limitations-of-the-standard-api-and-set-eb4a3b/

Latenode Community. (2025, 2 de mayo). *Creating a Telegram bot: setWebhook issues*. https://community.latenode.com/t/creating-a-telegram-bot-setwebhook-issues/14761

n8n Community. (2025, 4 de noviembre). *Getting large file from telegram node "get file" in n8n problem: Bad Request: file is too big*. https://community.n8n.io/t/getting-large-file-from-telegram-node-get-file-in-n8n-problem-bad-request-file-is-too-big/216193

Postman. (2025). *deleteMessage | Telegram Bot API*. https://www.postman.com/davtur19/telegram/request/cyknn8c/deletemessage

StackOverflow. (2015, 10 de agosto). *Telegram bot api: Error code 429, Error: Too many requests: retry later*. https://stackoverflow.com/questions/31914062/telegram-bot-api-error-code-429-error-too-many-requests-retry-later

StackOverflow. (2023, 5 de diciembre). *How to send large files to user? node-telegram-bot-api*. https://stackoverflow.com/questions/77604914/how-to-send-large-files-to-user-node-telegram-bot-api

tdlib. (2024). *Does telegram-bot-api still not support uploading files up to 4G? #583* [GitHub Issue]. https://github.com/tdlib/telegram-bot-api/issues/583

Zernio. (2025). *Telegram API Error Codes Reference*. https://zernio.com/telegram/errors

---

*Documento reestructurado el 3 de junio de 2026. Todos los datos verificados contra al menos una fuente; se indica explícitamente cuando la corroboración es limitada. Las secciones específicas del ecosistema Erando (arquitectura, esquema SurrealDB, handlers conceptuales) son propuestas de diseño basadas en la investigación documental y requieren validación en el entorno de desarrollo.*