· 5 min read
Building a Telegram “Incident Relay” Bot for On-Call Engineering
A practical tutorial for Telegram developers on turning chat messages into an incident relay: detect incidents, enrich them with service context, and route them to the right responders. The example focuses on a new use case—using Telegram as the coordination layer for lightweight on-call automation.
Telegram is often treated as just a notification channel. For software teams, it can do more than that: it can become a lightweight incident relay that recognizes high-priority events, gathers context, and helps route the right people into the right chat at the right time.
This tutorial walks through a new use case for Telegram developers: an incident relay bot. The bot listens for alerts from your monitoring system, enriches them with service metadata, and posts a structured message into a Telegram group where engineers can act quickly.
The goal is not to replace your paging stack. Instead, the bot helps you build a fast, low-friction coordination layer around it.
What the bot does
Our incident relay bot will:
- Receive alert webhooks from your monitoring system.
- Validate the payload and decide whether it matters.
- Enrich the alert with service ownership and severity rules.
- Format a Telegram message with useful action buttons.
- Route the alert into the correct Telegram group or thread.
This is especially useful for teams that:
- use Telegram for engineering coordination,
- want a custom incident workflow,
- manage multiple services and response teams,
- need to reduce noise from alert floods.
Architecture overview
A simple version looks like this:
- Monitoring system sends webhook alerts to your backend.
- Backend service normalizes and classifies the alert.
- Telegram Bot API posts the incident into a group.
- On-call responders use inline buttons to acknowledge, escalate, or open a runbook.
A good design keeps the bot stateless and pushes business rules into configuration.
Step 1: Create the Telegram bot
Use [BotFather] in Telegram to create a bot and obtain the token. Then add the bot to your incident group and give it permission to send messages.
Record three values:
BOT_TOKENCHAT_IDfor the incident group- optional
THREAD_IDif you use forum topics
If you plan to route by service, you may need multiple chat IDs or topic IDs.
Step 2: Define the alert payload
Start with a payload shape that is easy to validate and expand later.
{
"service": "payments-api",
"severity": "critical",
"summary": "Error rate exceeded 12% for 5 minutes",
"environment": "production",
"timestamp": "2026-10-01T14:22:00Z",
"runbook_url": "https://example.internal/runbooks/payments-api",
"dashboard_url": "https://example.internal/dashboards/payments-api",
"incident_id": "inc_483920"
}
Keep the payload small but actionable. If responders need to jump between ten systems to understand the problem, the bot is not doing enough.
Step 3: Set up the webhook handler
Below is a minimal example using Node.js and Express. It receives an alert, validates the required fields, and sends a message to Telegram.
import express from "express";
const app = express();
app.use(express.json());
const BOT_TOKEN = process.env.BOT_TOKEN;
const CHAT_ID = process.env.CHAT_ID;
function isValidAlert(body) {
return body
&& typeof body.service === "string"
&& typeof body.severity === "string"
&& typeof body.summary === "string"
&& typeof body.incident_id === "string";
}
function buildMessage(alert) {
return [
`🚨 *${alert.severity.toUpperCase()}* incident`,
`*Service:* ${alert.service}`,
`*Environment:* ${alert.environment || "unknown"}`,
`*Summary:* ${alert.summary}`,
`*Incident ID:* ${alert.incident_id}`,
alert.runbook_url ? `*Runbook:* ${alert.runbook_url}` : null,
alert.dashboard_url ? `*Dashboard:* ${alert.dashboard_url}` : null,
].filter(Boolean).join("\n");
}
async function sendTelegramMessage(text) {
const response = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: CHAT_ID,
text,
parse_mode: "Markdown",
disable_web_page_preview: true,
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Telegram API error: ${errorText}`);
}
}
app.post("/alerts", async (req, res) => {
try {
if (!isValidAlert(req.body)) {
return res.status(400).json({ error: "Invalid alert payload" });
}
const message = buildMessage(req.body);
await sendTelegramMessage(message);
res.status(202).json({ ok: true });
} catch (error) {
console.error(error);
res.status(500).json({ error: "Failed to relay incident" });
}
});
app.listen(3000, () => {
console.log("Incident relay listening on port 3000");
});
Why this structure works
- Validation prevents accidental noise from malformed alerts.
- Formatting in one place keeps message style consistent.
- A single send function makes retries and observability easier later.
Step 4: Add message actions
A useful incident message should let responders do something immediately.
Telegram inline keyboards are a good fit for this use case. Add buttons such as:
- Acknowledge
- Open runbook
- Open dashboard
- Escalate
Here is a version that adds buttons.
function buildKeyboard(alert) {
return {
inline_keyboard: [
[
{ text: "Acknowledge", callback_data: `ack:${alert.incident_id}` },
{ text: "Escalate", callback_data: `esc:${alert.incident_id}` },
],
[
...(alert.runbook_url ? [{ text: "Runbook", url: alert.runbook_url }] : []),
...(alert.dashboard_url ? [{ text: "Dashboard", url: alert.dashboard_url }] : []),
],
],
};
}
Then include it in sendMessage:
body: JSON.stringify({
chat_id: CHAT_ID,
text,
parse_mode: "Markdown",
disable_web_page_preview: true,
reply_markup: buildKeyboard(alert),
})
For Telegram developers, the key idea is that the button layout should reflect your workflow. Avoid adding buttons that only duplicate information already in the message.
Step 5: Handle callbacks
When a responder taps Acknowledge or Escalate, Telegram sends a callback query. Your bot should answer it quickly, then update the incident message if needed.
app.post("/telegram/callback", async (req, res) => {
const { callback_query } = req.body;
if (!callback_query) {
return res.sendStatus(200);
}
const [action, incidentId] = callback_query.data.split(":");
// Example: persist action in your incident store
console.log({ action, incidentId, user: callback_query.from.username });
// Answer immediately so Telegram stops loading
await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/answerCallbackQuery`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
callback_query_id: callback_query.id,
text: action === "ack" ? "Incident acknowledged" : "Incident escalated",
show_alert: false,
}),
});
res.sendStatus(200);
});
A good pattern is to treat Telegram as the interaction surface and your database as the source of truth.
Step 6: Route by service ownership
The incident relay becomes much more useful if it can route alerts automatically.
For example, maintain a config map:
{
"payments-api": {
"chat_id": "-1001111111111",
"severity_threshold": "warning"
},
"search-indexer": {
"chat_id": "-1002222222222",
"severity_threshold": "critical"
}
}
Then use the service name to choose the destination chat. If your organization uses forum topics, you can route by topic instead of by group.
This is a strong fit for Telegram because it lets one bot coordinate multiple teams without forcing everyone into the same noisy room.
Step 7: Reduce alert spam
Incident channels fail when they become unreliable. To keep the bot useful:
- deduplicate repeated alerts by
incident_id, - batch duplicate events within a short time window,
- suppress low-value alerts outside of business hours if appropriate,
- include only the most relevant metadata in the message.
A practical rule: if the alert does not change the human response, do not send it again.
Step 8: Add escalation timing
You can make the bot proactive by checking whether an incident has been acknowledged.
A simple background job can:
- mark an incident as open,
- wait N minutes,
- verify whether anyone acknowledged it,
- post a follow-up if it is still open.
Example follow-up message:
⏰ No acknowledgement after 10 minutes
Service: payments-api
Incident: inc_483920
Next step: paging secondary on-call
This is more effective than a generic timer because it reflects the actual state of the incident.
Step 9: Make the message readable under pressure
Incident messages should be scannable in seconds.
Use this order:
- severity
- service
- environment
- summary
- immediate links
- incident ID
Avoid dense prose. Engineers under pressure need the message to answer:
- What broke?
- How bad is it?
- Where should I look?
- What should I do next?
Step 10: Hardening tips
Before putting this into production, add a few essentials:
- verify webhook signatures from your alert source,
- store incident state in a database,
- retry Telegram API calls with backoff,
- log message IDs returned by Telegram,
- separate bot logic from routing rules,
- redact secrets and customer data from alert payloads.
If you support multiple environments, make sure production incidents never get mixed into test chats.
A good next step: incident summaries
Once the relay bot is working, you can add a second feature: a summary command.
For example:
/incident 483920shows current status,/open-incidentslists active issues,/whois payments-apishows the owning team.
That turns your Telegram bot from a relay into a small operational interface.
Conclusion
A Telegram incident relay bot is a strong, practical use case for software engineering teams. It gives you a focused way to turn webhook noise into actionable incident coordination, while keeping your workflow in a place engineers already watch.
The key is to optimize for action, not just delivery. Validate alerts, enrich them with ownership data, add response buttons, and route them to the right group or topic. Once that is in place, Telegram becomes more than a chat app—it becomes part of your incident response system.