Екосистема Telegram Stars (XTR) дозволяє легко продавати цифрові товари, підписки та преміум-контент всередині месенджера. На відміну від класичних платіжних провайдерів (YuKassa, Stripe), покупка Stars відбувається без перенаправлення на зовнішні форми — інтерфейс вбудований безпосередньо в клієнт Telegram. Однак технічний протокол вимагає суворого дотримання двоетапного підтвердження покупки на вашому сервері.
У цьому посібнику ми проаналізуємо архітектуру роботи з Telegram Stars в чистому PHP, напишемо надійного клієнта для генерації рахунків-фактур, реалізуємо валідацію передкасових запитів та забезпечимо ідемпотентність при завершенні транзакції.
Архітектурний платіжний цикл у Telegram Stars
Процес оплати Telegram Stars складається з п 'яти послідовних кроків:
- Ініціалізувати Сервер ботів створює рахунок за допомогою методу
sendInvoiceабо створює посилання на рахунок-фактуру за допомогоюcreateInvoiceLink. - Клік користувача: Користувач натискає кнопку «Оплатити» в чаті або міні-додатку.
- Попереднє оформлення замовлення: Telegram надсилає вебхук
pre_checkout_queryна ваш сервер. Сервер повинен підтвердити протягом 10 секунд, що він готовий відправити товар або відхилити транзакцію з поясненням причини. - Списання Telegram списує зірки з балансу користувача.
- Завершення (успішна оплата): Telegram відправляє повідомлення з об 'єктом
successful_payment. Бот надає доступ до послуги або продукту.
Важлива відмінність між Telegram Stars і фіатними валютами: вартість
provider_tokenМабуть. порожній рядок""і код валюти сувороXTRКрім того, в Stars 1 одиниця дорівнює 1 Star (на відміну від рублів або доларів, де сума перераховується в копійках/центах).
Розрахунок: метод sendInvoice
Щоб надіслати рахунок у чат, скористайтеся способом sendInvoice. Кожна транзакція повинна містити унікальну корисне навантаження. Рекомендується сформувати захищений унікальний ідентифікатор замовлення та зберегти його в базі даних у статусі Pending.
<?php
declare(strict_types=1);
/**
* Отправка инвойса Telegram Stars в чат
*/
function sendStarsInvoice(int $chatId, string $title, string $description, int $starsAmount, PDO $pdo): array
{
$botToken = getenv('BOT_TOKEN');
if (!$botToken) {
throw new \RuntimeException('BOT_TOKEN environment variable is missing');
}
// 1. Генерируем уникальный payload заказа
$leadId = bin2hex(random_bytes(7)); // 14 символов hex
$payload = 'order_stars_' . $leadId;
// 2. Фиксируем заказ в базе данных
$stmt = $pdo->prepare("INSERT INTO orders (payload, chat_id, amount, status, created_at) VALUES (:payload, :chat_id, :amount, 'pending', NOW())");
$stmt->execute([
':payload' => $payload,
':chat_id' => $chatId,
':amount' => $starsAmount,
]);
$url = "https://api.telegram.org/bot{$botToken}/sendInvoice";
// 3. Формируем тело запроса
$postData = [
'chat_id' => $chatId,
'title' => $title,
'description' => $description,
'payload' => $payload,
'provider_token' => '', // Обязательно пустая строка для Stars
'currency' => 'XTR', // Валюта Telegram Stars
'prices' => json_encode([
['label' => $title, 'amount' => $starsAmount]
], JSON_UNESCAPED_UNICODE),
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError !== '') {
throw new \RuntimeException("cURL error: {$curlError}");
}
if ($httpCode !== 200) {
throw new \RuntimeException("Telegram API HTTP error {$httpCode}: {$response}");
}
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException('JSON parse error: ' . json_last_error_msg());
}
if (!isset($data['ok']) || $data['ok'] !== true) {
throw new \RuntimeException('Telegram API error: ' . ($data['description'] ?? 'Unknown error'));
}
return $data['result'];
}
Перевірка та швидка відповідь на pre_checkout_query
Коли користувач натискає кнопку «Оплатити», сервер Telegram генерує подію pre_checkout_query. На цьому етапі потрібно за частку секунди перевірити:
- Чи є замовлення із зазначеним
invoice_payloadу вашій базі даних. - Чи збігається запитувана сума
total_amountз ціною у вашій системі. - Чи є товар на складі (чи цифровий доступ був придбаний раніше).
Якщо перевірка пройшла успішно, потрібно зателефонувати за допомогою способу answerPreCheckoutQuery з параметром ok = true. Якщо сталася помилка — передайте ok = false і вкажіть зрозумілий для людини текст у Можна визначати лише змінні, який буде відображатися клієнту в месенджері.
<?php
declare(strict_types=1);
/**
* Обработка pre_checkout_query и отправка ответа
*/
function handlePreCheckoutQuery(array $preCheckoutQuery, PDO $pdo, string $botToken): void
{
$queryId = (string)$preCheckoutQuery['id'];
$payload = (string)($preCheckoutQuery['invoice_payload'] ?? '');
$totalAmount = (int)($preCheckoutQuery['total_amount'] ?? 0);
$currency = (string)($preCheckoutQuery['currency'] ?? '');
// 1. Проверка валюты
if ($currency !== 'XTR') {
answerPreCheckoutQuery($queryId, false, 'Неподдерживаемая валюта', $botToken);
return;
}
// 2. Поиск заказа в БД
$stmt = $pdo->prepare("SELECT amount, status FROM orders WHERE payload = :payload LIMIT 1");
$stmt->execute([':payload' => $payload]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$order) {
answerPreCheckoutQuery($queryId, false, 'Заказ не найден в системе', $botToken);
return;
}
if ($order['status'] !== 'pending') {
answerPreCheckoutQuery($queryId, false, 'Заказ уже оплачен или отменен', $botToken);
return;
}
if ((int)$order['amount'] !== $totalAmount) {
answerPreCheckoutQuery($queryId, false, 'Сумма оплаты не совпадает', $botToken);
return;
}
//Все проверки пройдены
answerPreCheckoutQuery($queryId, true, null, $botToken);
}
function answerPreCheckoutQuery(string $queryId, bool $ok, ?string $errorMessage, string $botToken): void
{
$url = "https://api.telegram.org/bot{$botToken}/answerPreCheckoutQuery";
$postData = [
'pre_checkout_query_id' => $queryId,
'ok' => $ok ? 'true' : 'false',
];
if (!$ok && $errorMessage !== null) {
$postData['error_message'] = $errorMessage;
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$response = curl_exec($ch);
curl_close($ch);
}
Завершення покупки та обробка Successful_PAYMENT
Після успішного списання Зірок Telegram відправляє чергове повідомлення, що містить об 'єкт successful_payment. Важливо захистити точку входу від підробки через жатку X-Telegram-Bot-Api-Secret-Token і гарантують ідемпотентність через таблицю оброблених update_id.
<?php
declare(strict_types=1);
header('Content-Type: application/json');
$pdo = new PDO('mysql:host=localhost;dbname=bot_app;charset=utf8mb4', 'db_user', 'db_pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$secretToken = getenv('WEBHOOK_SECRET');
$botToken = getenv('BOT_TOKEN');
// 1. Защита вебхука через Secret Token
$incomingSecret = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '';
if ($secretToken && !hash_equals($secretToken, $incomingSecret)) {
http_response_code(403);
echo json_encode(['ok' => false, 'error' => 'Forbidden']);
exit;
}
$rawInput = file_get_contents('php://input');
if (!$rawInput) {
http_response_code(400);
exit;
}
$update = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE || !isset($update['update_id'])) {
http_response_code(400);
exit;
}
$updateId = (int)$update['update_id'];
// 2. Дедупликация апдейтов (Идемпотентность)
$stmt = $pdo->prepare("INSERT IGNORE INTO processed_updates (update_id, created_at) VALUES (:id, NOW())");
$stmt->execute([':id' => $updateId]);
if ($stmt->rowCount() === 0) {
// Повторный запрос от Telegram — просто отдаем 200 OK
http_response_code(200);
echo json_encode(['ok' => true, 'status' => 'already_processed']);
exit;
}
// 3. Маршрутизация событий
if (isset($update['pre_checkout_query'])) {
handlePreCheckoutQuery($update['pre_checkout_query'], $pdo, $botToken);
} elseif (isset($update['message']['successful_payment'])) {
$message = $update['message'];
$payment = $message['successful_payment'];
$payload = (string)$payment['invoice_payload'];
$chargeId = (string)$payment['telegram_payment_charge_id']; // Идентификатор платежа в Telegram Stars
$totalStars = (int)$payment['total_amount'];
// Атомарное обновление статуса заказа
$stmt = $pdo->prepare("
UPDATE orders
SET status = 'paid', telegram_charge_id = :charge_id, paid_at = NOW()
WHERE payload = :payload AND status = 'pending'
");
$stmt->execute([
':charge_id' => $chargeId,
':payload' => $payload,
]);
if ($stmt->rowCount() > 0) {
// Заказ успешно переведен в оплачен - выдаем цифровой товар или услугу
// sendMessage($message['chat']['id'], "Спасибо! Оплата принята.");
}
}
http_response_code(200);
echo json_encode(['ok' => true]);
Поширені помилки та підводні камені
- Вказати непотрібний provider_token: Якщо під час дзвінка
sendInvoiceз валютоюXTRпередати зовнішній токен еквайрингу (наприклад, YooKassa), Telegram поверне помилкуPAYMENT_PROVIDER_INVALID. Поле має містити порожній рядок. - Перевищено час очікування PRE_Checkout_QUERY: Якщо ваш сервер відповідає на попередню перевірку більше 10 секунд, Telegram скасовує угоду і показує користувачеві помилку. Вивести на сцену тривалі операції (запити до сторонніх API, генерацію ключів) після отримання
successful_payment. - Неперевірка тарифу в базі даних: Ніколи не довіряйте сумі
pre_checkout_queryбез узгодження з локальною базою даних. Зловмисник може створити занижену вартість, якщо ваш код не перевіряєtotal_amount. - Повторне надання послуги при повторному оновленні: Telegram може надіслати повідомлення через збої мережі
successful_paymentкілька разів. Обов 'язково перевіртеtelegram_payment_charge_idі оновити статус заказа атомно через умовуДЕ status = 'pending'.
Розробку складного платіжного функціоналу, Telegram Mini Apps та автоматизації продажів у месенджері можна доручити фахівцям BotCreator.