Developing a Telegram bot locally often involves more than just dealing with localhost. In many real-world network setups, a developer’s machine is not reachable from the internet at all: it may sit behind a home router, corporate firewall, ISP-level CGNAT, mobile network, or VPN.
Even if you already have an HTTP server running at http://127.0.0.1:3000, Telegram cannot connect to it directly. Telegram webhooks require a public HTTPS URL that is reachable from the internet. Traditionally, that means obtaining a public IP address, opening an inbound firewall port, configuring NAT or port forwarding on a router, registering a domain, and issuing a TLS certificate.
For local development, that infrastructure is unnecessary overhead. In this guide, Telegram will send updates to an Adal Endpoint, and Adal will deliver them to your local handler through an outbound Adal CLI connection. Your computer can remain behind NAT and a firewall: no public IP address, open inbound ports, or self-hosted HTTPS server are required.
What you will build
The final setup will look like this:
Telegram user
↓
Telegram Bot API
↓ HTTPS POST
Public Adal Endpoint
↓
Outbound Adal CLI connection
↓
Developer machine behind NAT / firewall
↓
http://127.0.0.1:3000/telegram
Telegram only communicates with the public HTTPS Endpoint in Adal. Your computer does not accept any inbound connections from the internet. Instead, Adal CLI establishes an outbound connection to Adal and receives webhook deliveries for the local Destination through that connection.
This works even when:
your computer is connected to home Wi-Fi behind a router;
your ISP uses CGNAT and does not provide a public IPv4 address;
opening inbound ports is prohibited on your work computer;
the application runs inside a local Docker container;
you are working through a VPN, mobile network, or restricted corporate network.
Why NAT and firewalls prevent direct Telegram webhook delivery
When Telegram sends a webhook, it needs to open an HTTPS connection to your server. That server must be publicly reachable from the internet.
A local machine, however, is usually part of a private network:
Telegram
↓
Router or ISP public IP address
↓
NAT / firewall
↓
Your local computer: 192.168.x.x or 10.x.x.x
Addresses such as 192.168.x.x, 10.x.x.x, 172.16.x.x, and 127.0.0.1 are not routable from the public internet. Even if you know your router’s external IP address, an inbound connection may be blocked by a firewall or fail to reach the correct machine without explicit NAT configuration.
Adal reverses the direction of the connection. Instead of Telegram connecting to your computer, your computer connects to Adal:
Your computer → Adal CLI → Adal
Telegram → public Adal Endpoint
Outbound HTTPS and WebSocket connections are typically allowed even on networks that block inbound connections. This lets your local machine receive Telegram webhooks while remaining completely inaccessible to direct traffic from the internet.
What you need
To follow this guide, you will need:
a Telegram bot created through
@BotFather;your bot token;
an Adal account;
Adal CLI installed and authenticated;
a local HTTP server that accepts
POSTrequests;Node.js, PHP, Python, or another runtime for your application.
The example below uses Node.js, but the overall setup is independent of the programming language or framework you use.
1. Create a Telegram bot and save its token
Open @BotFather in Telegram and run:
/newbot
BotFather will ask you to choose a bot name and username, then provide a token similar to this:
123456789:AAExampleTokenThatMustRemainPrivate
Do not commit this token to Git, send it in messages, or expose it in frontend code. It grants full access to your bot through the Telegram Bot API.
For local development, store it in an environment variable:
export TELEGRAM_BOT_TOKEN='123456789:AAExampleTokenThatMustRemainPrivate'
In Windows PowerShell:
$env:TELEGRAM_BOT_TOKEN = '123456789:AAExampleTokenThatMustRemainPrivate'
2. Start a local Telegram webhook handler
Create a file named server.mjs:
import http from 'node:http';
const port = 3000;
const webhookSecret = process.env.TELEGRAM_WEBHOOK_SECRET;
const server = http.createServer(async (request, response) => {
if (request.method !== 'POST' || request.url !== '/telegram') {
response.writeHead(404);
response.end('Not found');
return;
}
const receivedSecret =
request.headers['x-telegram-bot-api-secret-token'];
if (!webhookSecret || receivedSecret !== webhookSecret) {
response.writeHead(401);
response.end('Unauthorized');
return;
}
let body = '';
request.on('data', (chunk) => {
body += chunk;
});
request.on('end', () => {
try {
const update = JSON.parse(body);
console.log('Telegram update received:');
console.dir(update, { depth: null });
const message = update.message;
if (message?.text) {
console.log(
`Message from ${message.from.username ?? message.from.id}: ${message.text}`
);
}
response.writeHead(200, {
'content-type': 'application/json',
});
response.end(JSON.stringify({ ok: true }));
} catch (error) {
console.error('Failed to process Telegram update:', error);
response.writeHead(400, {
'content-type': 'application/json',
});
response.end(JSON.stringify({ ok: false }));
}
});
});
server.listen(port, '127.0.0.1', () => {
console.log(`Listening on http://127.0.0.1:${port}/telegram`);
});
Generate a separate secret to verify requests from Telegram:
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='
Store the generated value in an environment variable:
export TELEGRAM_WEBHOOK_SECRET='your-random-secret'
Then start the server:
node server.mjs
You should see the following output:
Listening on http://127.0.0.1:3000/telegram
Your local server is now ready to receive Telegram updates, but it is still accessible only from your computer. The next step is to create a public receiving endpoint in Adal.
3. Create an Endpoint for Telegram in Adal
Open the Adal Dashboard and create a new Endpoint.
The following settings are recommended:
Name:
telegram-localAllowed methods:
POSTDeliver pending requests on connect: enabled
Notes:
Telegram Bot API updates for local development
After creating the Endpoint, Adal will provide a permanent public URL. It will look similar to this:
https://your-endpoint.se1.adal.cloud
This is the URL you will configure as the webhook URL in the Telegram Bot API.
Do not add a local path such as /telegram to this URL unless you have explicitly configured path routing in your Destination. In this guide, the public Endpoint URL remains at the root path, while /telegram is used only inside the local application.
When you create the Endpoint, Adal will also provide a CLI token. Save it immediately, because it is shown only once. If you lose it, it cannot be recovered; you will need to generate a new token.
4. Create a local Destination through Adal CLI
Within the Endpoint, create a Destination that delivers requests through Adal CLI to the following local address:
http://127.0.0.1:3000/telegram
Create a CLI token for this Destination and connect the local Adal CLI client using the command shown in the Dashboard after the token is created.
Once the connection is established, the Destination should appear as online.
At this point, Adal CLI establishes an outbound connection to Adal. This is the key part of the setup: Telegram and other external services never connect to your computer directly.
Your computer may be behind NAT, a home router, a corporate firewall, VPN, or CGNAT. It does not need a public IP address, port forwarding, an open inbound port, a domain name, or its own TLS certificate. It only needs to be able to establish outbound connections to Adal.
5. Set the webhook in Telegram
Create an environment variable containing the Adal Endpoint URL:
export ADAL_ENDPOINT_URL='https://your-endpoint.se1.adal.cloud'
Then call the setWebhook method:
curl -sS -X POST \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/setWebhook" \
-d "url=${ADAL_ENDPOINT_URL}" \
-d 'allowed_updates=["message","callback_query"]' \
-d "secret_token=${TELEGRAM_WEBHOOK_SECRET}"
When the webhook is configured successfully, Telegram will return:
{
"ok": true,
"result": true,
"description": "Webhook was set"
}
The secret_token parameter is especially important. Telegram includes its value in every webhook request through the following header:
X-Telegram-Bot-Api-Secret-Token
The local handler in the example above verifies this header and rejects requests that do not contain the correct secret.
6. Verify that the webhook works
Open a chat with your bot and send it a message:
/start
Your local application should print a Telegram update in the console:
Telegram update received:
{
update_id: 123456789,
message: {
message_id: 1,
from: {
id: 12345678,
is_bot: false,
first_name: "Yuriy"
},
chat: {
id: 12345678,
type: "private"
},
date: 1782921600,
text: "/start"
}
}
At the same time, the Adal Dashboard will show the incoming webhook and its delivery status for your local Destination.
This is useful not only for initial setup, but also for debugging. You can inspect request headers, the request body, and delivery status, then replay a specific update to your local application when needed.
How to check the current Telegram webhook configuration
Telegram provides the getWebhookInfo method for viewing the current webhook status:
curl -sS \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getWebhookInfo"
A successful response may look like this:
{
"ok": true,
"result": {
"url": "https://your-endpoint.se1.adal.cloud",
"has_custom_certificate": false,
"pending_update_count": 0,
"max_connections": 40,
"allowed_updates": [
"message",
"callback_query"
]
}
}
Pay particular attention to the following fields:
url— the address Telegram is currently sending updates to;pending_update_count— the number of updates waiting to be delivered;last_error_message— the most recent delivery error;last_error_date— when the most recent error occurred;allowed_updates— the types of updates the bot is configured to receive.
If url is empty, no webhook is configured. The bot may be using long polling instead, or it may not be receiving updates at all.
How to receive Telegram webhooks behind NAT and a firewall
Addresses such as localhost, 127.0.0.1, and ::1 exist only on the computer where the application is running. From Telegram’s perspective, localhost does not point to your MacBook, work laptop, or Docker container. It points to Telegram’s own infrastructure.
But the problem is not limited to localhost. Even a server available on your home network at an address such as 192.168.1.10 cannot be reached directly from the internet. These addresses sit behind a router’s NAT and are not routable on the public internet.
Without Adal, allowing Telegram to reach a local computer would usually require you to:
obtain a public IP address;
configure port forwarding on the router;
open an inbound firewall port;
configure public DNS;
issue and maintain a TLS certificate;
ensure that the network address and routing rules do not change.
In a corporate network, behind CGNAT, or on a mobile connection, some of these steps may not be possible at all.
With Adal, none of this infrastructure is required. Telegram sends the webhook to a public HTTPS Endpoint in Adal, and Adal forwards the request to your local application through the already established outbound Adal CLI connection.
The delivery flow is as follows:
Telegram sends an update to the public HTTPS Endpoint in Adal.
Adal receives and stores the incoming webhook.
Adal creates a delivery for the connected local Destination.
Adal CLI receives that delivery through the outbound connection from your computer.
The CLI forwards the HTTP request to
http://127.0.0.1:3000/telegram.Your handler responds as though Telegram had connected to it directly.
As a result, Telegram webhooks can work on a computer behind NAT and a firewall without opening any inbound ports.
What happens when the local computer is offline
Telegram will continue sending updates to the Adal Endpoint because the public URL remains available even when your laptop is offline.
When the local Destination is unavailable, the delivery is not immediately lost. Adal records it in the delivery history and handles it according to the configured retry policy. Once Adal CLI reconnects, pending webhook deliveries can be sent to your local application.
This is particularly useful during development: you can close your laptop, return to the task later, reconnect the CLI, and inspect the updates that arrived while your local environment was unavailable.
Your bot’s business logic should still account for duplicate delivery. Telegram updates may be delivered more than once, so your handler should be idempotent. At a minimum, store each update_id and avoid performing the same action twice.
How to disable a Telegram webhook
To switch back to long polling with getUpdates, remove the webhook first:
curl -sS -X POST \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/deleteWebhook"
To remove the webhook and discard any pending updates at the same time, pass drop_pending_updates:
curl -sS -X POST \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/deleteWebhook" \
-d 'drop_pending_updates=true'
Use this option carefully: it deletes unprocessed updates held by Telegram.
Common issues
Telegram returns 409 Conflict
This usually means that your application is trying to use getUpdates while a webhook is already configured.
A Telegram bot cannot use webhooks and long polling at the same time. Check the current configuration:
curl -sS \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getWebhookInfo"
If url is not empty, either disable polling in your application or remove the webhook.
Adal received the webhook, but the local application does not receive it
Check the following:
Adal CLI is connected and the Destination is online.
The local server is listening on
127.0.0.1:3000.The Destination is configured with the correct address:
http://127.0.0.1:3000/telegram
Also review the delivery history in Adal. It will show the HTTP status code, connection error, or timeout returned by the local handler.
The local application returns 401 Unauthorized
This means that TELEGRAM_WEBHOOK_SECRET does not match the secret configured in Telegram.
Make sure that the same value is used in both places:
the environment variable used by the local application;
the
secret_tokenparameter passed tosetWebhook.
After changing the secret, call setWebhook again so Telegram starts sending the new header value.
Telegram reports a webhook delivery error
Call getWebhookInfo and inspect the last_error_message field.
If the Adal Endpoint receives the request but the local delivery fails, the relevant error will usually appear in the delivery history for that webhook. This makes it easier to separate Telegram → Adal connectivity issues from Adal → localhost delivery issues.
Conclusion
Telegram Bot API requires a public HTTPS URL for webhook delivery, but your local computer does not need to be publicly accessible.
With Adal, you can receive Telegram webhooks on a machine behind NAT, a home router, corporate firewall, CGNAT, VPN, or mobile network. You do not need a public IP address, port forwarding, inbound firewall rules, a domain, or a TLS certificate on the local machine.
Create an Endpoint in Adal, connect a local Destination through Adal CLI, and configure the Endpoint URL with setWebhook. Telegram sends updates to Adal, while your application receives them on localhost through an outbound connection.
This approach lets you develop and debug Telegram bots locally in a restricted network without changing your computer’s network configuration or exposing it to direct access from the internet.