Skip to content
  1. Docs
  2. Integrations
  3. Telegram

Telegram astrology bot, webhook or polling

Ship a working /horoscope, /tarot, or /lifepath bot on Telegram in about 15 minutes. One token from BotFather, one RoxyAPI key, two screens of code.

The Bot API is plain JSON over HTTPS, BotFather hands you a token in three messages, and the handler runs anywhere that can speak HTTPS, so there is very little between you and a working bot. RoxyAPI supplies the answers: 258+ endpoints across 18+ domains on one key, and a Remote MCP server per domain when you want a model to choose the call itself.

Pick the path first

Two shapes, and the choice decides everything below.

You wantBuildRoxyAPI path
/horoscope aries returns the same reading every timeA command botOne endpoint per command, called from your handler
A chat that answers "what does my chart say about my career?"An AI botRemote MCP, one URL, tools discovered at runtime

A command bot is cheaper, deterministic, and needs no model. An AI bot does not need you to wire endpoints at all: point the model at https://roxyapi.com/mcp/{domain} and it reads the tool catalog itself. Start at the command bot below, then add the MCP path when free text arrives.

Step 1: prove the call

Before Telegram is involved at all, confirm your key works. Get one on the pricing page, then:

curl "https://roxyapi.com/api/v2/astrology/horoscope/aries/daily" \
  -H "X-API-Key: $ROXY_API_KEY"

A 200 returns clean JSON with sign, date, overview, love, career, luckyNumber, moonSign, moonPhase, and energyRating, plus column, the whole reading as one ready-to-send block of 120 to 180 words. Errors come back as { error, code }. Nothing is wrapped in a success envelope.

column is the fastest path to a good-looking Telegram reply: one field, already written as prose, well inside the 4096 character message cap. The six topic fields (overview, love, career, health, finance, advice) read the same dated events filtered to their own life area, each written as a column of its own, so render one shape or the other, never both.

Step 2: register the bot and pick a transport

Open @BotFather, send /newbot, pick a name and a username ending in bot. You get an HTTP token like 12345:ABC.... That token authenticates every call your handler makes back to Telegram, so treat it like a password.

Your handler loops on getUpdates, processes each message, replies. No webhook, no HTTPS host, no signing. Run it on a laptop, in a container, during a demo.

curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getUpdates"

Use it for prototyping and low volume. Move to a webhook when you deploy.

Step 3: the /horoscope handler

The examples run behind the webhook from Step 2. The body of each handler works unchanged inside a long-polling loop.

// npm install express @roxyapi/sdk
import express from 'express';
import { createRoxy } from '@roxyapi/sdk';

const roxy = createRoxy(process.env.ROXY_API_KEY!);
const TG = `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}`;
const app = express();
app.use(express.json());

app.post('/webhook', async (req, res) => {
  if (req.get('X-Telegram-Bot-Api-Secret-Token') !== process.env.TELEGRAM_WEBHOOK_SECRET) {
    return res.sendStatus(403);
  }
  const msg = req.body.message;
  if (!msg?.text) return res.sendStatus(200);

  const [cmd, sign] = msg.text.split(/\s+/);
  if (cmd === '/horoscope' && sign) {
    const { data, error } = await roxy.astrology.getDailyHoroscope({
      path: { sign: sign.toLowerCase() },
    });
    const reply = error
      ? `Could not read ${sign}: ${error.code}`
      : `${data.sign} for ${data.date}\n\n${data.column}\n\nLucky number: ${data.luckyNumber}\nMoon: ${data.moonSign} (${data.moonPhase})\nEnergy: ${data.energyRating}/10`;
    await fetch(`${TG}/sendMessage`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ chat_id: msg.chat.id, text: reply }),
    });
  }
  res.sendStatus(200);
});

app.listen(3000);

Send /horoscope aries to the bot. The reading comes back within a second.

Add /tarot with the card image

POST /tarot/daily returns card.imageUrl, a public CDN URL, and Telegram sendPhoto accepts a remote URL directly, so there is nothing to host. Passing seed makes the draw reproducible: the same seed on the same date always returns the same card, so a user who asks ten times before midnight sees one card.

Drop this inside the /webhook handler above; it reuses the same roxy, msg, and TG.

