Secure Telegram Stars Payment Flow in PHP: sendInvoice pre_checkout_query successful_payment

2026-11-09来源 dev.toEnglish

Architecture Overview

A Telegram Stars payment flow has three distinct phases. First, the bot sends an sendInvoice request containing a unique lead identifier and an HMAC-protected initData. The lead identifier is a 14-character hex string generated with bin2hex(random_bytes(7)) and stored in a database. Second, when the user taps the MainButton, the bot calls pre_checkout_query to verify the invoice hasn't been replayed, the bot token is still valid, and the auth_date is fresh. Third, on successful payment, successful_payment is invoked to mark the invoice as paid, update the lead state, and send a confirmation. If any step is skipped or validated incorrectly, an attacker can forge an initData and redirect funds to their own account.

sendInvoice — Create the Invoice

The sendInvoice function creates a Telegram Invoice object and returns the data field that the bot will send to the user. The critical part is the initData string: it must contain the lead identifier, amount, timestamp, and an HMAC signature. The HMAC is computed over a canonical JSON payload and signed with the bot's secret key. This signature is embedded in initData so the server can verify it later without trusting the front-end.

<?php require 'vendor/autoload.php'; use GuzzleHttp\Client; use Telegram\Bot\Client as TelegramClient; use Telegram\Bot\Message\Invoice; // Configuration – keep these in environment variables $botToken = getenv('BOT_TOKEN'); $secretKey = getenv('BOT_SECRET_KEY'); // HMAC key for initData signing function sendInvoice($leadId, $amount) { // 1. Generate a unique hex lead identifier $leadId = bin2hex(random_bytes(7)); // 2. Build the canonical payload for HMAC $payload = json_encode([ 'lead_id' => $leadId, 'amount' => (int)$amount, 'timestamp' => time(), ], JSON_PRETTY_PRINT); // 3. HMAC-SHA256 signature $hmac = hash_hmac('sha256', $payload, $secretKey); // 4. Assemble initData with HMAC $initData = sprintf( 'init=%s|amount=%d|timestamp=%d|hmac=%s', $payload, $hmac, time() ); // 5. Create and send the invoice via Telegram Bot API $client = new TelegramClient($botToken, new \TelegramClient($botToken)); $client->setWebhook('https://yourdomain.com/star-webhook'); $invoice = new Invoice([ 'data' => $initData, 'amount' => $amount, 'currency' => 'USDT', ]); $response = $client->sendInvoice([ 'chat_id' => $leadId, 'data' => $invoice->data, ]); return $response->getMessage(); } 

If the HMAC verification fails on the server, the invoice is never sent to the user and the bot should respond with an error (e.g., “Invalid invoice”). This prevents an attacker from crafting a fake initData that points to a different lead or a higher amount.

pre_checkout_query — Validate the Invoice

Before processing a payment, the bot must call pre_checkout_query to ensure the invoice is still valid and hasn't been replayed. This function performs three checks: the auth_date must be recent (within 30 minutes), the initData HMAC must match the stored value, and the update_id must not have been processed before. If any check fails, the invoice is rejected and the user is shown an error.

