October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk9 min

Set Up a Secure and Idempotent Telegram Webhook in Pure PHP

A step-by-step PHP guide to registering a Telegram webhook, verifying the secret token header with hash_equals(), validating JSON, and preventing duplicate processing with a unique update_id table.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A secure, idempotent Telegram webhook in plain PHP needs five things: an HTTPS endpoint Telegram can reach, a high-entropy secret_token registered with setWebhook, a header check using hash_equals() before the body is trusted, a JSON decode with explicit error handling, and a database row keyed on update_id that commits in the same transaction as your bot’s side effects. Return a 2xx status only after that work is durably committed. Telegram retries any response outside the 2xx range, and it does not promise exactly-once delivery, so the endpoint must assume it will see the same update more than once.

How Telegram delivers updates and retries them

When a bot receives something, Telegram sends an HTTPS POST request to the URL you registered. The body is a JSON-serialized Update object. The Bot API reference describes the mechanism in these words: “Whenever there is an update for the bot, we will send an HTTPS POST request to the specified URL, containing a JSON-serialized Update.” (Telegram Bot API documentation, setWebhook description.)

As an Amazon Associate I earn from qualifying purchases.

Two behaviours shape the design. First, Telegram treats any HTTP response outside the 2xx class as unsuccessful and retries delivery. Second, the documentation does not guarantee that an update will be processed exactly once. A correct endpoint therefore has to accept repeats gracefully. Everything below follows from those two facts.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Step 1: Prepare a publicly reachable HTTPS endpoint

Telegram’s webhook guide requires TLS and a server that Telegram’s servers can reach over the public internet (Telegram webhooks guide). Before you register anything, confirm the following:

  • The URL uses https:// and has a certificate that Telegram accepts for that hostname.
  • The URL is served directly. The Telegram FAQ states that redirects are not supported (Telegram Bots FAQ), so a 301 from http:// to https:// or from a bare domain to www will break delivery.
  • The port in the URL is one Telegram supports. The documented ports are 443, 80, 88, and 8443. In practice, use the default HTTPS port 443 unless you have a specific reason not to.

The endpoint should accept only POST. Anything else should receive a 405 response, which is a deliberate non-2xx answer and is safe because Telegram only sends POST requests.

Step 2: Register the webhook with a secret token

The Bot API’s setWebhook method accepts an optional secret_token parameter of 1 to 256 characters, limited to letters, digits, underscores, and hyphens. When it is set, Telegram includes the value in the X-Telegram-Bot-Api-Secret-Token header on every update. The Telegram FAQ also suggests a secret path, but the header is the dedicated mechanism and should be your primary check.

Generate a token with enough entropy. A 64-character hexadecimal string from OpenSSL fits within the allowed character set and length:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
openssl rand -hex 32

Register the webhook from a deployment script or an administrative shell, not from a public page. Replace the bot token and secret with values from your environment variables, not literal strings. The command below assumes both are exported as BOT_TOKEN and WEBHOOK_SECRET:

curl -sS -X POST "https://api.telegram.org/bot${BOT_TOKEN}/setWebhook" 
  --data-urlencode "url=https://bot.example.com/telegram/webhook.php" 
  --data-urlencode "secret_token=${WEBHOOK_SECRET}" 
  --data-urlencode "max_connections=40"

The max_connections value limits how many concurrent HTTPS connections Telegram opens to your endpoint. Choose it based on how many requests your PHP workers can handle at once. A check on the response of setWebhook confirms only that Telegram accepted the configuration, not that delivery works. Verify delivery separately in Step 7.

Step 3: Authenticate the request before reading its body

Read the received header first and compare it against the server-side secret. Use hash_equals(), which the PHP manual describes as a timing-attack-safe string comparison. Its signature is hash_equals(string $known_string, string $user_string), so pass your stored secret first and the received value second (PHP manual: hash_equals).

Under PHP-FPM or Apache, the header typically appears in $_SERVER as HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN. Some proxies and server configurations normalize or strip header names, so confirm the mapping in your own environment with a test request before relying on it. An absent header and an empty string both fail the check.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Store the expected secret in an environment variable or a file readable only by the PHP process user. Do not commit it to source control, do not echo it, and do not write it to logs.

Step 4: Read and validate the JSON body

Read the raw body with file_get_contents('php://input'), not from $_POST, because Telegram sends JSON. Telegram’s official Hello Bot sample uses this raw-body and json_decode pattern (Telegram Hello Bot sample), but that sample illustrates the API and does not include the security, concurrency, or idempotency handling described here.

Decode with JSON_THROW_ON_ERROR so malformed input raises an exception you can handle. Then check the shape. The Update’s update_id is an integer, and it is the field the deduplication logic depends on:

$raw = file_get_contents('php://input');
if ($raw === false || $raw === '' || strlen($raw) > 1048576) {
    respond(413, 'payload rejected');
}

try {
    $update = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    respond(400, 'invalid json');
}

if (!is_array($update) || !isset($update['update_id']) || !is_int($update['update_id'])) {
    respond(400, 'invalid update');
}

Do not treat filter_input() as validation. PHP’s default filter is FILTER_UNSAFE_RAW, which performs no filtering (PHP manual: filter_input). Validate the specific fields your bot uses, including their types, and reject anything unexpected at the boundary. For JSON function details, see the PHP JSON functions reference.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Step 5: Make each update idempotent

Telegram’s documentation does not promise exactly-once delivery, so the design must make repeated processing harmless. The approach below uses update_id as the idempotency key and stores it in a table with a unique constraint. A database constraint is safer than a separate “check, then insert” sequence, because two simultaneous deliveries can both pass a check before either inserts. Only one insert can win the constraint.