const { data } = await roxy.tarot.getDailyCard({ body: { seed: String(msg.from.id) } });
await fetch(`${TG}/sendPhoto`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    chat_id: msg.chat.id,
    photo: data.card.imageUrl,
    caption: `${data.card.name}${data.card.reversed ? ' (reversed)' : ''}\n\n${data.dailyMessage}`,
  }),
});

In Python the same call is roxy.tarot.get_daily_card(seed=str(msg['from']['id'])).

Step 4: more commands

Every command is the same three moves: pick the endpoint, build the parameters, format the reply. Browse everything in the API reference.

The endpoints that fit a bot reply best:

Any chart, panchang, dasha, synastry or compatibility call needs latitude, longitude, and timezone. Resolve them from the city with GET /location/search (roxy.location.searchCities in TypeScript, roxy.location.search_cities in Python) and pass the IANA timezone straight through. The server resolves DST for the chart date, so a January birth in New York gets EST and a July birth gets EDT.

Add an AI bot on Remote MCP

Hardcoded commands stop at the questions you predicted. For free text, connect the model to a Remote MCP server and let it choose.

The servers run at https://roxyapi.com/mcp/{domain} over Streamable HTTP, POST only. No stdio, no Docker, no local process. Authenticate with the X-API-Key header or Authorization: Bearer. The model calls tools/list, sees the typed catalog with field descriptions and enums, picks a tool, and calls it. You wire no endpoints. In the handler you forward msg.text to your agent and send the answer back with sendMessage.

Tool discovery is free. Each tools/call counts as one request, the same as the equivalent REST call. Full setup, per-client configs, and the compact argument that cuts agent token cost are on the Remote MCP page. If you would rather have a coding agent build the whole bot, the prompts page hands it the truth sources first.

Frequently asked questions

How do I keep group chats from burning quota?

Long polling and webhooks both deliver every message in a group your bot has been added to. Filter on a leading / so only commands reach RoxyAPI, and cache daily content per user in a short-lived store so ten /horoscope aries in a minute cost one call. Sending /setprivacy to BotFather turns on Privacy Mode, after which the bot only sees commands and replies.

Can the bot reply in another language?

Yes. Most domains take a lang query parameter: en, tr, de, es, hi, pt, fr, ru, zh-Hans, zh-Hant. Pass query: { lang: 'hi' } in TypeScript or lang='hi' in Python. Telegram passes an IETF tag in msg.from.language_code, so you can map it and forward. Machine values such as sign names stay canonical; the prose translates in place.

What happens on a malformed birth date?

RoxyAPI returns a 400 with code: validation_error and an issues[] array naming every field problem at once. Catch it (if (error) in TypeScript, except RoxyAPIError as e in Python), reply with something readable, and log the raw error. Retry only on 429 and 5xx; a 400 will fail the same way forever.

Do I have to verify webhook payloads?

Telegram does not sign the body. The defense is the secret_token you pass to setWebhook, echoed back in the X-Telegram-Bot-Api-Secret-Token header on every request, as the handlers above check. Without it, anyone who guesses your URL can post updates to it.

Can I use both polling and a webhook?

No. Setting a webhook disables getUpdates. Call deleteWebhook to go back to polling.

Gotchas

  • Backend-only key. The RoxyAPI key and the bot token live in your handler. Never echo either into a chat and never commit one.
  • Chat IDs are integers. JSON handles both forms, but typed code in TypeScript or Pydantic should match.
  • text is missing on photo, sticker and location messages. Guard before parsing commands.
  • A message caps at 4096 characters, a photo caption at 1024. A daily column fits both; a full natal chart interpretation fits neither, so split it.
  • Photos sent by URL cap at 5 MB. Upload as multipart if you generate something larger.
  • Formatting needs parse_mode. Add 'HTML' or 'MarkdownV2' to sendMessage to bold the sign. Without it everything is plain text, and MarkdownV2 needs its reserved characters escaped.
  • Rate limits are per chat and in bulk. About one message per second to the same chat, about 30 per second across different chats. A blast to a thousand subscribers needs pacing.
  • Prefer the IANA timezone. "Europe/London" beats 5.5, because the server resolves DST for the chart date.
  • Tool discovery is free, tool calls are not. Debugging an agent against tools/list costs nothing; every tools/call is one request.

What to build next