Recommended Free Tools
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.
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:
#1 Best Overall
- 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://tohttps://or from a bare domain towwwwill 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:
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.
Rank #2
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.
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.
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.
Rank #4
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.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →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_tokenheader 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.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchSources: 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.
Quick Recap
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.




