# Build a Telegram astrology bot with RoxyAPI

> Ship a working `/horoscope`, `/kundli`, or `/tarot` bot on [Telegram](https://telegram.org/) in under 20 minutes. One Bot API token from [BotFather](https://t.me/BotFather), one Roxy API key, two screens of code.

Telegram is the easiest messaging platform to ship a bot on. The official [Bot API](https://core.telegram.org/bots/api) is a plain JSON-over-HTTPS surface, BotFather hands you a token in three messages, and your handler can run anywhere that can speak HTTPS. RoxyAPI provides the astrology, tarot, and numerology answers your bot replies with: 145+ endpoints across 12 domains behind one key, plus a remote MCP server per domain when you want an LLM to pick the right call on its own.

**TL;DR:**
- You will ship a Telegram bot that replies to `/horoscope <sign>`, `/tarot`, and `/lifepath <YYYY-MM-DD>` with live RoxyAPI data
- You need a [BotFather](https://t.me/BotFather) token (free) and a [RoxyAPI key](/pricing). Plans start at $39 a month for 25K requests; a free test key is available on request via [contact](/contact)
- Working code in three languages, ready to deploy. About 15 minutes start to finish

## What you can build on Telegram

- A daily horoscope DM that fires from a cron at 8 AM for every subscriber
- A `/kundli` command that asks for birth date, time, and place, then replies with a Vedic chart
- A `/tarot` command that draws a daily card and sends the card image as `sendPhoto`
- A `/numerology` flow that returns Life Path, Expression, and Soul Urge from a name and date
- A group bot that drops the daily moon phase or hexagram into a chat at sunset
- A multi-language support bot that calls Roxy with `?lang=hi` or `?lang=tr` for non-English users
- An AI agent bot where the LLM picks between horoscope, kundli, and tarot endpoints based on the question

## What you need, 30 seconds

1. A Telegram account and the [BotFather](https://t.me/BotFather) chat open. Free.
2. A RoxyAPI key. Plans start at $39 a month on the [pricing page](/pricing); a free test key for evaluation is available on request via [contact](/contact).
3. A place to run the handler. Local laptop with [ngrok](https://ngrok.com/) for testing, or any HTTPS host (Vercel, Fly.io, Railway, Render, your own server) for production.
4. About 15 minutes.

**Tip: Run the [quickstart](/docs/quickstart) curl in a terminal first. A 200 response confirms the Roxy key is good before you wire it into any handler.**

## Step 1, register your bot and pick a transport

Open a chat with [@BotFather](https://t.me/BotFather), send `/newbot`, pick a name and a username ending in `bot`. BotFather replies with an HTTP token like `12345:ABC...`. Save it. That token authenticates every call your handler makes back to Telegram.

Telegram offers two transports. Pick based on whether you have a public HTTPS URL.


### Long polling (fastest first run)

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

```bash
# One-shot poll. Starts the conversation feedback loop.
curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getUpdates"
```

Use long polling for prototyping and bots with low message volume. Switch to webhooks once you deploy.

### Webhook (production)

Telegram POSTs every incoming message to an HTTPS URL you own. No polling, lower latency, scales horizontally.

```bash
# Register a webhook. URL must be HTTPS.
curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook?url=https://your-app.example.com/webhook"
```

For local development, run [ngrok](https://ngrok.com/) (`ngrok http 3000`) and use the HTTPS URL it gives you. Telegram requires a valid TLS certificate, self-signed will not work without extra setup.


## Step 2, ship the /horoscope command

The handler watches for `/horoscope <sign>`, calls Roxy, and replies. The example below runs over the webhook transport from Step 1, but the body of each handler also works inside a long-polling loop without changes.


### curl (smoke test the endpoint)

Verify the Roxy call is correct before wiring anything to Telegram.

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

You should get back JSON with `sign`, `date`, `overview`, `love`, `career`, `luckyNumber`, `moonSign`, `moonPhase`, and `energyRating`. Pick the fields you want in the bot reply.

### Node.js (Express + SDK)

```typescript
// 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) => {
  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.overview}\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);
```

### Python (FastAPI + SDK)

```python
# pip install fastapi uvicorn httpx roxy-sdk
import os
import httpx
from fastapi import FastAPI, Request
from roxy_sdk import create_roxy, RoxyAPIError

roxy = create_roxy(os.environ['ROXY_API_KEY'])
TG = f"https://api.telegram.org/bot{os.environ['TELEGRAM_BOT_TOKEN']}"
app = FastAPI()

@app.post('/webhook')
async def webhook(request: Request):
    update = await request.json()
    msg = update.get('message')
    if not msg or 'text' not in msg:
        return {'ok': True}

    parts = msg['text'].split()
    if parts and parts[0] == '/horoscope' and len(parts) > 1:
        sign = parts[1].lower()
        try:
            data = roxy.astrology.get_daily_horoscope(sign=sign)
            reply = (
                f"{data['sign']} for {data['date']}\n\n"
                f"{data['overview']}\n\n"
                f"Lucky number: {data['luckyNumber']}\n"
                f"Moon: {data['moonSign']} ({data['moonPhase']})\n"
                f"Energy: {data['energyRating']}/10"
            )
        except RoxyAPIError as e:
            reply = f"Could not read {sign}: {e.code}"
        async with httpx.AsyncClient() as client:
            await client.post(
                f"{TG}/sendMessage",
                json={'chat_id': msg['chat']['id'], 'text': reply},
            )
    return {'ok': True}

# uvicorn telegram_bot:app --reload --port 3000
```


Send `/horoscope aries` to your bot. You should get back the daily reading within a second.

### Bonus, a /tarot command that sends the card image

The `tarot/daily` response carries `card.imageUrl`, a public CDN URL. The Telegram `sendPhoto` method accepts a remote URL directly, so you can return the card image with no extra hosting.

Drop this block inside the `app.post('/webhook', ...)` handler from above; it reuses the same `roxy`, `msg`, and `TG` variables.

```typescript
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}`,
  }),
});
```

Seeding by `msg.from.id` makes the daily card deterministic per user per day. Same user calls `/tarot` ten times before midnight, gets the same card every time. Works the same way in Python via `roxy.tarot.get_daily_card(seed=str(msg['from']['id']))`.

## Step 3, scale to the full surface

Adding a new command is the same shape as `/horoscope`. Pick the endpoint, build the parameters, format the reply.

- **[API reference](/api-reference)** with a pre-filled test key. Browse endpoints, run a call in the browser, copy the curl
- **Domain guides** for endpoint ordering: [Western astrology](/docs/guides/astrology), [Vedic astrology](/docs/guides/vedic-astrology), [Tarot](/docs/guides/tarot), [Numerology](/docs/guides/numerology)
- **Most-used endpoints** that fit Telegram bot replies:
  - [`GET /astrology/horoscope/{sign}/daily`](/api-reference#tag/western-astrology/GET/astrology/horoscope/{sign}/daily) for daily, weekly, monthly variants
  - [`POST /vedic-astrology/birth-chart`](/api-reference#tag/vedic-astrology/POST/vedic-astrology/birth-chart) for kundli (call [`GET /location/search`](/api-reference#tag/location-and-timezone/GET/location/search) first to geocode the city)
  - [`POST /numerology/life-path`](/api-reference#tag/numerology/POST/numerology/life-path) for the Life Path number from `year`, `month`, `day`
  - [`POST /tarot/spreads/three-card`](/api-reference#tag/tarot/POST/tarot/spreads/three-card) for past, present, future spreads

Any chart, horoscope, panchang, dasha, dosha, navamsa, KP, synastry, or compatibility endpoint needs `latitude`, `longitude`, and `timezone`. Always call `roxy.location.searchCities` (TS) or `roxy.location.search_cities` (Py) first and feed the result through. The server resolves IANA strings like `"Europe/London"` to the DST-correct offset for the chart date, so January births in New York pick EST and July births pick EDT automatically.

## Add an MCP-powered AI agent (optional)

Hardcoded `/horoscope` and `/tarot` commands are fine for a focused bot. For a free-text bot ("what does my chart say about my career?"), point an LLM at the Roxy MCP server and let it pick the endpoint.

The remote MCP server runs at `https://roxyapi.com/mcp/{domain}` over Streamable HTTP. No stdio, no Docker, no local setup. Point [Claude Code](/docs/guides/claude-code), [Cursor](/docs/guides/cursor), [OpenAI Agents SDK](/docs/guides/function-calling), or any other MCP-aware client at the URL with your `X-API-Key` header. The agent inspects `tools/list`, picks the right tool, builds parameters, and calls Roxy. From inside your Telegram handler, the LLM owns the routing logic; you only forward `msg.text` to the agent and reply with the result.

`tools/list` is free. Every `tools/call` bills the same as the equivalent REST call. See [/docs/mcp](/docs/mcp) for the full setup.

## Frequently asked questions


### Is the test API key OK for production?
No. The shared test key on [API reference](/api-reference) is rate-limited and rotates. For your bot, mint a real key on the [pricing page](/pricing). The Starter plan ($39 a month) covers 25K requests, enough to run a small bot end to end. If you need more headroom for evaluation before paying, request a free test key via [contact](/contact).

### How do I keep the bot from burning quota in group chats?
Long polling and webhooks both deliver every message in groups your bot is added to. Filter on `msg.text.startsWith('/')` so only commands trigger Roxy calls. Cache daily content per user with a short-lived KV (Redis, SQLite) so the same user calling `/horoscope aries` ten times in a minute pays for one call.

### Can the bot reply in Hindi, Spanish, or Turkish?
Yes. Most Roxy domains accept a `lang` query parameter (`en`, `tr`, `de`, `es`, `fr`, `hi`, `pt`, `ru`). In the SDK, pass `query: { lang: 'hi' }` (TS) or `lang='hi'` (Py). Detect the user language from `msg.from.language_code` (Telegram passes IETF tags) and forward it.

### What happens when a user sends a malformed birth date?
Roxy returns a 400 with `code: validation_error`. Catch it (TS: `if (error)`, Python: `except RoxyAPIError as e`), reply with a friendly message, and log the raw error for debugging. Never let the 400 bubble up as a generic Telegram crash.

### Do I need to verify webhook payloads?
Telegram does not sign webhook bodies the way Slack and WhatsApp do. The standard defense is a `secret_token` you pass to `setWebhook` and check on every incoming request via the `X-Telegram-Bot-Api-Secret-Token` header. Add it once you go to production.

## Gotchas

- **Backend-only key.** The Roxy key lives in your handler, never in a client message. If you proxy something to Telegram users, never echo `process.env.ROXY_API_KEY`.
- **Telegram chat IDs are integers, not strings.** Use them as numbers. JSON serialization handles both, but typed code in TS or Pydantic in Python should match.
- **Webhook URL must be HTTPS.** Self-signed certs work only with the `certificate` form-data parameter on `setWebhook`. Use [ngrok](https://ngrok.com/) for local dev, a real cert in production.
- **`text` is missing on photo, sticker, or location messages.** Always guard `if (!msg?.text)` before parsing commands.
- **Group bots see every message by default.** BotFather, `/setprivacy` to enable Privacy Mode so the bot only sees commands and replies, unless you actually want full feeds.
- **Send formatting matters.** `sendMessage` with `parse_mode: 'HTML'` or `'MarkdownV2'` lets you bold the sign and italicize the advice. Without `parse_mode` everything is plain text.
- **Telegram rate limits are 30 messages per second to different chats and 1 per second to the same chat.** A bulk send to 1000 users needs throttling.
- **MCP `tools/call` is billable, `tools/list` is free.** When debugging an agent, the discovery call does not count, the actual answer does.

## What to build next

- The [Western astrology guide](/docs/guides/astrology) and [Vedic astrology guide](/docs/guides/vedic-astrology) cover endpoint ordering for chart and kundli flows.
- The [WhatsApp integration](/docs/integrations/whatsapp) and [Slack integration](/docs/integrations/slack) cover the same patterns on adjacent messaging platforms.
- The [AI chatbot tutorial](/docs/tutorials/ai-chatbot) is the reference for wiring multiple domains into an LLM-routed agent.
- Browse the [API reference](/api-reference) for every endpoint across 12 domains.
