Skip to main content
The webhookCallback function creates a callback that integrates your grammY bot with web frameworks for webhook-based deployments. Instead of long polling, webhooks allow Telegram to push updates directly to your server.

Function Signature

function webhookCallback<C extends Context = Context>(
  bot: Bot<C>,
  adapter: FrameworkAdapter | string,
  webhookOptions?: WebhookOptions
): RequestHandler
bot
Bot<C>
required
Your bot instance
adapter
FrameworkAdapter | string
required
Framework adapter name or custom adapter. Built-in adapters:
  • 'express' - Express.js
  • 'koa' - Koa
  • 'fastify' - Fastify
  • 'std/http' - Deno std/http
  • 'hono' - Hono
  • 'oak' - Oak (Deno)
  • 'worktop' - Worktop (Cloudflare Workers)
  • 'cloudflare' - Cloudflare Workers
  • 'aws-lambda' - AWS Lambda
  • 'azure' - Azure Functions
  • 'vercel' - Vercel
webhookOptions
WebhookOptions
Optional configuration for webhook behavior
Returns: Framework-specific request handler function

WebhookOptions

Configuration options for webhook handling.
onTimeout
'throw' | 'return' | ((...args: any[]) => unknown)
Strategy for handling request timeouts:
  • 'throw' (default) - Throw an error on timeout
  • 'return' - Return successfully on timeout
  • Function - Custom timeout handler
timeoutMilliseconds
number
Request timeout in milliseconds. Defaults to 10000 (10 seconds).
secretToken
string
Secret token for validating requests via the X-Telegram-Bot-Api-Secret-Token header. Should match the token you set when configuring the webhook.

Basic Usage

Express

import express from 'express'
import { Bot, webhookCallback } from 'grammy'

const bot = new Bot('YOUR_BOT_TOKEN')

bot.command('start', (ctx) => ctx.reply('Hello!'))

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

app.listen(3000, () => {
  console.log('Webhook server running on port 3000')
})

Koa

import Koa from 'koa'
import { bodyParser } from '@koa/bodyparser'
import { Bot, webhookCallback } from 'grammy'

const bot = new Bot('YOUR_BOT_TOKEN')

bot.command('start', (ctx) => ctx.reply('Hello!'))

const app = new Koa()
app.use(bodyParser())
app.use(webhookCallback(bot, 'koa'))

app.listen(3000)

Fastify

import Fastify from 'fastify'
import { Bot, webhookCallback } from 'grammy'

const bot = new Bot('YOUR_BOT_TOKEN')

bot.command('start', (ctx) => ctx.reply('Hello!'))

const server = Fastify()

server.post('/', webhookCallback(bot, 'fastify'))

await server.listen({ port: 3000 })

Deno (std/http)

import { serve } from 'https://deno.land/std/http/server.ts'
import { Bot, webhookCallback } from 'https://deno.land/x/grammy/mod.ts'

const bot = new Bot('YOUR_BOT_TOKEN')

bot.command('start', (ctx) => ctx.reply('Hello!'))

const handleUpdate = webhookCallback(bot, 'std/http')

await serve(handleUpdate, { port: 3000 })

Hono

import { Hono } from 'hono'
import { Bot, webhookCallback } from 'grammy'

const bot = new Bot('YOUR_BOT_TOKEN')

bot.command('start', (ctx) => ctx.reply('Hello!'))

const app = new Hono()
app.post('/', webhookCallback(bot, 'hono'))

export default app

Advanced Configuration

With Secret Token

import express from 'express'
import { Bot, webhookCallback } from 'grammy'

const bot = new Bot('YOUR_BOT_TOKEN')
const SECRET_TOKEN = 'my-secret-token'

bot.command('start', (ctx) => ctx.reply('Secure webhook!'))

const app = express()
app.use(express.json())

app.use(webhookCallback(bot, 'express', {
  secretToken: SECRET_TOKEN
}))

app.listen(3000)

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

Custom Timeout Handling

import express from 'express'
import { Bot, webhookCallback } from 'grammy'

const bot = new Bot('YOUR_BOT_TOKEN')

const app = express()
app.use(express.json())

app.use(webhookCallback(bot, 'express', {
  timeoutMilliseconds: 5000, // 5 second timeout
  onTimeout: () => {
    console.log('Request timed out, but continuing...')
  }
}))

app.listen(3000)

Different Paths

import express from 'express'
import { Bot, webhookCallback } from 'grammy'

const bot = new Bot('YOUR_BOT_TOKEN')

const app = express()
app.use(express.json())

// Handle webhook at /webhook endpoint
app.post('/webhook', webhookCallback(bot, 'express'))

// Health check endpoint
app.get('/health', (req, res) => {
  res.json({ status: 'ok' })
})

app.listen(3000)

Cloud Platform Examples

Vercel

// api/webhook.ts
import { Bot, webhookCallback } from 'grammy'

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

bot.command('start', (ctx) => ctx.reply('Hello from Vercel!'))

export default webhookCallback(bot, 'std/http')

Cloudflare Workers

