Telegram Bot API: ready-to-use code snippets
Nine snippets covering one full round trip of a Telegram bot: verifying the webhook, identifying the user, parsing a deep link, escaping MarkdownV2, laying out an inline keyboard, and answering a callback query. JavaScript and PHP.
Almost every Telegram bot handles the same round trip: an update arrives on your webhook, you work out who sent it, you reply with formatted text and a keyboard, and then you answer the button press. The Bot API reference documents each method in isolation, so the parts that actually bite — MarkdownV2 escaping, the shape of update, the two-second callback deadline — are spread across a dozen pages. These nine snippets are that round trip in order.
Verify the webhook before you trust anything
Your webhook endpoint is a public URL. Anyone who finds it can POST a JSON body that looks exactly like a Telegram update, and a bot that skips verification will happily treat it as real. Telegram's answer is the secret_token parameter you pass to setWebhook: it comes back on every request in the X-Telegram-Bot-Api-Secret-Token header. Compare it in constant time and reject anything that does not match, before you parse a single field.
The user is not where you expect
There is no single update.user. Depending on whether you received a message, an edited message, a callback query, or an inline query, the sender sits under a different key. Writing update.message.from.id works right up until the first button press, when message is undefined and the bot throws. Normalising the update into one user object at the entry point removes a whole class of intermittent bugs.
Deep links carry your onboarding state
t.me/yourbot?start=<payload> is the only piece of state Telegram will hand you before a conversation begins, which makes it the standard way to attribute a signup, resume a checkout, or bind a chat to an account in your own database. The payload arrives as text after /start, is limited to 64 characters, and is restricted to a URL-safe alphabet — so it holds a token or an ID, not a serialised object.
MarkdownV2 escaping is stricter than it looks
MarkdownV2 requires escaping eighteen characters, including ones that appear constantly in ordinary text: ., -, !, (, ), +. Miss one and the API rejects the whole message with a 400 rather than sending it unformatted, so a single unescaped period in user-supplied text takes down the reply. The escaping rules differ inside code blocks and link targets, which is where hand-rolled escapers usually break.
Keyboards and callbacks
Inline keyboards are an array of rows, and Telegram does not lay them out for you — a row of six buttons is a row of six unreadable buttons on a phone. Answering the callback query is not optional either: until you call answerCallbackQuery, the client shows a loading spinner on the button, and the call has to happen within a few seconds of the press.
Every snippet below is standalone — copy the one you need. Together they form a working skeleton for the bot request cycle.
Parse Telegram /start deep-link parameters
Extracts traffic source, UTM tags, or a referrer ID from the payload Telegram passes after /start — the standard way to track where bot users came from.
How it works: Telegram deep links look like t.me/YourBot?start=PAYLOAD, and the bot receives it as /start PAYLOAD; a simple prefix convention (ref_, utm_, referral_) on the payload lets you encode attribution data without any extra API calls.
function parseStartParams(startCommand) {
const [, param] = startCommand.split(' ')
if (!param) return { source: 'telegram' }
// /start ref_telegram_ad
if (param.startsWith('ref_')) {
return { source: param.replace('ref_', '') }
}
// /start utm_source-google_medium-cpc_campaign-spring
if (param.startsWith('utm_')) {
const utm = {}
param.replace('utm_', '').split('_').forEach((part) => {
const [key, value] = part.split('-')
if (key && value) utm[`utm_${key}`] = decodeURIComponent(value)
})
return { source: utm.utm_source || 'unknown', medium: utm.utm_medium, campaign: utm.utm_campaign, utm }
}
// /start referral_12345
if (param.startsWith('referral_')) {
const referrerId = parseInt(param.replace('referral_', ''), 10)
if (!Number.isNaN(referrerId)) return { source: 'referral', referrerId }
}
return { source: param }
}
// Example usage
parseStartParams('/start ref_instagram_bio')
// { source: 'instagram_bio' }
parseStartParams('/start referral_12345')
// { source: 'referral', referrerId: 12345 }
Answer Telegram Callback Query
Responds to an inline button press (answerCallbackQuery) without an SDK — removes the loading spinner from the button, optionally showing a notification or alert to the user.
How it works: Telegram requires you to answer every callback_query within 10 seconds, otherwise the user sees an infinite spinner on the button. answerCallbackQuery dismisses it. The optional text shows a pop-up notification (toast or alert).
async function answerCallbackQuery(token, callbackQueryId, options = {}) {
const url = `https://api.telegram.org/bot${token}/answerCallbackQuery`
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
callback_query_id: callbackQueryId,
...options,
}),
})
if (!res.ok) throw new Error(`Telegram API error: ${res.status}`)
return res.json()
}
// Example usage — silent answer (just dismiss the spinner)
await answerCallbackQuery(token, query.id)
// With a notification (toast at the bottom, up to 200 characters)
await answerCallbackQuery(token, query.id, {
text: 'Order confirmed ✅',
show_alert: false,
})
// As a modal alert
await answerCallbackQuery(token, query.id, {
text: 'Action unavailable. Please try again later.',
show_alert: true,
})
Get User ID and Data from Telegram Update
Extracts the user ID, name, and username from any type of incoming Telegram update — message, callback_query, edited_message, inline_query, and others.
How it works: Telegram can send updates of different types, and the from field (user data) lives in different locations. The function walks through all possible paths and returns a normalised object, or null if the update has no user data (e.g. a channel post).
function getUserFromUpdate(update) {
const from =
update.message?.from ??
update.callback_query?.from ??
update.edited_message?.from ??
update.inline_query?.from ??
update.chosen_inline_result?.from ??
update.pre_checkout_query?.from ??
update.my_chat_member?.from ??
null
if (!from) return null
return {
id: from.id,
username: from.username ?? null,
firstName: from.first_name,
lastName: from.last_name ?? null,
fullName: [from.first_name, from.last_name].filter(Boolean).join(' '),
isBot: from.is_bot ?? false,
languageCode: from.language_code ?? null,
isPremium: from.is_premium ?? false,
}
}
function getChatIdFromUpdate(update) {
return (
update.message?.chat?.id ??
update.callback_query?.message?.chat?.id ??
update.edited_message?.chat?.id ??
null
)
}
// Example usage
const user = getUserFromUpdate(update)
// { id: 123456789, username: 'alice', fullName: 'Alice Smith', ... }
const chatId = getChatIdFromUpdate(update)
// 123456789
Telegram User Link and Mention
Generates a clickable link to a Telegram user profile and mention markup for MarkdownV2 — via username if available, otherwise via a tg://user?id= deep link.
How it works: if the user has a username, we use t.me/username. If not — a tg://user?id= deep link that opens the profile directly. For MarkdownV2 mentions, we wrap the name in [text](tg://user?id=...).
/**
* Public profile link (opens in browser / app)
*/
function telegramProfileLink(user) {
return user.username
? `https://t.me/${user.username}`
: `tg://user?id=${user.id}`
}
/**
* Mention in MarkdownV2 — clickable name inline in the message text
*/
function telegramMentionMd(user) {
const name = [user.first_name, user.last_name]
.filter(Boolean)
.join(' ')
.replace(/[_*[\]()~`>#+\-=|{}.!]/g, '\\$&') // escape MarkdownV2
return `[${name}](tg://user?id=${user.id})`
}
/**
* Mention in Telegram HTML markup
*/
function telegramMentionHtml(user) {
const name = [user.first_name, user.last_name].filter(Boolean).join(' ')
return `<a href="tg://user?id=${user.id}">${name}</a>`
}
// Example usage
const user = { id: 123456789, username: 'alice', first_name: 'Alice', last_name: 'Smith' }
telegramProfileLink(user) // 'https://t.me/alice'
telegramMentionMd(user) // '[Alice Smith](tg://user?id=123456789)'
telegramMentionHtml(user) // '<a href="tg://user?id=123456789">Alice Smith</a>'
Escape text for Telegram MarkdownV2
Telegram's Bot API rejects messages containing unescaped MarkdownV2 special characters — this escapes all of them safely.
How it works: it escapes every character Telegram's MarkdownV2 parser treats as special. This is the "escape everything" recipe — safe for plain user-supplied text, but note it will also escape markdown syntax you wrote yourself (like *bold*), so use it on dynamic values you're inserting into a message template, not on the template's own formatting.
<?php
function escapeMarkdownV2(string $text): string
{
$reserved = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'];
return str_replace($reserved, array_map(fn ($c) => '\\' . $c, $reserved), $text);
}
// Example usage
$message = escapeMarkdownV2('Price: $10.99 (was $12.99!)');
// "Price: \$10\.99 \(was \$12\.99\!\)"
$bot->sendMessage([
'chat_id' => $chatId,
'text' => $message,
'parse_mode' => 'MarkdownV2',
]);
Get Telegram User ID via PHP Webhook
Reads an incoming Telegram update from php://input and returns a normalised user object — id, username, name, and language code. Works with message, callback_query, and other update types.
How it works: Telegram sends a POST request with JSON in the body. PHP reads it via php://input, parses it, and looks for the from field in different parts of the update depending on its type.
<?php
function getTelegramUpdate(): ?array
{
$body = file_get_contents('php://input');
return $body ? json_decode($body, true) : null;
}
function getUserFromUpdate(array $update): ?array
{
$from = $update['message']['from']
?? $update['callback_query']['from']
?? $update['edited_message']['from']
?? $update['inline_query']['from']
?? $update['my_chat_member']['from']
?? null;
if (!$from) return null;
$firstName = $from['first_name'] ?? '';
$lastName = $from['last_name'] ?? '';
return [
'id' => (int) $from['id'],
'username' => $from['username'] ?? null,
'first_name' => $firstName,
'last_name' => $lastName,
'full_name' => trim("$firstName $lastName"),
'is_bot' => (bool) ($from['is_bot'] ?? false),
'language_code' => $from['language_code'] ?? null,
'is_premium' => (bool) ($from['is_premium'] ?? false),
];
}
function getChatIdFromUpdate(array $update): ?int
{
$chatId = $update['message']['chat']['id']
?? $update['callback_query']['message']['chat']['id']
?? $update['edited_message']['chat']['id']
?? null;
return $chatId !== null ? (int) $chatId : null;
}
// Example usage
$update = getTelegramUpdate();
if (!$update) exit;
$user = getUserFromUpdate($update);
$chatId = getChatIdFromUpdate($update);
// $user['id'] => 123456789
// $user['username'] => 'alice'
// $chatId => 123456789
Send Telegram Message via Bot API (PHP)
Sends a message through the Telegram Bot API without any SDK — only built-in curl. Supports MarkdownV2, ReplyMarkup, reply_to_message_id, and any other sendMessage parameters.
How it works: the function builds a POST request to api.telegram.org/bot{token}/sendMessage with a JSON body. All extra parameters (parse_mode, reply_markup, etc.) are passed via $options and merged with the required fields.
<?php
function telegramSendMessage(
string $token,
int|string $chatId,
string $text,
array $options = []
): array {
$url = "https://api.telegram.org/bot{$token}/sendMessage";
$payload = array_merge(['chat_id' => $chatId, 'text' => $text], $options);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) throw new \RuntimeException("cURL error: $error");
return json_decode($response, true);
}
// Example usage — plain message
telegramSendMessage($_ENV['BOT_TOKEN'], $chatId, 'Hello!');
// With Markdown and inline keyboard
telegramSendMessage($_ENV['BOT_TOKEN'], $chatId, 'Choose an action:', [
'parse_mode' => 'MarkdownV2',
'reply_markup' => json_encode([
'inline_keyboard' => [[
['text' => '✅ Confirm', 'callback_data' => 'confirm'],
['text' => '❌ Cancel', 'callback_data' => 'cancel'],
]],
]),
]);
Verify Telegram Webhook Secret
Validates the X-Telegram-Bot-Api-Secret-Token header on an incoming webhook request — protects the endpoint from requests that don't come from Telegram.
How it works: when registering a webhook (setWebhook) you can pass a secret_token. Telegram will include it in the X-Telegram-Bot-Api-Secret-Token header of every request. We compare with hash_equals to prevent timing attacks.
<?php
function verifyTelegramWebhookSecret(string $expectedSecret): bool
{
$header = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '';
if ($header === '') return false;
return hash_equals($expectedSecret, $header);
}
// Example usage — first line of the webhook handler
if (!verifyTelegramWebhookSecret($_ENV['TELEGRAM_WEBHOOK_SECRET'])) {
http_response_code(403);
exit;
}
$update = json_decode(file_get_contents('php://input'), true);
// ... handle the update
Register the webhook with a secret:
<?php
function registerWebhook(string $token, string $url, string $secret): array
{
$endpoint = "https://api.telegram.org/bot{$token}/setWebhook";
$payload = ['url' => $url, 'secret_token' => $secret, 'allowed_updates' => ['message', 'callback_query']];
$ch = curl_init($endpoint);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
]);
$res = curl_exec($ch);
curl_close($ch);
return json_decode($res, true);
}
More snippet collections
Contact Information
Submit a request or write to me directly on Telegram. I'll reply within 24 hours.