Part 2 of 4
LaLiga bot: Telegram alerts
Creating a bot with @BotFather, getting your chat_id and sending kickoff-change alerts with the Bot API.
With the matches structured, the next step was for the bot to ping my phone whenever the site changed a kickoff time. Telegram is perfect for this: free, with a simple HTTP API and no server needed.
Creating the bot with @BotFather
- Open a chat with @BotFather.
- Send
/newbot. - Give it a name (for example
Laliga Calendar Bot) and a username ending inbot(for exampleLaligaCalendarBot). - @BotFather replies with the HTTP API token. That token is the password for our bot.
Getting the chat_id
The bot needs to know which chat to deliver messages to. You get that number by talking to @userinfobot, which replies with your numeric Id.
With those two values (
TELEGRAM_BOT_TOKENandTELEGRAM_CHAT_ID) we have everything we need. They must never live in the code: we pass them through environment variables.
Testing the API with curl
Before writing code I checked that the token worked:
curl "https://api.telegram.org/bot<TOKEN>/getMe"
That returns {"ok": true, ...} if the token is valid. With the right chat_id, a test send:
curl -X POST "https://api.telegram.org/bot<TOKEN>/sendMessage" \
-d "chat_id=<CHAT_ID>" \
-d "text=Hello from the bot"
The domain bug: .com vs .org
Here came a bug that cost us a while. We sent the message to https://api.telegram.com/... and the answer was a 301 redirect from a server called Varnish, never reaching Telegram.
Lesson: the official Bot API domain is
api.telegram.org(with.org, not.com). A301or an answer likelocation: https://www.telegram.com/...is the clue that you’re using the wrong domain.
With the right domain, curl -i returned 200 OK and {"ok": true}.
The send function
In main.py I added a reusable function that reads the token and chat from the environment:
def enviar_alerta_telegram(mensaje):
token = os.environ.get("TELEGRAM_BOT_TOKEN")
chat_id = os.environ.get("TELEGRAM_CHAT_ID")
if not token or not chat_id:
print("[Aviso] Telegram no configurado o faltan variables de entorno.")
return
url = f"https://api.telegram.org/bot{token}/sendMessage"
payload = {
"chat_id": chat_id,
"text": mensaje,
"parse_mode": "Markdown"
}
try:
response = requests.post(url, json=payload, timeout=10)
if response.status_code != 200:
print(f"[Error] No se pudo enviar el mensaje a Telegram: {response.text}")
except Exception as e:
print(f"[Error] Excepción al conectar con Telegram: {e}")
parse_mode="Markdown" enables bold text and formatting. The kickoff-change alert ended up like this:
msg = (
f"🚨 *¡Cambio de horario en la próxima jornada!*\n\n"
f"📌 *Partido:* {p['titulo']}\n"
f"❌ *Antes:* {fecha_antigua_str}\n"
f"✅ *Ahora:* {fecha_nueva_str}\n"
f"🏟️ *Lugar:* {p['ubicacion']}"
)
enviar_alerta_telegram(msg)
Something we learned the hard way: with
400 Bad Requestfrom Telegram it’s almost always a wrongchat_idor a broken Markdown format. With401 Unauthorized, it’s the token. It’s worth reading the response body, it always spells it out.
A workflow to test the send
Since the bot runs on GitHub Actions, I left a manual workflow (test-telegram.yml) that forces a send using the real secrets. That way you can check with one click that the bot reaches your phone:
name: Prueba Forzada Telegram
on:
workflow_dispatch:
jobs:
test-envio:
runs-on: ubuntu-latest
steps:
- name: Forzar Mensaje Directo
env:
TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
EQUIPO: ${{ secrets.EQUIPO_OBJETIVO }}
run: |
curl -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
-d "parse_mode=Markdown" \
-d "text=🚨 *¡Cambio de horario en la próxima jornada!*"
In the next part we tie everything to Google Calendar so matches get scheduled and alerts fire automatically.