import { Bot, webhookCallback } from 'grammy'

const bot = new Bot('YOUR_BOT_TOKEN')

bot.command('start', (ctx) => ctx.reply('Hello from Cloudflare!'))

export default {
  async fetch(request: Request) {
    const handler = webhookCallback(bot, 'cloudflare')
    return await handler(request)
  }
}

AWS Lambda

import { Bot, webhookCallback } from 'grammy'

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

bot.command('start', (ctx) => ctx.reply('Hello from Lambda!'))

export const handler = webhookCallback(bot, 'aws-lambda')

Azure Functions

import { AzureFunction, Context, HttpRequest } from '@azure/functions'
import { Bot, webhookCallback } from 'grammy'

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

bot.command('start', (ctx) => ctx.reply('Hello from Azure!'))

const httpTrigger: AzureFunction = webhookCallback(bot, 'azure')

export default httpTrigger

Setting Up the Webhook

After deploying your webhook server, configure Telegram to send updates to it:
import { Bot } from 'grammy'

const bot = new Bot('YOUR_BOT_TOKEN')

// Set the webhook URL
await bot.api.setWebhook('https://example.com/webhook', {
  allowed_updates: ['message', 'callback_query'],
  secret_token: 'my-secret-token',
  drop_pending_updates: true
})

console.log('Webhook configured!')

Webhook Best Practices

1. Use HTTPS

Telegram requires HTTPS for webhooks (except localhost for testing).

2. Validate Requests

Always use a secret token to validate incoming requests:
app.use(webhookCallback(bot, 'express', {
  secretToken: process.env.SECRET_TOKEN
}))

3. Handle Timeouts

Telegram expects responses within 60 seconds. For long-running operations, acknowledge quickly:
bot.on('message', async (ctx) => {
  // Start long operation without awaiting
  processLongTask(ctx).catch(console.error)
  
  // Respond quickly
  await ctx.reply('Processing your request...')
})

4. Error Handling

Implement proper error handling:
bot.catch((err) => {
  const ctx = err.ctx
  console.error(`Error while handling update ${ctx.update.update_id}:`)
  const e = err.error
  if (e instanceof GrammyError) {
    console.error('Error in request:', e.description)
  } else {
    console.error('Unknown error:', e)
  }
})

5. Monitor Health

Add health check endpoints:
app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    timestamp: Date.now(),
    botUsername: bot.botInfo?.username
  })
})

Debugging Webhooks

Check Webhook Info

const info = await bot.api.getWebhookInfo()
console.log('Webhook URL:', info.url)
console.log('Pending updates:', info.pending_update_count)
console.log('Last error:', info.last_error_message)

Delete Webhook

await bot.api.deleteWebhook({ drop_pending_updates: true })
console.log('Webhook deleted')

Test Locally with ngrok

# Start your local server
node server.js

# In another terminal, expose it via ngrok
ngrok http 3000

# Use the ngrok URL to set your webhook
await bot.api.setWebhook('https://abc123.ngrok.io/webhook')

Complete Example

import express from 'express'
import { Bot, webhookCallback, GrammyError, HttpError } from 'grammy'

const bot = new Bot(process.env.BOT_TOKEN!)
const SECRET_TOKEN = process.env.SECRET_TOKEN!
const WEBHOOK_URL = process.env.WEBHOOK_URL!
const PORT = process.env.PORT || 3000

// Bot handlers
bot.command('start', (ctx) => {
  ctx.reply('Welcome! I\'m running on webhooks.')
})

bot.on('message:text', (ctx) => {
  ctx.reply(`You said: ${ctx.message.text}`)
})

// Error handling
bot.catch((err) => {
  const ctx = err.ctx
  console.error(`Error while handling update ${ctx.update.update_id}:`)
  const e = err.error
  
  if (e instanceof GrammyError) {
    console.error('Error in request:', e.description)
  } else if (e instanceof HttpError) {
    console.error('Could not contact Telegram:', e)
  } else {
    console.error('Unknown error:', e)
  }
})

// Express server
const app = express()
app.use(express.json())

// Webhook endpoint
app.post('/webhook', webhookCallback(bot, 'express', {
  secretToken: SECRET_TOKEN,
  timeoutMilliseconds: 10000,
  onTimeout: 'return'
}))

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: Date.now() })
})

// Start server and configure webhook
app.listen(PORT, async () => {
  console.log(`Server running on port ${PORT}`)
  
  // Set webhook
  await bot.api.setWebhook(WEBHOOK_URL, {
    secret_token: SECRET_TOKEN,
    allowed_updates: ['message', 'callback_query']
  })
  
  const info = await bot.api.getWebhookInfo()
  console.log('Webhook configured:', info.url)
})

// Graceful shutdown
process.once('SIGINT', async () => {
  console.log('Deleting webhook...')
  await bot.api.deleteWebhook()
  process.exit(0)
})

process.once('SIGTERM', async () => {
  console.log('Deleting webhook...')
  await bot.api.deleteWebhook()
  process.exit(0)
})

See Also

Build docs developers (and LLMs) love