INTEGRATIONS

Reactions for your Telegram agent.

Give your Telegram agent a way to react to messages or stay quiet. Emote chooses the reaction, and your bot adds it to the original message.

Prerequisites

An existing Telegram bot that receives messages through a webhook or long polling, its bot token, and its ID from getMe. This example connects to the Telegram Bot API directly and fits into your current message handler.

Set EMOTE_API_KEY and TELEGRAM_BOT_TOKEN. Keep both keys server-side, and avoid logging Telegram request URLs because they contain your bot token.

Copy the TypeScript or Python example into your server code. TypeScript uses Node.js 20 or later; Python uses Python 3.10 or later with pip install httpx. Each example includes the Emote request function. Keep API keys in server environment variables.

Reaction options

Pass the chat ID and original message ID from your incoming update. The helper checks the chat’s allowed reactions with getChat, asks Emote to choose from the remaining options, and calls setMessageReaction. If reactions are disabled or Emote returns "none", it sends no reaction.

Pass agent to set the personality or use your agent’s system prompt. Pass reactions to choose a different palette. Use emoji from Telegram’s supported list; Telegram does not accept every Unicode emoji. The sample uses custom Emote options for , 😁, and 🙏, with the exact Telegram emoji as each ID.

Set frequency to reserved, balanced, or expressive. Emote can return "none" with any setting.

Integration example

import { createHash } from "node:crypto";

export const REACTIONS = [
  "👍", "❤️", "😂", "🎉", "😮", "😢",
  "🤔", "🔥", "💀", "🙌", "👀", "💯",
];

export type ReactionOption = string | { id: string; description: string };
export type ContextMessage = { role: "user" | "assistant"; content: string; reaction?: string };
export type ReactionInput = {
  eventId: string; message: string; agent?: string;
  context?: ContextMessage[]; reactions?: ReactionOption[];
  frequency?: "reserved" | "balanced" | "expressive";
};

// Server only. eventId must identify this message across all your conversations.
export async function chooseReaction({
  eventId, message, agent, context = [], reactions = REACTIONS, frequency = "balanced",
}: ReactionInput): Promise<string> {
  const key = process.env.EMOTE_API_KEY;
  if (!key) throw new Error("Set EMOTE_API_KEY on your server.");
  if (!eventId) throw new Error("A stable eventId is required.");

  const response = await fetch("https://useemote.com/v1/react", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${key}`,
      "Content-Type": "application/json",
      "Idempotency-Key": createHash("sha256").update(eventId).digest("hex"),
    },
    body: JSON.stringify({ message, agent, context, reactions, frequency }),
    signal: AbortSignal.timeout(3000),
  });
  if (!response.ok) throw new Error(`Emote: HTTP ${response.status}`);
  const reaction = await response.json();
  const allowed = reactions.map((option) => typeof option === "string" ? option : option.id);
  if (typeof reaction !== "string" || (reaction !== "none" && !allowed.includes(reaction))) {
    throw new Error("Unexpected Emote response.");
  }
  return reaction;
}

type TelegramInput = Omit<ReactionInput, "eventId" | "message"> & {
  botId: number; chatId: number | string; messageId: number; text: string;
};

// Telegram accepts a fixed set of standard emoji, further restricted per chat.
export const TELEGRAM_REACTIONS = [
  "👍", "🎉", "🔥", "😢", "👀",
  { id: "❤", description: "Love, warmth, or heartfelt support." },
  { id: "😁", description: "A cheerful grin in response to something funny or happy." },
  { id: "🙏", description: "Gratitude, thanks, or a hopeful wish." },
];

// Call from your existing verified, deduplicated Telegram message worker.
export async function reactOnTelegram({
  botId, chatId, messageId, text, agent, context = [], reactions = TELEGRAM_REACTIONS, frequency = "balanced",
}: TelegramInput) {
  const token = process.env.TELEGRAM_BOT_TOKEN;
  if (!token) throw new Error("Set TELEGRAM_BOT_TOKEN.");
  if (!botId) throw new Error("Pass your bot's ID from getMe.");

  async function telegram(method: string, body: Record<string, unknown>) {
    // Telegram authenticates in the URL. Do not log URLs or raw fetch errors.
    let response;
    try {
      response = await fetch(`https://api.telegram.org/bot${token}/${method}`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(5000),
      });
    } catch {
      throw new Error(`Telegram ${method}: request failed`);
    }
    if (!response.ok) throw new Error(`Telegram ${method}: HTTP ${response.status}`);
    const data = await response.json();
    if (!data.ok) throw new Error(`Telegram ${method}: request rejected`);
    return data.result;
  }

  const chat = await telegram("getChat", { chat_id: chatId });
  const allowed: Array<{ type: string; emoji?: string }> | undefined = chat.available_reactions;
  const choices = allowed === undefined ? reactions : reactions.filter((option) => {
    const emoji = typeof option === "string" ? option : option.id;
    return allowed.some((reaction) => reaction.type === "emoji" && reaction.emoji === emoji);
  });
  if (choices.length === 0) return;

  const reaction = await chooseReaction({
    eventId: JSON.stringify(["telegram", botId, chatId, messageId]),
    message: text, agent, context, frequency, reactions: choices,
  });
  if (reaction === "none") return;

  return telegram("setMessageReaction", {
    chat_id: chatId,
    message_id: messageId,
    reaction: [{ type: "emoji", emoji: reaction }],
  });
}

