Прийом платежів Telegram Stars на PHP: sendInvoice, pre_checkout_query та successful_payment

Екосистема Telegram Stars (XTR) дозволяє легко продавати цифрові товари, підписки та преміум-контент всередині месенджера. На відміну від класичних платіжних провайдерів (YuKassa, Stripe), покупка Stars відбувається без перенаправлення на зовнішні форми — інтерфейс вбудований безпосередньо в клієнт Telegram. Однак технічний протокол вимагає суворого дотримання двоетапного підтвердження покупки на вашому сервері.

У цьому посібнику ми проаналізуємо архітектуру роботи з Telegram Stars в чистому PHP, напишемо надійного клієнта для генерації рахунків-фактур, реалізуємо валідацію передкасових запитів та забезпечимо ідемпотентність при завершенні транзакції.

Архітектурний платіжний цикл у Telegram Stars

Процес оплати Telegram Stars складається з п 'яти послідовних кроків:

  1. Ініціалізувати Сервер ботів створює рахунок за допомогою методу sendInvoice або створює посилання на рахунок-фактуру за допомогою createInvoiceLink.
  2. Клік користувача: Користувач натискає кнопку «Оплатити» в чаті або міні-додатку.
  3. Попереднє оформлення замовлення: Telegram надсилає вебхук pre_checkout_query на ваш сервер. Сервер повинен підтвердити протягом 10 секунд, що він готовий відправити товар або відхилити транзакцію з поясненням причини.
  4. Списання Telegram списує зірки з балансу користувача.
  5. Завершення (успішна оплата): 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.

Нові статті — у Telegram

Розбираємо, що автоматизувати в бізнесі та як це працює на практиці. Без спаму.