Skip to content
← Back to snippets
JavaScriptPHP

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.

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.

JavaScriptJavaScript

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 }
JavaScriptJavaScript

Auto-arrange Telegram inline keyboard buttons

Packs short buttons two-per-row and gives long ones their own row, based on the message text length above them — keeps keyboards compact without truncating labels.

How it works: Telegram inline keyboards have a fixed visual width. When the message above is short, one button per row reads better; when it's long enough that vertical space matters, adjacent short buttons (like "Yes"/"No") get greedily paired into the same row while long ones stay on their own.

const SHORT_CONTENT_THRESHOLD = 35
const SHORT_BUTTON_THRESHOLD = 10

function layoutTelegramButtons(buttons, contentLength) {
  if (buttons.length <= 1) return buttons.map((b) => [b])

  if (contentLength < SHORT_CONTENT_THRESHOLD) {
    return buttons.map((b) => [b])
  }

  const rows = []
  let i = 0
  while (i < buttons.length) {
    const current = buttons[i]
    const next = buttons[i + 1]
    const bothShort = next && current.text.length <= SHORT_BUTTON_THRESHOLD && next.text.length <= SHORT_BUTTON_THRESHOLD

    if (bothShort) {
      rows.push([current, next])
      i += 2
    } else {
      rows.push([current])
      i += 1
    }
  }
  return rows
}

// Example usage — feed straight into Telegram's inline_keyboard
const inline_keyboard = layoutTelegramButtons(
  [{ text: 'Yes', callback_data: 'y' }, { text: 'No', callback_data: 'n' }],
  120
)
// [[{text:'Yes',...}, {text:'No',...}]] — paired into one row
JavaScriptJavaScript

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,
})
JavaScriptJavaScript

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
PHP

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',
]);
PHP

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
PHP

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'],
        ]],
    ]),
]);
PHP

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);
}
// CONNECT

Contact Information

Submit a request or write to me directly on Telegram. I'll reply within 24 hours.

Provide either your email or Telegram so I can reply.