Payments

Invoices and payments

Complete guide on receiving payments in Telegram bots with PHP. We cover the methods sendInvoice, pre_checkout_query and successful payment with payload validation using HMAC.

Telegram Bot API provides a full-featured built-in payment system. The bot can sell physical goods, digital content, or subscriptions directly within the messenger interface. The user enters their card details once and pays with one click via Apple Pay, Google Pay, or built-in payment gateways.

The payment process consists of three steps:

  1. The bot sends the user an invoice using method sendInvoice.
  2. The user presses the pay button. Telegram sends the bot request pre_checkout_query. The bot has exactly 10 seconds to confirm product availability and readiness to complete the transaction.
  3. After successful deduction of funds, Telegram sends an update with object successful_payment. This is the final trigger by which the bot issues the item.

Step 0: Obtaining the Provider Token

Telegram does not handle money itself. It acts as a secure window between your bot, the user, and the payment gateway (Stripe, YooKassa, CloudPayments, PayMaster, etc.). To accept real payments, you need to connect a payment provider:

  • Open the bot @BotFather.
  • Send command /mybots and select your bot.
  • Go to the Bot Settings → Payments section.
  • Select the appropriate provider and follow the authorization instructions.
  • As a result, you will receive provider_token. For development and testing, always choose test mode (the token name will contain TEST).

Step 1: Sending Invoice (sendInvoice)

The invoice is sent as a special type of message. The most important parameter here is payload. This is a hidden string (up to 128 bytes) that Telegram will return at the verification stage and after payment. Never pass just the item ID there. A user can intercept or forge such a request. Always sign the payload using HMAC-SHA256, using your server's secret salt.

We use function telegramApi(), which we created in chapter Первый запрос: getMe. Here we only call it — we do not copy the implementation.

<?php
// Токен платежного провайдера (тестовый или боевой)
$providerToken = getenv('TELEGRAM_PROVIDER_TOKEN');
$chatId = 987654321; // ID чата с пользователем
$userId = 987654321; // ID пользователя
$itemId = 'premium_sub_30';

// Формируем безопасный payload с подписью
$secretKey = getenv('PAYLOAD_SECRET'); // Секретный ключ вашего сервера
$hmac = hash_hmac('sha256', "{$userId}:{$itemId}", $secretKey);
$payload = json_encode([
'user_id' => $userId,
'item_id' => $itemId,
'hmac' => $hmac
]);

$response = telegramApi('sendInvoice', [
'chat_id' => $chatId,
'title' => 'Подписка Premium',
'description' => 'Доступ к закрытым функциям бота на 30 дней',
'payload' => $payload,
'provider_token' => $providerToken,
'currency' => 'USD',
'prices' => [
[
'label' => 'Premium 30 дней',
'amount' => 499, // Сумма в минимальных единицах валюты (4.99 USD)
]
],
'start_parameter' => 'premium-30-days', // Параметр для deep-linking
'need_email' => true, // Запросить email при оплате
'send_email_to_provider' => true // Передать email провайдеру для чека
]);

// Посмотреть результат в терминале или логах
file_put_contents('php://stdout', print_r($response, true));

Example of a successful API response to method sendInvoice:

{
"ok": true,
"result": {
"message_id": 4501,
"from": {
"id": 12345678,
"is_bot": true,
"first_name": "My Shop Bot",
"username": "my_shop_bot"
},
"chat": {
"id": 987654321,
"first_name": "Alex",
"type": "private"
},
"date": 1700000000,
"invoice": {
"title": "Подписка Premium",
"description": "Доступ к закрытым функциям бота на 30 дней",
"start_parameter": "premium-30-days",
"currency": "USD",
"total_amount": 499
}
}
}

The field ok confirms that the invoice message was successfully delivered to the user. Inside result is located the standard object Message, but with an additional field invoice. Save message_id if you later need to delete the invoice if the user changed their mind.

Step 2: Readiness Check (pre_checkout_query)

When the user enters card details and presses the final pay button, Telegram sends your bot an update of type pre_checkout_query. At this stage the transaction has not been completed yet. You must check:

  • Is payload valid (does the HMAC signature match)?
  • Is the item in stock?
  • Has the price changed?

You have exactly 10 seconds to call method answerPreCheckoutQuery. If you do not respond in time or respond ok: false, Telegram will cancel the transaction and show the user an error.

We use function telegramApi(), which we created in chapter Первый запрос: getMe. Here we only call it — we do not copy the implementation.

<?php
// Пример обработки входящего вебхука (см. главу "Webhooks")
$input = file_get_contents('php://input');
$update = json_decode($input, true);

