Documentation Index
Fetch the complete documentation index at: https://mintlify.com/grammyjs/grammY/llms.txt
Use this file to discover all available pages before exploring further.
grammY ships with several powerful built-in utilities that enhance your bot development experience. These are included in the core package and don’t require additional dependencies.
Session Plugin
The session plugin provides persistent data storage for your bot, allowing you to remember information about users and chats across updates.
Basic Usage
import { Bot, Context, session, SessionFlavor } from 'grammy'
interface SessionData {
messageCount: number
lastMessage?: string
}
type MyContext = Context & SessionFlavor<SessionData>
const bot = new Bot<MyContext>('YOUR_BOT_TOKEN')
bot.use(session({
initial: () => ({ messageCount: 0 })
}))
bot.on('message', (ctx) => {
ctx.session.messageCount++
ctx.session.lastMessage = ctx.message.text
ctx.reply(`Message #${ctx.session.messageCount}`)
})
Session Options
The session plugin accepts several configuration options:
bot.use(session({
// Provide initial session data
initial: () => ({ count: 0 }),
// Custom session key generation
getSessionKey: (ctx) => {
// Store sessions per user instead of per chat
return ctx.from?.id.toString()
},
// Storage adapter (defaults to in-memory)
storage: myStorageAdapter,
// Optional prefix for session keys
prefix: 'my-bot:'
}))
Storage Adapters
By default, sessions are stored in memory and will be lost when your bot restarts. For production, use a storage adapter:
import { MemorySessionStorage } from 'grammy'
// In-memory storage with TTL
const storage = new MemorySessionStorage(60 * 60 * 1000) // 1 hour TTL
bot.use(session({ storage }))
For persistent storage, use external adapters (see the grammY documentation for database adapters).
Custom Storage Adapter
You can create your own storage adapter:
import { StorageAdapter } from 'grammy'
class MyStorage<T> implements StorageAdapter<T> {
private data = new Map<string, T>()
read(key: string): T | undefined {
return this.data.get(key)
}
write(key: string, value: T): void {
this.data.set(key, value)
}
delete(key: string): void {
this.data.delete(key)
}
}
bot.use(session({ storage: new MyStorage() }))
Lazy Sessions
For better performance, especially in groups, use lazy sessions that only load data when accessed:
import { lazySession, LazySessionFlavor } from 'grammy'
type MyContext = Context & LazySessionFlavor<SessionData>
const bot = new Bot<MyContext>('YOUR_BOT_TOKEN')
bot.use(lazySession({
initial: () => ({ count: 0 })
}))
bot.on('message', async (ctx) => {
// Session is loaded on first access
const session = await ctx.session
session.count++
})
Multi Sessions
Manage multiple independent session namespaces:
interface UserSession {
language: string
}
interface ChatSession {
settings: { welcome: boolean }
}
type MyContext = Context & SessionFlavor<{
user: UserSession
chat: ChatSession
}>
const bot = new Bot<MyContext>('YOUR_BOT_TOKEN')
bot.use(session({
type: 'multi',
user: {
initial: () => ({ language: 'en' }),
getSessionKey: (ctx) => ctx.from?.id.toString(),
},
chat: {
initial: () => ({ settings: { welcome: true } }),
getSessionKey: (ctx) => ctx.chat?.id.toString(),
},
}))
bot.on('message', (ctx) => {
console.log(ctx.session.user.language)
console.log(ctx.session.chat.settings.welcome)
})
Enhanced Sessions
Add migrations and expiry to sessions:
import { enhanceStorage } from 'grammy'
const storage = enhanceStorage({
storage: new MemorySessionStorage(),
millisecondsToLive: 24 * 60 * 60 * 1000, // 24 hours
migrations: {
1: (old) => ({ ...old, version: 1 }),
2: (old) => ({ ...old, newField: 'default' }),
},
})
bot.use(session({ storage }))
Keyboard Builders
grammY provides two keyboard builder classes for creating custom and inline keyboards easily.
Custom Keyboards (Keyboard)
Custom keyboards replace the user’s system keyboard:
import { Keyboard } from 'grammy'
const keyboard = new Keyboard()
.text('Button 1').text('Button 2').row()
.text('Button 3').text('Button 4')
await ctx.reply('Choose an option:', {
reply_markup: keyboard
})
Keyboard Options
const keyboard = new Keyboard()
.text('Yes').text('No')
.resized() // Make keyboard smaller
.oneTime() // Hide after button press
.persistent() // Show keyboard persistently
.selected() // Show only to mentioned users
.placeholder('Choose an option...')
await ctx.reply('Question?', { reply_markup: keyboard })
const keyboard = new Keyboard()
// Text button
.text('Send text')
// Request user's contact
.requestContact('Share contact')
// Request user's location
.requestLocation('Share location')
// Request a poll
.requestPoll('Create poll', 'quiz')
// Request users
.requestUsers('Select users', 123)
// Request a chat
.requestChat('Select chat', 456, { chat_is_channel: false })
// Web app button
.webApp('Open app', 'https://example.com')
const keyboard = new Keyboard()
.text('Danger').danger() // Red button
.text('Success').success() // Green button
.text('Primary').primary() // Blue button
.text('Default') // Default style
Keyboard Layout Methods
// Transpose rows and columns
const transposed = keyboard.toTransposed()
// Reflow to specific number of columns
const flowed = keyboard.toFlowed(3)
// Clone a keyboard
const clone = keyboard.clone()
// Append another keyboard
keyboard.append(anotherKeyboard)
Removing Keyboards
import { Keyboard } from 'grammy'
await ctx.reply('Keyboard removed', {
reply_markup: { remove_keyboard: true }
})
Inline Keyboards (InlineKeyboard)
Inline keyboards appear directly below messages:
import { InlineKeyboard } from 'grammy'
const keyboard = new InlineKeyboard()
.text('Button 1', 'callback-data-1')
.text('Button 2', 'callback-data-2').row()
.url('Visit Website', 'https://grammy.dev')
await ctx.reply('Choose:', { reply_markup: keyboard })
// Handle button clicks
bot.callbackQuery('callback-data-1', async (ctx) => {
await ctx.answerCallbackQuery('You clicked Button 1!')
await ctx.editMessageText('Button 1 was clicked')
})
const keyboard = new InlineKeyboard()
// Callback data button
.text('Click me', 'my-callback-data')
// URL button
.url('Visit', 'https://example.com')
// Web app button
.webApp('Open app', 'https://app.example.com')
// Login button
.login('Login', 'https://example.com/auth')
// Switch inline query
.switchInline('Share')
.switchInlineCurrent('Share here', 'query')
.switchInlineChosen('Share to...', { allow_user_chats: true })
// Copy text button
.copyText('Copy', 'Text to copy')
// Game button
.game('Play game')
// Payment button
.pay('Pay 10 XTR')
Inline Keyboard Styling
const keyboard = new InlineKeyboard()
.text('Delete', 'delete').danger()
.text('Confirm', 'confirm').success()
.text('Info', 'info').primary()
Dynamic Keyboards
Build keyboards from data:
const items = ['Apple', 'Banana', 'Cherry']
const keyboard = new InlineKeyboard()
items.forEach((item, index) => {
keyboard.text(item, `item:${index}`)
if ((index + 1) % 2 === 0) keyboard.row()
})
await ctx.reply('Choose a fruit:', { reply_markup: keyboard })
Webhook Callback
The webhook callback utility makes it easy to run your bot on webhooks with any web framework.
Basic Usage
import { webhookCallback } from 'grammy'
import express from 'express'
const bot = new Bot('YOUR_BOT_TOKEN')
const app = express()
// Use the webhook callback
app.use(express.json())
app.post('/webhook', webhookCallback(bot, 'express'))
app.listen(3000)
Supported Frameworks
grammY supports many web frameworks:
// Express
app.post('/webhook', webhookCallback(bot, 'express'))
// Fastify
fastify.post('/webhook', webhookCallback(bot, 'fastify'))
// Koa
router.post('/webhook', webhookCallback(bot, 'koa'))
// NestJS
webhookCallback(bot, 'http') // Node.js http module
// Next.js API route
export default webhookCallback(bot, 'next-js')
// Vercel serverless
export default webhookCallback(bot, 'std-http')
// Cloudflare Workers
export default { fetch: webhookCallback(bot, 'cloudflare') }
// Deno
Serve(webhookCallback(bot, 'std-http'))
Webhook Options
webhookCallback(bot, 'express', {
// Timeout handling
timeoutMilliseconds: 10000,
onTimeout: 'throw', // or 'return' or custom function
// Secret token validation
secretToken: 'my-secret-token',
})
Setting the Webhook
Before using webhooks, set the webhook URL:
await bot.api.setWebhook('https://example.com/webhook', {
secret_token: 'my-secret-token',
allowed_updates: ['message', 'callback_query'],
})
Inline Query Result Builder
The InlineQueryResultBuilder helps create inline query results easily.
Basic Usage
import { InlineQueryResultBuilder } from 'grammy'
bot.on('inline_query', async (ctx) => {
const results = [
InlineQueryResultBuilder.article(
'id-1',
'Result Title',
).text('This is the message content'),
InlineQueryResultBuilder.photo(
'id-2',
'https://grammy.dev/images/Y.png',
),
InlineQueryResultBuilder.audio(
'id-3',
'Audio Title',
'https://example.com/audio.mp3',
),
]
await ctx.answerInlineQuery(results)
})
Result Types
// Article (requires calling .text() or other content method)
InlineQueryResultBuilder.article('id', 'Title')
.text('Message content')
// Photo
InlineQueryResultBuilder.photo('id', 'photo-url')
// Cached photo
InlineQueryResultBuilder.photoCached('id', 'file-id')
// Video
InlineQueryResultBuilder.videoMp4(
'id',
'Title',
'video-url',
'thumbnail-url'
)
// Audio
InlineQueryResultBuilder.audio('id', 'Title', 'audio-url')
// Document
InlineQueryResultBuilder.documentPdf('id', 'Title', 'doc-url')
// Location
InlineQueryResultBuilder.location('id', 'Title', 40.7128, -74.0060)
// Venue
InlineQueryResultBuilder.venue(
'id',
'Place Name',
40.7128,
-74.0060,
'Address'
)
// Contact
InlineQueryResultBuilder.contact('id', '+1234567890', 'John Doe')
// Game
InlineQueryResultBuilder.game('id', 'game-short-name')
// GIF
InlineQueryResultBuilder.gif('id', 'gif-url', 'thumb-url')
// Voice
InlineQueryResultBuilder.voice('id', 'Title', 'voice-url')
Custom Message Content
Override the sent message content:
const result = InlineQueryResultBuilder
.photo('id', 'https://example.com/photo.jpg')
.text('Custom message text when photo is selected')
// Or other content types
const result2 = InlineQueryResultBuilder
.article('id', 'Article')
.location(40.7128, -74.0060)
const result3 = InlineQueryResultBuilder
.article('id', 'Contact')
.contact('John Doe', '+1234567890')
With Inline Keyboard
import { InlineKeyboard, InlineQueryResultBuilder } from 'grammy'
const keyboard = new InlineKeyboard()
.url('Visit', 'https://grammy.dev')
const result = InlineQueryResultBuilder
.article('id', 'Title')
.text('Content', {
reply_markup: keyboard,
})
Next Steps