Create the table with a primary key and an InnoDB engine (or the equivalent transactional engine in your database):

CREATE TABLE processed_updates (
    update_id BIGINT NOT NULL PRIMARY KEY,
    received_at DATETIME NOT NULL
) ENGINE=InnoDB;

Open a PDO connection with exceptions enabled, then wrap the insert and the business changes in one transaction. PDO’s transaction support depends on the driver and the storage engine; the PHP manual warns that transactions do not behave as expected when the database does not support them (PHP manual: PDO transactions). Confirm this with your engine before you depend on it.

$pdo = new PDO($dsn, $dbUser, $dbPass, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES => false,
]);

$pdo->beginTransaction();
try {
    $mark = $pdo->prepare('INSERT INTO processed_updates (update_id, received_at) VALUES (:id, NOW())');
    $mark->execute([':id' => $update['update_id']]);
} catch (PDOException $e) {
    $pdo->rollBack();
    if ($e->getCode() === '23000' || $e->getCode() === '23505') {
        respond(200, 'duplicate, already processed');
    }
    respond(500, 'temporary failure');
}

try {
    // Your bot logic goes here. Write domain changes through $pdo so they
    // commit or roll back together with the processed_updates row.
    $pdo->commit();
} catch (Throwable $e) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }
    respond(500, 'temporary failure');
}

respond(200, 'ok');

The SQLSTATE values shown are the standard integrity-constraint codes for MySQL/MariaDB (23000) and PostgreSQL (23505). If you use another engine, check its duplicate-key error code.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Two boundaries matter here. Database writes roll back with the transaction, but messages sent through the Bot API do not. If your handler calls sendMessage before commit(), and the commit then fails, the user has already received the message and Telegram will redeliver the update. Either send outgoing messages after a successful commit, or make the outgoing call safe to repeat, for example by recording its own identifier before sending.

Step 6: Choose response codes deliberately

Because Telegram retries every non-2xx response, your status codes control redelivery. Map each outcome to a deliberate answer:

Situation HTTP response Telegram behaviour
Header missing or mismatched 403 Treated as unsuccessful; retried. A correctly configured Telegram never produces this, so it signals a misconfiguration or a forged request.
Body is not valid JSON, or fails shape validation 400 Retried. If the payload can never be valid, log it and return 200 instead, so it is not retried forever.
Update already recorded (duplicate) 200 Accepted. No side effects are repeated.
Update processed and committed 200 Accepted.
Temporary failure (database unavailable, deadlock, timeout) 500 Retried, which is the intended recovery path.

Synchronous processing or queued processing

The handler above does its work inside the HTTP request. That is simple and keeps the transaction boundary clear, but it means Telegram waits for your business logic. For slow work, such as calling external APIs or generating large files, record the update first and return 200 immediately, then process it in a worker with its own retry and idempotency rules.

Factor Synchronous in the request Durable enqueue, then background work
Response latency Grows with business logic time Short; the endpoint only writes and acknowledges
Failure and retry A failure returns non-2xx, and Telegram redelivers The worker retries; the endpoint has already acknowledged
Transaction boundary The dedupe row and business changes commit together The queue row commits, then the worker needs its own idempotent processing
Operational needs Only PHP and a database A worker process or scheduled job, plus monitoring of the queue

Neither pattern is universally better. Choose based on workload and what your hosting provides. A database-backed dedupe table works with either.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Webhook or getUpdates polling

A webhook pushes updates to a reachable server. The alternative is getUpdates, which your bot polls. Telegram states that polling cannot be used while an outgoing webhook is set. If you move from a webhook back to polling, remove the webhook first with the deleteWebhook method. This article covers only the webhook side.

Diagnose delivery with getWebhookInfo

A successful setWebhook call does not prove that updates reach your server. Call getWebhookInfo and inspect the returned fields:

curl -sS "https://api.telegram.org/bot${BOT_TOKEN}/getWebhookInfo"
  • url: confirm it matches the endpoint you intended, character for character.
  • pending_update_count: a number that keeps growing means updates are queued and not being acknowledged.
  • last_error_date and last_error_message: the most recent delivery failure. Typical causes are a certificate problem, a redirect, a closed port, or a timeout.
  • Synchronization error information: shows problems Telegram reports while delivering to the configured URL.

Do not paste the response publicly without removing your bot token. The response itself does not include the bot token, but the request URL does.

Common mistakes to avoid

  • Relying on a secret path alone. A secret URL path can help as defence in depth, but the secret_token header is the dedicated check. Use both if you want, and keep the path out of public documentation.
  • Returning 200 before anything is recorded. If the process crashes after the response, the update is lost. Acknowledge only after the transaction commits or the update is durably queued.
  • Returning an error after side effects have committed. Telegram will redeliver, and the side effects will repeat unless the dedupe row commits with them.
  • Using an in-memory array or a cache as the only dedupe store. Memory is lost on restart and is not shared across PHP workers. Use a durable table with a unique constraint.
  • Assuming filter_input() sanitizes values by default. It does not, as noted in Step 4.
  • Shipping the Hello Bot sample as the handler. It demonstrates the API, not a production design.

Work through the checks in Steps 1 to 4 once in a staging environment, then run getWebhookInfo after each deployment that changes the URL, the certificate, or the secret.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Sources: Telegram Bot API (the current reference revision, 10.3, is dated 24 August 2026), Telegram webhooks guide, Telegram Bots FAQ, Telegram Hello Bot sample, PHP manual: hash_equals, PHP manual: PDO transactions, PHP manual: filter_input, PHP manual: JSON functions.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.