<?php function pre_checkout_query($leadId, $expectedInvoiceData) { // Load the invoice from database (simplified) $invoice = getInvoiceByLeadId($leadId); if (!$invoice) { return false; } // 1. Reject stale auth_date (older than 30 minutes) $authDate = (int)invoice->getAuthDate(); if (time() - $authDate > 1800) { return false; } // 2. Verify the HMAC of initData $payload = $invoice->getInitData(); $computedHmac = hash_hmac('sha256', $payload, $secretKey); if (!hash_equals($computedHmac, $payload)) { return false; } // 3. Idempotency check: ensure update_id hasn't been processed $updateId = $invoice->getUpdateId(); if (is_processed($updateId)) { return false; } // Mark as processed to prevent replay markProcessed($updateId); return true; } 

Note that getInvoiceByLeadId, is_processed, and markProcessed are assumed to be helper functions that query a database table with a unique constraint on update_id. The update_id acts as an idempotency key; if the same callback arrives twice, the second attempt will be rejected.

successful_payment — Handle the Payment Callback

When the user completes a payment, Telegram sends an answerCallbackQuery with callback_data that encodes the invoice ID. The successful_payment function receives this callback, looks up the invoice, and marks it as paid. It also updates the lead state to "paid" and optionally sends a confirmation message to the user.

<?php function successful_payment($callbackData) { $invoiceId = extract('invoice_id', $callbackData); $invoice = getInvoiceById($invoiceId); if (!$invoice) { // Invoice not found – possible replay or deleted invoice return; } // Mark the invoice as successfully paid $invoice->setStatus('paid'); $invoice->setUpdatedAt(time()); $invoice->save(); // Update the lead in the database $lead = getLeadById($invoice->getLeadId()); if ($lead) { $lead->setStatus('paid'); $lead->setUpdatedAt(time()); $lead->save(); } // Send a confirmation to the user (optional) sendConfirmation($invoice->getLeadId(), 'Payment received'); } 

If the payment fails (e.g., the user cancels or the network drops), the answerCallbackQuery will still be received. The function should not mark the invoice as paid in that case. Only when the Telegram API confirms the payment (via answerCallbackQuery with a success response) should the state be updated.

What to Check on the Server

When implementing the flow, verify these server-side checks:

  1. HMAC integrity – Always recompute the HMAC over the canonical initData and compare it with the value sent by the client. A mismatched HMAC means the initData was tampered with.
  2. Auth date freshness – Reject invoices where auth_date is older than 30 minutes. This prevents replay attacks from old, cached invoices.
  3. Idempotency via update_id – Store each processed update_id in a database table with a unique constraint. Before processing a callback, check that the update_id hasn't been seen before.
  4. Bot token verification – Ensure the callback_data is generated by your bot (e.g., t.me/YourBot?start=invoice_123). The answerCallbackQuery endpoint should validate the bot token before processing.
  5. Amount and currency validation – Confirm the amount matches the lead's pricing table and that the currency is supported.
  6. Database transaction safety – Wrap invoice creation and status updates in a transaction to ensure atomicity. If the payment fails after the invoice is marked paid, you can roll back and avoid partial states.
  7. Logging – Log every invoice creation, validation, and payment event with the update_id and timestamp. This makes it possible to audit and recover from disputes.

Typical Errors and How to Avoid Them

  • Missing HMAC – If you send initData without an HMAC, an attacker can modify it to point to a different lead or a higher amount. Always sign initData and verify on the server.
  • Stale auth_date – Without a freshness check, an old invoice could be replayed indefinitely. Enforce a tight window (e.g., 30 minutes) and reject older ones.
  • Replay attacks – Without idempotency, the same update_id could be reused to mark an invoice as paid multiple times. Store update_id in a DB table with a unique index and check before processing.
  • Callback spoofing – If you accept any callback_data without verifying the bot token, a malicious user can trigger the payment flow. Always validate the bot token on the server.
  • Insufficient logging – Log every step with the update_id and timestamp. This makes it possible to audit and recover from disputes.
  • Incorrect HMAC algorithm – Using md5 or sha1 instead of sha256 weakens security. Always use hash_hmac('sha256', ...).
  • Not handling payment failures – If the Telegram API returns an error (e.g., 400 for invalid payment), the function should not mark the invoice as paid. Only on a successful response should the state transition occur.

Production Notes

  • Webhook secret – Store your webhook secret in getenv('WEBHOOK_SECRET') and verify it on every incoming request. Reject requests that don't match.
  • Idempotency storage – Use a database table (or Redis) to track processed update_id values. The table should have a TTL to prevent unbounded growth.
  • Database schema hints – The invoice table should have columns id, lead_id, amount, currency, status, auth_date, init_data, update_id, created_at, updated_at. The update_id column must be unique and indexed.
  • Rate limiting – Protect the /star-webhook endpoint from abuse by rate-limiting based on chat_id or IP address. A common pattern is to allow only one callback per lead per minute.
  • Error handling – Return appropriate HTTP status codes (e.g., 400 for invalid HMAC, 403 for missing bot token) and log errors for debugging.
  • Graceful degradation – If the database is unavailable, do not process the invoice. Return an error to the user and queue the invoice for later processing.

By following this three-step flow — sendInvoice → pre_checkout_query → successful_payment — you ensure that only genuine Telegram users can complete a Stars payment, and you prevent the most common attack vectors: spoofed initData, replayed invoices, and unauthorized payment callbacks. The combination of HMAC signing, freshness checks, and idempotency is what makes the flow robust in production.

BotCreator — studio that ships Telegram bots / Mini Apps.

原文:https://www.a8q9.cn/en/article/secure-telegram-stars-payment-flow-in-php-sendinvoice-pre-checkout-query-success-b13dd0.html

相关文章