if (isset($update['pre_checkout_query'])) {
$query = $update['pre_checkout_query'];
$queryId = $query['id'];
$payload = json_decode($query['invoice_payload'], true);

// 1. Проверяем подпись payload
$secretKey = getenv('PAYLOAD_SECRET');
$expectedHmac = hash_hmac('sha256', "{$payload['user_id']}:{$payload['item_id']}", $secretKey);

$isValid = hash_equals($expectedHmac, $payload['hmac'] ?? '');

// 2. Проверяем наличие товара в БД (условная функция)
$isAvailable = ($payload['item_id'] === 'premium_sub_30');

if ($isValid && $isAvailable) {
// Разрешаем платеж
telegramApi('answerPreCheckoutQuery', [
'pre_checkout_query_id' => $queryId,
'ok' => true
]);
} else {
// Отклоняем платеж с объяснением причины
telegramApi(&039;answerPreCheckoutQuery', [
'pre_checkout_query_id' => $queryId,
'ok' => false,
'error_message' => 'Извините, этот товар сейчас недоступен или сессия оплаты устарела.'
]);
}
exit;
}

Example of an incoming update pre_checkout_query that will arrive at your webhook:

{
"update_id": 88442211,
"pre_checkout_query": {
"id": "123456789012345",
"from": {
"id": 987654321,
"is_bot": false,
"first_name": "Alex",
"username": "alex_username"
},
"currency": "USD",
"total_amount": 499,
"invoice_payload": "{\"user_id\":987654321,\"item_id\":\"premium_sub_30\",\"hmac\":\"a1b2c3d4...\"}",
"order_info": {
"email": "alex@example.com"
}
}
}

Note the field invoice_payload. It arrives as a string, so we must definitely decode it through json_decode($query['invoice_payload'], true) before validating the signature.

Step 3: Fixing Successful Payment (successful_payment)

If step 2 was successful, the payment system deducts the funds. Immediately after this, Telegram sends your bot a standard message (object Message), containing field successful_payment. This is the final point: money has been deducted, you are obligated to deliver the item to the user.

We use function telegramApi(), which we created in chapter Первый запрос: getMe. Here we only call it — we do not copy the implementation.

<?php
// Продолжение обработки вебхука (см. главу "Webhooks")
if (isset($update['message']['successful_payment'])) {
$payment = $update['message']['successful_payment'];
$chatId = $update['message']['chat']['id'];
$payload = json_decode($payment['invoice_payload'], true);

// Повторно проверяем HMAC для защиты от аномалий
$secretKey = getenv('PAYLOAD_SECRET');
$expectedHmac = hash_hmac('sha256', "{$payload['user_id']}:{$payload['item_id']}", $secretKey);

if (hash_equals($expectedHmac, $payload['hmac'] ?? '')) {
// Начисляем услугу в базе данных
// saveOrderToDb($payload['user_id'], $payload['item_id'], $payment['telegram_payment_charge_id']);

// Отправляем подтверждение пользователю
telegramApi('sendMessage', [
'chat_id' => $chatId,
'text' => "🎉 Спасибо за покупку! Подписка успешно активирована."
]);
} else {
// Критическая ошибка: подпись не совпала на финальном шаге
error_log("CRITICAL: Payment signature mismatch for user " . $payload['user_id']);
}
exit;
}

Example of an incoming message with successful payment:

{
"update_id": 88442212,
"message": {
"message_id": 4505,
"from": {
"id": 987654321,
"is_bot": false,
"first_name": "Alex"
},
"chat": {
"id": 987654321,
"type": "private"
},
"date": 1700000015,
"successful_payment": {
"currency": "USD",
"total_amount": 499,
"invoice_payload": "{\"user_id\":987654321,\"item_id\":\"premium_sub_30\",\"hmac\":\"a1b2c3d4...\"}",
"telegram_payment_charge_id": "tg_charge_id_9988776655",
"provider_payment_charge_id": "ch_1234567890abcdef"
}
}
}

Always save in your database the field telegram_payment_charge_id (unique transaction ID in Telegram) and provider_payment_charge_id (transaction ID in the payment system itself). They will be needed for accounting, dispute resolution, or processing returns.

How to Test Payments Without Real Money

  • Use only provider test tokens (obtained from BotFather).
  • During test invoice payment, Telegram will suggest entering a test card.
  • For Stripe, use card number: 4242 4242 4242 4242, any future expiration date and any CVC code.
  • For YooKassa, use a test card: 1111 1111 1111 1026 or other test details from official YooKassa documentation.
  • In test mode, the entire cycle (including pre_checkout_query and successful_payment) proceeds identically to the real one, but no funds are deducted from real cards.

Refunds and Fiscalization

Within the standard Bot API, for fiat currencies there is no method for issuing refunds. If a user needs to get money back, you must do it manually through your payment provider's personal account (for example, in the Stripe or YooKassa dashboard), using the saved provider_payment_charge_id.

Regarding receipts (54-FZ in Russia and similar laws in other countries): most modern providers can generate and send receipts to the tax authority automatically. For this, when sending sendInvoice transfer parameters need_email => true and send_email_to_provider => true, and enable automatic fiscalization in your payment gateway settings.

Continue: Telegram Stars

"}