Agent skill
telegram-integration
Use when user explicitly requests Telegram bot development, Mini App integration, webhook/polling setup, or debugging Telegram-specific issues. Handles Telegraf-based bots, AI chatbot integration via Telegram, and deployment to Railway/Vercel. DO NOT use unless user mentions Telegram work.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/telegram-integration-imehr-skills
SKILL.md
Telegram Bot & Mini App Integration
⚠️ VERIFICATION REQUIRED
BEFORE using this skill, verify user needs Telegram integration:
- User explicitly mentions "Telegram" in their request, OR
- User mentions Telegram-specific terms:
- "Telegram bot", "Telegram Mini App", "Telegram webhook"
- "@BotFather", "Telegraf", "telegram-polling"
- Debugging existing Telegram features
- When in doubt, ASK: "Are you working on Telegram integration?"
DO NOT use this skill for:
- General chatbot or messaging work (Discord, Slack, WhatsApp)
- Generic deployment questions
- API or webhook issues unrelated to Telegram
- Just because Telegram code exists in the project
Overview
Comprehensive skill for building Telegram bots and Mini Apps using Telegraf. Covers setup, development, debugging, and deployment with conditional workflows for local vs production environments.
Core principle: Use polling for local development, webhooks for production. Validate Mini App data server-side. Never mix polling and webhooks simultaneously.
When to Use
ONLY after verifying Telegram work, use this skill when you see:
- "Build a Telegram bot"
- "Telegram webhook not working"
- "Add Telegram Mini App"
- "Deploy Telegram bot to Railway/Vercel"
- "Telegram bot works locally but not in production"
- "Integrate AI with Telegram"
When NOT to Use
NEVER use this skill for:
- Other messaging platforms (Discord, Slack, WhatsApp, SMS)
- General webhook/API issues unrelated to Telegram
- Non-Telegram chatbot work
- Just because you see
telegramin file names
If uncertain about Telegram involvement, verify first.
Quick Start: Diagnostic Router
Start every Telegram task by routing to the correct workflow:
| What are you doing? | Workflow |
|---|---|
| Starting new Telegram integration | → NEW_PROJECT |
| Webhook/polling not working | → DEBUGGING |
| Adding commands/AI/Mini App | → FEATURE_ADDITION |
| Deploying to production | → DEPLOYMENT |
NEW_PROJECT Workflow
Use when: User wants to create a new Telegram bot or Mini App.
TodoWrite Checklist
Create these todos when starting new project:
- [ ] Determine project type (bot only, mini app only, or both)
- [ ] Create bot via @BotFather, save token to .env.local
- [ ] Install Telegraf: npm install telegraf
- [ ] Choose environment setup (local polling or production webhook)
- [ ] Implement bot service with singleton pattern
- [ ] Add basic command handlers (/start, /help)
- [ ] Test bot responds to commands
Bot Creation (via @BotFather)
- Open Telegram, search for
@BotFather - Send
/newbotcommand - Follow prompts to set name and username
- Save the bot token:
TELEGRAM_BOT_TOKEN=<token> - Optional: Configure bot settings (
/setdescription,/setabouttext)
Environment-Specific Setup
Choose ONE based on environment:
Local Development (Polling)
Use polling to avoid HTTPS requirements:
Create telegram-polling.js:
const fetch = require('node-fetch');
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
const WEBHOOK_SECRET = process.env.TELEGRAM_WEBHOOK_SECRET || 'dev-secret';
const LOCAL_WEBHOOK_URL = 'http://localhost:3000/api/webhooks/telegram';
let offset = 0;
async function pollUpdates() {
try {
const response = await fetch(
`https://api.telegram.org/bot${BOT_TOKEN}/getUpdates`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ offset, timeout: 30 })
}
);
const data = await response.json();
if (data.ok && data.result.length > 0) {
for (const update of data.result) {
// Forward to local webhook
await fetch(LOCAL_WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Telegram-Bot-Api-Secret-Token': WEBHOOK_SECRET
},
body: JSON.stringify(update)
});
offset = update.update_id + 1;
}
}
} catch (error) {
console.error('Polling error:', error);
}
setTimeout(pollUpdates, 1000);
}
console.log('Starting Telegram polling...');
pollUpdates();
Start polling: node telegram-polling.js
Production (Webhook)
Use webhooks for event-driven updates:
Set webhook after deployment:
curl -X POST https://api.telegram.org/bot${TOKEN}/setWebhook \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/api/webhooks/telegram",
"secret_token": "YOUR_SECRET_TOKEN"
}'
Verify webhook:
curl https://api.telegram.org/bot${TOKEN}/getWebhookInfo
Bot Service (Singleton Pattern)
Create src/services/telegram/bot-service.ts:
import { Telegraf } from 'telegraf';
class TelegramBotService {
private static instance: Telegraf | null = null;
static getInstance(): Telegraf {
if (!this.instance) {
// Use dummy token for build, real token at runtime
const token = process.env.TELEGRAM_BOT_TOKEN || 'DUMMY_BUILD_TOKEN';
if (token === 'DUMMY_BUILD_TOKEN') {
console.warn('Using dummy token for build phase');
}
this.instance = new Telegraf(token);
this.registerCommands();
}
return this.instance;
}
private static registerCommands() {
const bot = this.instance!;
bot.command('start', (ctx) => {
ctx.reply('Welcome! Use /help to see available commands.');
});
bot.command('help', (ctx) => {
ctx.reply('Available commands:\n/start - Start bot\n/help - Show this message');
});
bot.on('text', async (ctx) => {
// Handle text messages
ctx.reply(`You said: ${ctx.message.text}`);
});
}
static async processUpdate(update: any) {
const bot = this.getInstance();
await bot.handleUpdate(update);
}
}
export default TelegramBotService;
Webhook API Route
Create src/app/api/webhooks/telegram/route.ts:
import { NextRequest } from 'next/server';
import TelegramBotService from '@/services/telegram/bot-service';
export async function POST(request: NextRequest) {
// Verify secret token
const secret = request.headers.get('x-telegram-bot-api-secret-token');
if (secret !== process.env.TELEGRAM_WEBHOOK_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const update = await request.json();
await TelegramBotService.processUpdate(update);
return Response.json({ ok: true });
} catch (error) {
console.error('Webhook error:', error);
return Response.json({ error: 'Internal error' }, { status: 500 });
}
}
// Force dynamic rendering (prevents caching issues)
export const dynamic = 'force-dynamic';
DEBUGGING Workflow
Use when: Telegram bot or webhook not working as expected.
Debugging Decision Tree
Is webhook receiving updates?
├─ No → Check webhook configuration
│ ├─ Run: curl https://api.telegram.org/bot${TOKEN}/getWebhookInfo
│ ├─ Check: url, pending_update_count, last_error_date, last_error_message
│ └─ See: [Webhook Troubleshooting](#webhook-troubleshooting)
├─ Yes, but errors → Check platform logs
│ ├─ Vercel: Check deployment logs and function errors
│ ├─ Railway: railway logs
│ └─ See: [Platform-Specific Issues](#platform-specific-issues)
└─ Updates received, bot not responding
├─ Check bot service initialization
├─ Verify command handlers registered
└─ Check for errors in message processing
Webhook Troubleshooting
Common webhook issues and solutions:
| Symptom | Cause | Solution |
|---|---|---|
| 401/403 Unauthorized | Missing or wrong secret token | Verify X-Telegram-Bot-Api-Secret-Token header matches setWebhook |
| Webhook not receiving updates | Webhook not set or deleted | Run setWebhook with correct URL and secret |
| SSL certificate error | Non-HTTPS URL | Ensure webhook URL uses https:// (except test environment) |
| Pending updates growing | Webhook timing out | Reduce processing time or use edge functions (Vercel) |
| Duplicate message processing | Timeout <10s, Telegram retries | Increase timeout limit or optimize response time |
| getUpdates conflict error | Polling + webhook both active | Delete webhook OR stop polling (never both) |
Diagnostic Commands:
# Check webhook status
curl https://api.telegram.org/bot${TOKEN}/getWebhookInfo
# Delete webhook (switch to polling)
curl -X POST https://api.telegram.org/bot${TOKEN}/deleteWebhook
# Test bot token
curl https://api.telegram.org/bot${TOKEN}/getMe
Platform-Specific Issues
Vercel Issues
Issue: Deployment Protection blocks webhooks
- Solution: Disable in Settings → Deployment Protection
Issue: 10-second timeout on Hobby plan
- Solution: Use edge functions or reduce processing time
- Code:
typescript
export const runtime = 'edge'; // Longer timeout
Issue: Bot not receiving updates after deploy
- Solution: Redeploy or set webhook again with new URL
Railway Issues
Issue: Bot works locally, fails on Railway
- Solution: Ensure bot initializes after build completes, use dummy token for build
Issue: Mini App buttons show old URL
- Solution: Force dynamic rendering, clear cache, check
TELEGRAM_MINI_APP_URLenv var - Code:
typescript
export const dynamic = 'force-dynamic';
Issue: Environment variables not loaded
- Solution: Use
railway variablesto verify, redeploy after changes
Local Development Issues
Issue: Polling script not receiving updates
- Solution:
- Verify bot token is correct
- Delete webhook:
curl -X POST https://api.telegram.org/bot${TOKEN}/deleteWebhook - Check polling script is running
Issue: Port conflicts
- Solution: Check port 3000 is free, or change
LOCAL_WEBHOOK_URLin polling script
FEATURE_ADDITION Workflow
Use when: Adding features to existing Telegram bot.
Feature Type Router
Choose feature type:
| Feature | Guide |
|---|---|
| Commands (/start, /help, custom) | Adding Commands |
| AI Integration (ChatGPT, Claude) | AI Integration Pattern |
| Inline keyboards/buttons | Interactive UI |
| Mini App | Mini App Integration |
| File handling (photos, voice, documents) | File Handlers |
Adding Commands
Pattern:
// In bot-service.ts registerCommands()
bot.command('mycommand', async (ctx) => {
// Command logic
await ctx.reply('Response text');
});
With parameters:
bot.command('search', async (ctx) => {
const query = ctx.message.text.split(' ').slice(1).join(' ');
if (!query) {
return ctx.reply('Usage: /search <query>');
}
const results = await searchFunction(query);
await ctx.reply(`Found: ${results}`);
});
AI Integration Pattern
Unified chat manager approach:
import { ChatManager } from '@/services/chat/chat-manager';
bot.on('text', async (ctx) => {
try {
// Get or create conversation for user
const conversation = await getOrCreateConversation(ctx.from.id);
// Process through AI
const aiResponse = await ChatManager.processMessage(
ctx.message.text,
{
platform: 'telegram',
userId: ctx.from.id,
context: conversation.pageContext
}
);
// Reply with markdown support
await ctx.reply(aiResponse, { parse_mode: 'Markdown' });
} catch (error) {
console.error('AI processing error:', error);
await ctx.reply('Sorry, I encountered an error processing your message.');
}
});
Streaming responses (for long AI outputs):
bot.on('text', async (ctx) => {
const statusMessage = await ctx.reply('Thinking...');
let fullResponse = '';
await ChatManager.streamMessage(ctx.message.text, {
onChunk: async (chunk) => {
fullResponse += chunk;
// Update message every 20 chunks to avoid rate limits
if (fullResponse.length % 100 === 0) {
await ctx.telegram.editMessageText(
ctx.chat.id,
statusMessage.message_id,
undefined,
fullResponse,
{ parse_mode: 'Markdown' }
);
}
},
onComplete: async () => {
await ctx.telegram.editMessageText(
ctx.chat.id,
statusMessage.message_id,
undefined,
fullResponse,
{ parse_mode: 'Markdown' }
);
}
});
});
Interactive UI
Inline keyboard:
import { Markup } from 'telegraf';
bot.command('menu', async (ctx) => {
await ctx.reply(
'Choose an option:',
Markup.inlineKeyboard([
[Markup.button.callback('Option 1', 'option_1')],
[Markup.button.callback('Option 2', 'option_2')],
[Markup.button.url('Visit Website', 'https://example.com')]
])
);
});
// Handle button callbacks
bot.action('option_1', async (ctx) => {
await ctx.answerCbQuery();
await ctx.editMessageText('You selected Option 1');
});
Context switching with page state:
bot.command('training', async (ctx) => {
// Update conversation context
await updateConversationContext(ctx.from.id, 'training');
await ctx.reply(
'Switched to Training mode. Ask me anything about your workouts!',
Markup.inlineKeyboard([
[Markup.button.callback('View Plan', 'view_plan')],
[Markup.button.callback('Log Workout', 'log_workout')],
[Markup.button.callback('Back to Menu', 'main_menu')]
])
);
});
File Handlers
Photo handling:
bot.on('photo', async (ctx) => {
const photo = ctx.message.photo[ctx.message.photo.length - 1];
const fileLink = await ctx.telegram.getFileLink(photo.file_id);
// Download or process photo
const analysis = await analyzeImage(fileLink.href);
await ctx.reply(`Analysis: ${analysis}`);
});
Voice message handling:
bot.on('voice', async (ctx) => {
const voice = ctx.message.voice;
const fileLink = await ctx.telegram.getFileLink(voice.file_id);
// Transcribe voice (e.g., using Whisper API)
const transcript = await transcribeAudio(fileLink.href);
await ctx.reply(`You said: ${transcript}`);
});
DEPLOYMENT Workflow
Use when: Deploying Telegram bot to production.
Deployment Checklist
Create these todos for production deployment:
- [ ] Determine deployment platform (Railway, Vercel, other)
- [ ] Set environment variables (TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRET)
- [ ] Create webhook API route (/api/webhooks/telegram)
- [ ] [VERCEL] Disable Deployment Protection in settings
- [ ] [RAILWAY] Use dummy token for build, real token at runtime
- [ ] Deploy application
- [ ] Delete existing webhook or stop polling
- [ ] Set webhook with production URL
- [ ] Verify webhook: check getWebhookInfo
- [ ] Send test message to bot
- [ ] Monitor platform logs for errors
Platform-Specific Deployment
Vercel Deployment
Requirements:
- HTTPS endpoint (automatic with Vercel)
- Deployment Protection disabled
- Edge runtime for longer timeouts (optional)
Steps:
-
Set environment variables in Vercel dashboard:
TELEGRAM_BOT_TOKENTELEGRAM_WEBHOOK_SECRETTELEGRAM_MINI_APP_URL(if using Mini Apps)
-
Disable Deployment Protection:
- Settings → Deployment Protection → Off
-
Deploy:
bashvercel --prod -
Set webhook:
bashcurl -X POST https://api.telegram.org/bot${TOKEN}/setWebhook \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.vercel.app/api/webhooks/telegram", "secret_token": "YOUR_SECRET" }' -
Test: Send message to bot, check Vercel logs
Railway Deployment
Requirements:
- HTTPS endpoint (automatic with Railway)
- Dummy token for build phase
- Environment variables set before deployment
Steps:
-
Set environment variables:
bashrailway variables --set "TELEGRAM_BOT_TOKEN=<token>" railway variables --set "TELEGRAM_WEBHOOK_SECRET=<secret>" -
Ensure bot service handles build phase:
typescriptconst token = process.env.TELEGRAM_BOT_TOKEN || 'DUMMY_BUILD_TOKEN'; -
Deploy:
bashrailway up -
Set webhook using Railway domain:
bash# Get Railway URL from railway status railway status # Set webhook curl -X POST https://api.telegram.org/bot${TOKEN}/setWebhook \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.up.railway.app/api/webhooks/telegram", "secret_token": "YOUR_SECRET" }' -
Monitor deployment:
bashrailway logs
Migration: Polling → Webhook
When switching from local to production:
-
Stop polling script: Kill the
telegram-polling.jsprocess -
Delete webhook (if any exists):
bashcurl -X POST https://api.telegram.org/bot${TOKEN}/deleteWebhook -
Deploy to production with webhook route
-
Set webhook with production URL (see platform steps above)
-
Verify switch:
bashcurl https://api.telegram.org/bot${TOKEN}/getWebhookInfo # Should show: url (your production URL), pending_update_count (should be 0 after test) -
Test: Send message to bot, check production logs
MINI APP INTEGRATION
Use when: User wants to add Telegram Mini App (web app within Telegram).
Mini App Checklist
- [ ] Create Mini App via @BotFather: /newapp or /setmenubutton
- [ ] Create Next.js page for Mini App (e.g., /app/telegram-mini-app/page.tsx)
- [ ] Include Telegram Web App SDK script
- [ ] Implement client-side initialization
- [ ] Create server-side initData validation endpoint
- [ ] Set TELEGRAM_MINI_APP_URL environment variable
- [ ] Configure bot to launch Mini App (keyboard/inline button)
- [ ] Test Mini App opens from bot
- [ ] Verify theme integration
- [ ] Test data validation
Mini App Setup (via @BotFather)
Create Mini App:
- Message
@BotFather - Send
/myapps - Select your bot
- Choose "Bot Settings" → "Menu Button" → "Edit Menu Button URL"
- Enter your Mini App URL:
https://your-app.com/telegram-mini-app
Or create standalone app:
- Message
@BotFather - Send
/newapp - Follow prompts to set name, description, photo
- Enter app URL
Client-Side Mini App
Create app/telegram-mini-app/page.tsx:
'use client';
import { useEffect, useState } from 'react';
declare global {
interface Window {
Telegram?: {
WebApp: any;
};
}
}
export default function TelegramMiniApp() {
const [user, setUser] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Initialize Telegram Web App
const tg = window.Telegram?.WebApp;
if (!tg) {
console.error('Telegram Web App SDK not loaded');
return;
}
// Notify Telegram app is ready
tg.ready();
// Get user data (UNSAFE - validate server-side)
const initDataUnsafe = tg.initDataUnsafe;
setUser(initDataUnsafe.user);
// Apply Telegram theme
document.body.style.backgroundColor = tg.themeParams.bg_color || '#ffffff';
document.body.style.color = tg.themeParams.text_color || '#000000';
// Listen for theme changes
tg.onEvent('themeChanged', () => {
document.body.style.backgroundColor = tg.themeParams.bg_color;
document.body.style.color = tg.themeParams.text_color;
});
setLoading(false);
// Enable closing confirmation for unsaved changes
tg.enableClosingConfirmation();
return () => {
tg.disableClosingConfirmation();
};
}, []);
const sendMessage = async (message: string) => {
const tg = window.Telegram?.WebApp;
// Validate on server with initData
const response = await fetch('/api/telegram-mini-app/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-telegram-init-data': tg.initData // For server validation
},
body: JSON.stringify({ message })
});
const data = await response.json();
return data;
};
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Welcome, {user?.first_name}!</h1>
{/* Your Mini App UI */}
</div>
);
}
Add SDK to layout:
// app/telegram-mini-app/layout.tsx
export default function TelegramMiniAppLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<head>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
</head>
<body>{children}</body>
</html>
);
}
Server-Side Validation (CRITICAL)
NEVER trust initDataUnsafe - always validate server-side.
Create validation utility:
// lib/telegram/validate-init-data.ts
import crypto from 'crypto';
export function validateTelegramInitData(
initData: string,
botToken: string
): boolean {
const urlParams = new URLSearchParams(initData);
const hash = urlParams.get('hash');
urlParams.delete('hash');
// Create data-check-string
const dataCheckArray = Array.from(urlParams.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}=${value}`);
const dataCheckString = dataCheckArray.join('\n');
// Compute secret key
const secretKey = crypto
.createHmac('sha256', 'WebAppData')
.update(botToken)
.digest();
// Compute hash
const computedHash = crypto
.createHmac('sha256', secretKey)
.update(dataCheckString)
.digest('hex');
return computedHash === hash;
}
export function parseTelegramUser(initData: string) {
const urlParams = new URLSearchParams(initData);
const userJson = urlParams.get('user');
if (!userJson) return null;
return JSON.parse(userJson);
}
Use in API route:
// app/api/telegram-mini-app/chat/route.ts
import { validateTelegramInitData, parseTelegramUser } from '@/lib/telegram/validate-init-data';
export async function POST(request: Request) {
const initData = request.headers.get('x-telegram-init-data');
if (!initData) {
return Response.json({ error: 'Missing init data' }, { status: 400 });
}
// VALIDATE - critical for security
const isValid = validateTelegramInitData(
initData,
process.env.TELEGRAM_BOT_TOKEN!
);
if (!isValid) {
return Response.json({ error: 'Invalid init data' }, { status: 401 });
}
// NOW safe to use
const user = parseTelegramUser(initData);
const { message } = await request.json();
// Process message with validated user
const aiResponse = await processAIMessage(message, user.id);
return Response.json({ response: aiResponse });
}
Launching Mini App from Bot
Inline keyboard button:
import { Markup } from 'telegraf';
bot.command('app', async (ctx) => {
await ctx.reply(
'Open Mini App:',
Markup.inlineKeyboard([
[Markup.button.webApp(
'Launch App',
process.env.TELEGRAM_MINI_APP_URL || 'https://your-app.com/telegram-mini-app'
)]
])
);
});
Menu button (persistent): Set via @BotFather as shown above, or programmatically:
await bot.telegram.setChatMenuButton({
menu_button: {
type: 'web_app',
text: 'Open App',
web_app: {
url: process.env.TELEGRAM_MINI_APP_URL!
}
}
});
Mini App Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Mini App won't load | Missing HTTPS | Use https:// in production (http:// only in test environment) |
| initData validation fails | Wrong bot token or hash computation | Verify bot token, check hash algorithm matches spec |
| Theme colors wrong | Hardcoded colors | Use tg.themeParams dynamically, listen for themeChanged |
| App closes unexpectedly | Missing ready() call |
Call tg.ready() early in initialization |
| Caching shows old version | Railway/Vercel cache | Force dynamic rendering: export const dynamic = 'force-dynamic' |
| Buttons disabled in production | Wrong URL in env var | Check TELEGRAM_MINI_APP_URL matches deployed URL |
Problem-Solution Quick Reference
Bot Not Responding
Checklist:
- Verify bot token:
curl https://api.telegram.org/bot${TOKEN}/getMe - Check webhook status:
curl https://api.telegram.org/bot${TOKEN}/getWebhookInfo - Review platform logs (Railway:
railway logs, Vercel: dashboard) - Verify secret token matches in webhook and API route
- Ensure no polling script running if using webhook
Local Setup Issues
Problem: Polling not receiving updates
- Solution: Delete webhook:
curl -X POST https://api.telegram.org/bot${TOKEN}/deleteWebhook
Problem: Port 3000 already in use
- Solution: Change port in polling script or kill process on port 3000
Problem: Updates received but bot doesn't respond
- Solution: Check bot service is initialized, commands are registered, no errors in console
Production Deployment Issues
Problem: Works locally, fails in production
- Cause: Build-time token initialization or environment variable issues
- Solution: Use dummy token for build, ensure env vars set before deployment
Problem: Webhook returns 401
- Cause: Secret token mismatch
- Solution: Verify
X-Telegram-Bot-Api-Secret-Tokenheader matchessetWebhooksecret
Problem: Vercel timeout errors
- Cause: Function exceeds 10-second limit on Hobby plan
- Solution: Use edge runtime or optimize processing time
Problem: Railway caching issues
- Cause: Static optimization caching responses
- Solution: Force dynamic:
export const dynamic = 'force-dynamic'
Mini App Issues
Problem: initDataUnsafe shows data but validation fails
- Cause: Using
initDataUnsafeinstead ofinitDatafor validation - Solution: Always validate
initData(raw string) server-side, never trustinitDataUnsafe
Problem: Mini App theme doesn't match Telegram
- Cause: Hardcoded colors
- Solution: Use
tg.themeParamsand listen forthemeChangedevent
Best Practices
Development
- Use polling locally, webhooks in production - simplifies local development
- Never run polling and webhook simultaneously - causes conflicts
- Validate Mini App data server-side -
initDataUnsafeis unsafe, validateinitData - Test with @BotFather commands - use
/setcommandsto show command list in UI
Production
- Use environment variables for tokens (never hardcode)
- Set secret tokens for webhooks (prevents unauthorized requests)
- Monitor webhook health with
getWebhookInfo(checkpending_update_count) - Force dynamic rendering on Next.js to prevent caching issues
- Use dummy token for build on Railway/Vercel to avoid initialization errors
Security
- Validate webhook secret tokens in all webhook endpoints
- Validate Mini App initData server-side using HMAC-SHA-256
- Never expose bot token in client code or logs
- Use HTTPS for webhooks in production (required by Telegram)
Performance
- Respond quickly to webhooks (<10 seconds on Vercel Hobby)
- Use edge functions for longer timeout limits
- Avoid heavy processing in webhook endpoint (queue jobs instead)
- Cache static responses when possible
Additional Resources
Official Documentation
- Bot API: https://core.telegram.org/bots/api
- Mini Apps (Web Apps): https://core.telegram.org/bots/webapps
- Telegraf Docs: https://telegraf.js.org/
Useful Tools
- @BotFather: Create and configure bots
- ngrok: Local webhook testing via HTTPS tunnel
- Telegram Test Environment: Test Mini Apps with http:// (https://docs.telegram-mini-apps.com/platform/test-environment)
Platform Documentation
- Vercel Deployment: https://vercel.com/docs
- Railway Deployment: https://docs.railway.com/
When Telegram Integration Fails
CRITICAL: Diagnose before acting. Never guess under pressure.
- Check webhook status -
getWebhookInfoshows errors, URL, pending updates - Review platform logs - Railway:
railway logs, Vercel: deployment logs - Verify environment variables - Token, secret, Mini App URL
- Test bot token -
curl https://api.telegram.org/bot${TOKEN}/getMe - Check Telegram status - https://core.telegram.org/bots/api (rare outages)
- Verify deployment platform - Deployment Protection (Vercel), caching (Railway)
For webhook issues: verify → delete → redeploy → set → test For Mini App issues: validate server-side → check theme → verify URL
Didn't find tool you were looking for?