Skip to main content
Webhooks are the preferred way to receive updates in production. Instead of polling Telegram’s servers, Telegram sends updates directly to your server via HTTP POST requests.

Why Webhooks?

Lower Latency

Updates arrive instantly without polling delays

Lower Load

No constant connections to Telegram’s servers

Scalable

Works well with serverless platforms and load balancers

Cost-Effective

Reduced bandwidth and compute costs

Basic Setup

import { Bot, webhookCallback } from "grammy";
import express from "express";

const bot = new Bot("YOUR_BOT_TOKEN");

bot.on("message:text", (ctx) => ctx.reply("Got your message!"));

// Create Express app
const app = express();

// Use the webhook callback
app.use(express.json());
app.use(`/webhook/${bot.token}`, webhookCallback(bot, "express"));

// Set webhook
await bot.api.setWebhook(`https://your-domain.com/webhook/${bot.token}`);

app.listen(3000);
Never expose your bot token in the URL! Use it as a secret path component to prevent unauthorized webhook calls.

Framework Integration

grammY supports many web frameworks:
import express from "express";
import { webhookCallback } from "grammy";

const app = express();
app.use(express.json());
app.use(webhookCallback(bot, "express"));

await bot.api.setWebhook("https://example.com/webhook");
app.listen(8080);

Serverless Platforms

Cloudflare Workers

import { Bot, webhookCallback } from "grammy";

const bot = new Bot("YOUR_BOT_TOKEN");

bot.on("message", (ctx) => ctx.reply("Hello from Cloudflare!"));

export default {
  async fetch(request: Request) {
    if (request.method === "POST") {
      const cb = webhookCallback(bot, "cloudflare");
      return await cb(request);
    }
    return new Response("OK");
  },
};

Deno Deploy

import { Bot, webhookCallback } from "https://deno.land/x/grammy/mod.ts";

const bot = new Bot(Deno.env.get("BOT_TOKEN") || "");

bot.on("message", (ctx) => ctx.reply("Hello from Deno!"));

const handleUpdate = webhookCallback(bot, "std/http");

Deno.serve(async (req) => {
  if (req.method === "POST") {
    const url = new URL(req.url);
    if (url.pathname === "/webhook") {
      return await handleUpdate(req);
    }
  }
  return new Response("Not Found", { status: 404 });
});

Vercel

import { Bot, webhookCallback } from "grammy";

const bot = new Bot(process.env.BOT_TOKEN!);

bot.on("message", (ctx) => ctx.reply("Hello from Vercel!"));

export default webhookCallback(bot, "std/http");

Webhook Configuration

await bot.api.setWebhook("https://example.com/webhook", {
  // Only receive specific update types
  allowed_updates: ["message", "callback_query"],
  
  // Drop pending updates from previous bot instance
  drop_pending_updates: true,
  
  // Secret token to verify requests
  secret_token: "your-secret-token",
  
  // Maximum allowed number of connections (1-100)
  max_connections: 40,
  
  // Use specific IP address
  ip_address: "1.2.3.4",
});

Security

Secret Token Verification

import { webhookCallback } from "grammy";

const secretToken = "your-secret-token";

app.post("/webhook", async (req, res) => {
  // Verify secret token
  if (req.header("X-Telegram-Bot-Api-Secret-Token") !== secretToken) {
    return res.status(401).send("Unauthorized");
  }

  const cb = webhookCallback(bot, "express");
  await cb(req, res);
});

// Set webhook with secret token
await bot.api.setWebhook("https://example.com/webhook", {
  secret_token: secretToken,
});

HTTPS Requirement

Telegram only sends webhooks to HTTPS URLs. For local development, use tunneling tools like ngrok, Cloudflare Tunnel, or Serveo.

Local Development with Tunneling

Using ngrok

# Install ngrok
npm install -g ngrok

# Start your bot
node bot.js

# In another terminal, create a tunnel
ngrok http 3000

# Use the https URL provided by ngrok
Then set the webhook:
const ngrokUrl = "https://abc123.ngrok.io";
await bot.api.setWebhook(`${ngrokUrl}/webhook/${bot.token}`);

Health Checks

app.get("/health", (req, res) => {
  res.json({
    status: "ok",
    uptime: process.uptime(),
    timestamp: Date.now(),
  });
});

app.post("/webhook", webhookCallback(bot, "express"));

Managing Webhooks

// Get current webhook info
const info = await bot.api.getWebhookInfo();
console.log(info);

// Delete webhook (switch back to long polling)
await bot.api.deleteWebhook({ drop_pending_updates: true });

// Check if webhook is set
const webhookInfo = await bot.api.getWebhookInfo();
if (webhookInfo.url) {
  console.log("Webhook is set to:", webhookInfo.url);
} else {
  console.log("No webhook set");
}

Error Handling

import { webhookCallback } from "grammy";

bot.catch((err) => {
  console.error("Error in bot:", err);
});

app.post("/webhook", async (req, res) => {
  try {
    const cb = webhookCallback(bot, "express");
    await cb(req, res);
  } catch (error) {
    console.error("Webhook error:", error);
    res.status(500).send("Internal Server Error");
  }
});

Best Practices

Use Secret Tokens

Always verify webhook requests with secret tokens

Handle Gracefully

Return 200 OK even if your handler fails to prevent retries

Keep URLs Secret

Use your bot token as part of the webhook path

Monitor Webhook Info

Regularly check getWebhookInfo() for errors

Troubleshooting

  • Verify your URL is accessible from the internet
  • Check that you’re using HTTPS with a valid certificate
  • Ensure your server responds quickly (under 60 seconds)
  • Check getWebhookInfo() for error messages
  • Use a valid SSL certificate from a trusted CA
  • Self-signed certificates must be uploaded via setWebhook
  • Check certificate expiration
  • Check your max_connections setting
  • Ensure your server responds within 60 seconds
  • Monitor webhook info for pending update count
  • Consider scaling your infrastructure

See Also

Build docs developers (and LLMs) love