When a contact form submission lands, you want to know immediately — not by checking an inbox an hour later. A Telegram bot gives you instant, free push notifications straight to your phone. This post walks through the exact Flask pattern for sending those notifications, including a UX trick that makes mobile follow-up dramatically faster.
Why Telegram beats email for form alerts
Email notifications get buried. Telegram messages arrive as push notifications, open instantly, and — crucially — you can tap-copy a phone number or email address directly from the message. No opening threads, no scrolling. For a contact form where the goal is to follow up fast, this matters.
Setting up a Telegram bot takes about two minutes. It's free, has no rate limits for low-volume notifications, and the API is dead simple — a single HTTP POST.
Getting your bot token and chat ID
Open Telegram and search for @BotFather. Send /newbot, follow the prompts, and you'll get a bot token that looks like 123456789:ABCdef.... Set that as your TELEGRAM_BOT_TOKEN environment variable.
For your chat ID: send any message to your new bot, then open this URL in a browser (replacing YOUR_TOKEN):
https://api.telegram.org/botYOUR_TOKEN/getUpdates
Look for "chat":{"id": ...} in the response. That number is your TELEGRAM_CHAT_ID. Set it as an environment variable alongside the token.
The UX insight: why 4 messages instead of 1
The natural instinct is to bundle all fields into one Telegram message. The problem: on mobile, you can't tap-copy a specific line from a multi-line message. You'd have to long-press, select the text, copy — for every field.
Sending 4 separate messages solves this. The status summary tells you what happened. The next three messages are just the name, phone number, and email — each as its own standalone message. One tap to copy the phone number, one tap to copy the email. That's the entire UX win.
The core notification function
This function sends the status summary (with Markdown formatting) followed by the three contact fields as plain text. Each message gets its own try/except so a single failure doesn't abort the rest. The 0.3-second sleep between sends avoids hitting Telegram's rate limiter.
import os
import time
import requests
def send_telegram_notifications(status_message, name, phone, email):
bot_token = os.getenv('TELEGRAM_BOT_TOKEN')
chat_id = os.getenv('TELEGRAM_CHAT_ID')
if not bot_token or not chat_id:
return False
url = f'https://api.telegram.org/bot{bot_token}/sendMessage'
messages = [
{'text': status_message, 'parse_mode': 'Markdown'},
{'text': name},
{'text': phone},
{'text': email},
]
for msg in messages:
payload = {'chat_id': chat_id, **msg}
try:
requests.post(url, json=payload, timeout=5)
except Exception:
pass
time.sleep(0.3)
A few things worth noting. The first message uses parse_mode: 'Markdown' so you can use bold text and emoji in the status summary. The remaining three messages omit parse_mode entirely — plain text is safer when you're sending user-submitted content that might contain characters Telegram's Markdown parser would choke on.
Wiring it into your contact form route
The route extracts the fields, saves the submission (however you handle persistence), then calls the notification function with a formatted status message. The status message is the only part that needs to look polished — the field messages are just raw strings.
from flask import Flask, request, jsonify
from datetime import datetime
@app.route('/api/contact', methods=['POST'])
def submit_contact():
data = request.get_json()
name = data.get('name', '').strip()
phone = data.get('phone', '').strip()
email = data.get('email', '').strip()
# ... your submission persistence logic here ...
status_message = (
f"✅ *New Contact Form Submission*\n"
f"🕐 {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}"
)
send_telegram_notifications(status_message, name, phone, email)
return jsonify({'success': True}), 200
The notification is fire-and-forget. If Telegram is down or the request times out, the form submission still succeeds and the user gets their confirmation. Never let a notification side effect break the primary user flow.
Handling partial and failed submissions
If your form has validation logic that can result in partial saves, use the status message to reflect that. The emoji prefix gives you instant visual scanning in your notification list.
if all_fields_valid and saved_successfully:
status_message = (
f"✅ *New Contact Form Submission*\n"
f"🕐 {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}"
)
elif partial_save:
status_message = (
f"⚠️ *Partial Submission*\n"
f"🕐 {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}"
)
else:
status_message = (
f"❌ *Submission Failed*\n"
f"🕐 {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}"
)
At a glance in your Telegram chat you can see green checkmarks for clean submissions, yellow warnings for edge cases, and red X's for failures — without opening anything.
Startup notifications: know when your server recovers
This is a bonus pattern that earns its keep. When your Flask app starts, send a startup notification that includes submission counts. If your server restarts unexpectedly, you'll see the notification and know immediately — and the counts confirm whether any data was lost.
from datetime import datetime, date
def send_startup_notification(total_count, today_count):
bot_token = os.getenv('TELEGRAM_BOT_TOKEN')
chat_id = os.getenv('TELEGRAM_CHAT_ID')
if not bot_token or not chat_id:
return
timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')
message = (
f"🚀 *App Started*\n"
f"🕐 {timestamp}\n"
f"📈 Total submissions: {total_count}\n"
f"📅 Today: {today_count}"
)
url = f'https://api.telegram.org/bot{bot_token}/sendMessage'
try:
requests.post(
url,
json={'chat_id': chat_id, 'text': message, 'parse_mode': 'Markdown'},
timeout=5
)
except Exception:
pass
Call this in your app's startup block, after you've queried your submission store for the counts. In Flask you can do this inside an if __name__ == '__main__' block, or in a startup hook depending on how you're deploying.
Environment variable setup summary
Two variables, both required:
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrSTUvwxYZ
TELEGRAM_CHAT_ID=987654321
Store these in your .env file (loaded via python-dotenv or your deployment platform's secret manager). Never hardcode them. The function checks for both at the top and returns early if either is missing, which makes local development safe — just don't set the variables and no notifications fire.
The complete pattern at a glance
The Flask Telegram contact form notification pattern comes down to three pieces: a send_telegram_notifications function that fires 4 sequential messages, a route that calls it after saving the submission, and a startup hook that confirms the app is live. The 4-message split is the detail that makes this actually useful on mobile — every field is independently tappable, which is exactly what you want when you're trying to call someone back quickly.