A two-way bot lets strangers contact you through a bot rather than your personal account. You receive their messages and reply through the same bot. This can support a small help desk or personal contact channel.
Create a Worker and KV binding
Open Cloudflare KV and create a namespace. In Workers & Pages, create a Worker and bind the namespace using the exact variable name telegrambot. Creating the namespace alone is not enough.
Add ENV_BOT_TOKEN and ENV_BOT_SECRET as secrets, and ENV_ADMIN_UID as your own numeric Telegram user ID. Use @BotFather and /newbot to obtain your bot token. The original ID helper was @GetIDcnBot.

Generate a random secret locally or with a trusted tool. The original post linked a UUID generator. This secret protects both Telegram webhook requests and setup actions; keep it private.
Deploy the relay
Replace the Worker code with the module below. It adapts the original relay to module-style bindings, protects webhook-management routes, and requires the administrator to reply to a relayed message. That avoids accidentally sending a reply to the most recently active stranger.
function makeHandler(env) {
const TOKEN = env.ENV_BOT_TOKEN;
const WEBHOOK = '/endpoint';
const SECRET = env.ENV_BOT_SECRET;
const ADMIN_UID = env.ENV_ADMIN_UID;
const KV_NAMESPACE = env.telegrambot;
const LAST_USER_KEY = 'last_user';
const USER_MESSAGES_KEY_PREFIX = 'user_message_';
const ADMIN_RESPONSES_KEY_PREFIX = 'admin_response_';
return async function handle(request) {
const event = { request };
const url = new URL(request.url);
if (url.pathname === WEBHOOK && request.method === 'POST') return handleWebhook(event);
if (url.pathname === '/registerWebhook' || url.pathname === '/unRegisterWebhook') {
if (request.headers.get('Authorization') !== 'Bearer ' + SECRET) return new Response('Unauthorized', { status: 403 });
if (url.pathname === '/registerWebhook') return registerWebhook(event, url, WEBHOOK, SECRET);
return unRegisterWebhook(event);
}
return new Response('Not found', { status: 404 });
};
async function handleWebhook(event) {
if (event.request.headers.get('X-Telegram-Bot-Api-Secret-Token') !== SECRET) {
return new Response('Unauthorized', { status: 403 });
}
const update = await event.request.json();
await onUpdate(update);
return new Response('Ok');
}
async function onUpdate(update) {
if ('message' in update) {
await onMessage(update.message);
}
}
async function onMessage(message) {
const chatId = message.chat.id;
const userName = message.from.username ? `@${message.from.username}` : message.from.first_name;
if (chatId == ADMIN_UID) {
// Relay or record the message.
let userChatId;
if (message.reply_to_message) {
const repliedMessageId = message.reply_to_message.message_id;
userChatId = await KV_NAMESPACE.get(`admin_message_${repliedMessageId}`);
if (!userChatId) {
await sendPlainText(ADMIN_UID, 'The recipient mapping is unavailable. Ask the user to send another message, then reply to it.');
return;
}
} else {
// Relay or record the message.
userChatId = null; // Require an explicit reply to avoid routing to the wrong person.
if (!userChatId) {
await sendPlainText(ADMIN_UID, 'Reply to a relayed message to choose a recipient.');
return;
}
}
// Relay or record the message.
let responseText = '';
if (message.photo) {
const photo = message.photo[message.photo.length - 1];
await sendPhoto(userChatId, photo.file_id);
responseText = `Admin sent a photo: ${photo.file_id}`;
} else if (message.sticker) {
await sendSticker(userChatId, message.sticker.file_id);
responseText = `Admin sent a sticker: ${message.sticker.file_id}`;
} else if (message.voice) {
await sendVoice(userChatId, message.voice.file_id);
responseText = `Admin sent a voice message: ${message.voice.file_id}`;
} else if (message.document) {
await sendDocument(userChatId, message.document.file_id);
responseText = `Admin sent a document: ${message.document.file_id}`;
} else if (message.video) {
await sendVideo(userChatId, message.video.file_id);
responseText = `Admin sent a video: ${message.video.file_id}`;
} else if (message.location) {
await sendLocation(userChatId, message.location.latitude, message.location.longitude);
responseText = `Admin sent a location: latitude ${message.location.latitude}, longitude ${message.location.longitude}`;
} else {
const text = message.text || 'Received a non-text message';
await sendPlainText(userChatId, text);
responseText = text;
}
await KV_NAMESPACE.put(`${ADMIN_RESPONSES_KEY_PREFIX}${userChatId}`, responseText, { expirationTtl: 86400 });
} else {
// Relay or record the message.
let userMessageText = '';
let response;
if (message.photo) {
const photo = message.photo[message.photo.length - 1];
userMessageText = `User sent a photo: ${photo.file_id}`;
response = await sendPhoto(ADMIN_UID, photo.file_id, `Photo from ${userName}`);
} else if (message.sticker) {
userMessageText = `User sent a sticker: ${message.sticker.file_id}`;
response = await sendSticker(ADMIN_UID, message.sticker.file_id);
} else if (message.voice) {
userMessageText = `User sent a voice message: ${message.voice.file_id}`;
response = await sendVoice(ADMIN_UID, message.voice.file_id);
} else if (message.document) {
userMessageText = `User sent a document: ${message.document.file_id}`;
response = await sendDocument(ADMIN_UID, message.document.file_id);
} else if (message.video) {
userMessageText = `User sent a video: ${message.video.file_id}`;
response = await sendVideo(ADMIN_UID, message.video.file_id);
} else if (message.location) {
userMessageText = `User sent a location: latitude ${message.location.latitude}, longitude ${message.location.longitude}`;
response = await sendLocation(ADMIN_UID, message.location.latitude, message.location.longitude);
} else {
const text = message.text || 'User sent a non-text message';
userMessageText = text;
response = await sendPlainText(ADMIN_UID, `Message from ${userName}:\n${text}`);
}
// Relay or record the message.
if (response && response.result && response.result.message_id) {
const adminMessageId = response.result.message_id;
await KV_NAMESPACE.put(`admin_message_${adminMessageId}`, chatId.toString(), { expirationTtl: 86400 });
}
await KV_NAMESPACE.put(`${USER_MESSAGES_KEY_PREFIX}${chatId}`, userMessageText, { expirationTtl: 86400 });
// Relay or record the message.
await KV_NAMESPACE.put(LAST_USER_KEY, chatId.toString());
}
}
function apiUrl(methodName) {
return `https://api.telegram.org/bot${TOKEN}/${methodName}`;
}
async function sendPlainText(chatId, text) {
const response = await fetch(apiUrl('sendMessage'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: chatId, text })
});
return response.json();
}
async function sendSticker(chatId, fileId) {
const response = await fetch(apiUrl('sendSticker'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: chatId, sticker: fileId })
});
return response.json();
}
async function sendPhoto(chatId, fileId, caption = '') {
const response = await fetch(apiUrl('sendPhoto'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: chatId, photo: fileId, caption })
});
return response.json();
}
async function sendVoice(chatId, fileId) {
const response = await fetch(apiUrl('sendVoice'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: chatId, voice: fileId })
});
return response.json();
}
async function sendDocument(chatId, fileId) {
const response = await fetch(apiUrl('sendDocument'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: chatId, document: fileId })
});
return response.json();
}
async function sendVideo(chatId, fileId) {
const response = await fetch(apiUrl('sendVideo'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: chatId, video: fileId })
});
return response.json();
}
async function sendLocation(chatId, latitude, longitude) {
const response = await fetch(apiUrl('sendLocation'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: chatId, latitude, longitude })
});
return response.json();
}
async function registerWebhook(event, requestUrl, suffix, secret) {
const webhookUrl = `${requestUrl.protocol}//${requestUrl.hostname}${suffix}`;
const response = await fetch(apiUrl('setWebhook'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: webhookUrl, secret_token: secret })
});
const r = await response.json();
return new Response('ok' in r && r.ok ? 'Ok' : JSON.stringify(r, null, 2));
}
async function unRegisterWebhook(event) {
const response = await fetch(apiUrl('setWebhook'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: '' })
});
const r = await response.json();
return new Response('ok' in r && r.ok ? 'Ok' : JSON.stringify(r, null, 2));
}
}
export default {
async fetch(request, env) {
try { return await makeHandler(env)(request); }
catch { return new Response("Relay error", { status: 500 }); }
}
};
Register the webhook
After deployment, call your own registration URL with an Authorization header. Replace both placeholders locally; do not publish the actual secret:
curl -H "Authorization: Bearer YOUR_SECRET" https://example.workers.dev/registerWebhook
The original example URL was https://example.workers.dev/registerWebhook; it is a placeholder, not your deployed endpoint. A successful response indicates registration, but test delivery in both directions. Send a message to the bot, then reply to its relayed copy from the administrator account.
Limits and privacy
This example relays common text and media types. It stores routing IDs and the latest message description in KV for 24 hours. KV can be eventually consistent, so a very fast reply may need to wait briefly for its mapping. It is a small example, not a durable queue or an exactly-once delivery system.
Check current Cloudflare quotas and billing; a low-volume bot may fit the free tier, but there is no blanket promise that any few hundred messages will always be free. See Worker bindings and the Telegram webhook documentation.
Self-hosting gives you control over the relay, but it does not remove Telegram, Cloudflare, or administrator access to messages. A third-party relay operator could log or alter content. Never treat a forwarded payment address as independent verification, and do not send sensitive material through a bot without understanding its operator and storage.
Adapted from the original Chinese article, published on February 6, 2026.

Comments NOTHING