An omitted available_reactions field means all standard Telegram reactions are allowed; an empty list means none are available. The helper only offers standard emoji. You can cache the chat settings in your existing integration if needed, refreshing them when settings change or Telegram rejects a reaction. See custom reaction options for Emote’s option formats and limits.

Message handler

Use the function above in your existing bot. The inbound message, bot identity, conversation context, and reply function below come from your application; the snippet belongs inside your async worker. Normalize inbound user text messages first; skip your bot’s own messages, reaction updates, service messages, and unsupported media.

// Inside your existing verified, deduplicated update worker.
// inbound is your normalized incoming text message.
const reactionTask = reactOnTelegram({
  botId: bot.id, // From your existing getMe result.
  chatId: inbound.chatId,
  messageId: inbound.messageId,
  text: inbound.text,
  agent: agentPersonality,
  context: recentMessages,
}).catch(() => console.error("Reaction failed; reply continues."));

// Your existing reply function runs independently.
const replyTask = Promise.resolve().then(() => replyToTelegram(inbound));
const [, reply] = await Promise.allSettled([reactionTask, replyTask]);
if (reply.status === "rejected") throw reply.reason;

For webhooks, verify the X-Telegram-Bot-Api-Secret-Token header against the secret configured with setWebhook, then acknowledge the update promptly and process it in your existing worker. For long polling, use your existing update handling. Deduplicate incoming updates and track reaction delivery separately. Keep the same Emote input on retries, and discard work for deleted messages or inactive conversations.

Delivery rules

Telegram’s setMessageReaction method currently allows bots one reaction per message. Some service messages cannot receive reactions. For a media group, Telegram attaches the reaction to its first non-deleted message. Bots cannot use paid reactions.

Custom Telegram emoji use a separate custom_emoji_id and must already be present on the message or explicitly allowed by chat administrators. To use them, map custom Emote IDs to Telegram’s custom_emoji reaction type in your own delivery code. The helper above sends standard emoji only.

Your bot must be able to receive the messages you want it to react to. Group privacy mode can limit which messages it receives; check Telegram’s bot privacy rules for your setup. Handle permission changes, unavailable reactions, and rate limits in your existing worker without stopping the agent’s normal reply.

Verification

In a test chat, confirm the chosen reaction lands on the original message. Check that "none" sends nothing, disabled reactions skip the Emote request, repeated updates do not resend reactions, and an Emote or Telegram error leaves your normal reply running. Try a restricted reaction palette before using the integration in production conversations.

API reference · Get an API key