Towns bots are built on Hono. While /webhook is reserved for Towns events, you can add custom routes for external webhooks, APIs, and scheduled tasks.
Using Bot Methods Anywhere
Bot methods like sendMessage() can be called directly on the bot instance, outside of event handlers. This enables integration with external services.
import { Hono } from 'hono'
import { makeTownsBot } from '@towns-protocol/bot'
const bot = await makeTownsBot(privateData, jwtSecret)
const { jwtMiddleware, handler } = bot.start()
const app = new Hono()
// Towns webhook (required)
app.post('/webhook', jwtMiddleware, handler)
// Custom API endpoint
app.post('/api/send', async (c) => {
const { channelId, message } = await c.req.json()
await bot.sendMessage(channelId, message)
return c.json({ ok: true })
})
export default app
External Webhooks
Integrate with external services like GitHub, Linear, Stripe, or custom APIs.
import { Hono } from 'hono'
import { makeTownsBot } from '@towns-protocol/bot'
const bot = await makeTownsBot(privateData, jwtSecret, { commands })
let notificationChannelId: string | null = null
// Configure channel for notifications
bot.onSlashCommand('setup', async (handler, event) => {
notificationChannelId = event.channelId
await handler.sendMessage(event.channelId, 'Notifications configured')
})
const { jwtMiddleware, handler } = bot.start()
const app = new Hono()
app.post('/webhook', jwtMiddleware, handler)
// External webhook endpoint
app.post('/external-webhook', async (c) => {
const payload = await c.req.json()
if (!notificationChannelId) {
return c.json({ error: 'No channel configured' }, 400)
}
// Send events to Towns channel
await bot.sendMessage(
notificationChannelId,
`Event received: ${payload.type}`
)
return c.json({ success: true })
})
export default app
You can use this pattern with any service that has webhook integration. Alchemy, GitHub, Linear, etc.
Scheduled Tasks
Use intervals or cron jobs to send periodic updates.
const bot = await makeTownsBot(privateData, jwtSecret)
const channels = new Set()
bot.onSlashCommand('alerts', async (handler, event) => {
channels.add(event.channelId)
await handler.sendMessage(event.channelId, 'Alerts enabled')
})
// Run every 5 minutes
setInterval(async () => {
const data = await fetch('https://api.example.com/data').then(r => r.json())
for (const channelId of channels) {
await bot.sendMessage(channelId, `Update: ${data.value}`)
}
}, 5 * 60 * 1000)
Persistent Storage
In-memory storage (Map, Set) only works on always-on servers. Use a database for production.
Popular options:
- SQLite/Turso - Simple, serverless-compatible
- Redis - Fast caching and rate limiting
- PostgreSQL - Full-featured relational database
- Supabase - Managed PostgreSQL with APIs
Next Steps