If you asked ChatGPT for a contact form that sends messages to Telegram, there is a good chance the bot token ended up in your page. We asked it five times on 9 November in fresh temporary chats, with the same prompt: "Write me a simple HTML contact form for my website that sends each message to my Telegram." Four of the five answers put the token in a <script> tag that every visitor downloads. The fifth wrote a PHP file instead.
To be fair to ChatGPT, all four of those answers did warn about it. The warning comes after the full page of code, though, and in two of the answers it also comes after a step that says "Replace these two lines with your actual Telegram bot token and chat ID." If you copy the code, paste the token, see the message arrive in Telegram and close the tab, you never read the warning.
We're Formgong, and we sell a way to send a website form to Telegram, so read the last section with that in mind. The data in the rest of the article doesn't depend on us.
How many sites already do this
We searched GitHub code for HTML files that contain both api.telegram.org/bot and sendMessage. GitHub reports about 10,900 such files. We read the first 200 results in full and checked them for a string with the shape of a bot token: digits, a colon, then AA and about 33 more characters.
155 of the 200 files have one. That's 142 different tokens across 132 repositories. 94 of those 155 files also have a <form> or FormData in them. We didn't call a single token, so we can't tell you how many still work. The shape only tells you someone pasted something that looks real. These are the first 200 results GitHub returned, not a random sample, so don't read 155 of 200 as a rate for the whole of GitHub. And a public repository is only the version of the problem you can search. A site that never touched GitHub serves the same token to every visitor in its page source.
What the token lets someone do
A Telegram bot token is the bot's password. The Bot API docs put it plainly: each bot gets "a unique authentication token" when it's created, and every method takes it in the URL. Whoever has your token can do anything your bot can do:
-
sendMessageto any chat the bot is in, as your bot, with whatever text and links they want. Your customers who pressed Start on the bot are in that list. -
getUpdatesreturns the messages people send to the bot. -
setWebhookpoints all of the bot's updates to their server. Your own code stops getting them, and nothing tells you why. -
setMyNameandsetMyDescriptionchange what people see when they open the bot. -
deleteMessageremoves messages the bot sent in the last 48 hours.
It doesn't give them your Telegram account, only the bot. For a contact form bot that's mostly spam and embarrassment. But if the same bot also runs your shop orders or a support chat, the damage gets bigger.
If your token has been in a page, open @botfather, send /revoke, pick the bot, and put the new token somewhere private. Taking the old token out of the page doesn't help: anyone who already copied it can keep using it.
Keep the token on a server
The fix ChatGPT suggested in its warnings is the right one: the browser sends the form to code you control, and that code talks to Telegram. The catch is the PHP version only runs on hosting with PHP. GitHub Pages, Netlify, Vercel and most AI site builders give you static hosting, so a POST to send.php there just fails.
A Cloudflare Worker runs anywhere your site does. This one takes the form, drops bots that fill a hidden field, and sends the message:
// wrangler secret put TELEGRAM_TOKEN // wrangler secret put TELEGRAM_CHAT_ID const SITE = "https://example.com"; // your site, so the page can read the answer export default { async fetch(request, env) { const cors = { "Access-Control-Allow-Origin": SITE }; if (request.method !== "POST") return new Response("Use POST", { status: 405, headers: cors }); const form = await request.formData(); // A hidden field people never see. Bots fill every field. if (form.get("website")) return Response.json({ ok: true }, { headers: cors }); const name = String(form.get("name") || "").slice(0, 100); const email = String(form.get("email") || "").slice(0, 200); const message = String(form.get("message") || "").slice(0, 3000); if (!email || !message) return Response.json({ ok: false, error: "missing fields" }, { status: 400, headers: cors }); const tg = await fetch(`https://api.telegram.org/bot${env.TELEGRAM_TOKEN}/sendMessage`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chat_id: env.TELEGRAM_CHAT_ID, text: `New message from the site\n\nName: ${name}\nEmail: ${email}\n\n${message}`, }), }); const result = await tg.json(); return Response.json({ ok: result.ok === true }, { status: result.ok ? 200 : 502, headers: cors }); }, }; In the form, add the hidden field and send to the Worker's URL instead of api.telegram.org:
<input type="text" name="website" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px"> const res = await fetch("https://contact.your-name.workers.dev", { method: "POST", body: new FormData(form) }); const { ok } = await res.json(); We ran this Worker with fetch stubbed out. A normal message reached the Telegram call with the right chat ID, a filled honeypot sent nothing, a missing message returned 400 and a GET returned 405. Don't skip the Access-Control-Allow-Origin line. Without it the browser still sends the POST and the message still arrives, but your page can't read the reply and shows an error to someone whose message got through.
There are two things this version doesn't do. Anyone can still post to the Worker URL, so a determined spammer can fill your chat. They just can't take over the bot. If that starts happening, add Cloudflare Turnstile in front of it. Also, if Telegram is down when someone writes, the message is gone, because nothing stores it or tries again.
Or don't run a bot at all
This is the part where we talk about our own product. Formgong is a form backend. Your form posts to https://formgong.com/submit with a form key in a hidden field:
<form action="https://formgong.com/submit" method="POST"> <input type="hidden" name="access_key" value="YOUR_FORM_KEY"> <input type="text" name="botcheck" tabindex="-1" autocomplete="off" style="display:none"> <input name="name" required> <input name="email" type="email" required> <textarea name="message" required></textarea> <button>Send</button> </form> That key is supposed to be public. It can only add a new submission to your inbox. It can't read anything, rename anything or redirect anything, so finding it in the page gets nobody anything. To get messages in Telegram, open the form's settings, press connect, and press Start in the Formgong bot. For a group you add the bot and send /connect with a code from the settings. The code works once and expires in 30 minutes. You never create a bot, and no token or chat ID goes into your site.
Spam is caught before it reaches the chat (a honeypot always, Cloudflare Turnstile if you turn it on), and there are rate limits per form and per visitor. Every message also stays in your inbox and goes to your email. If Telegram fails, Formgong retries five times over about 42 minutes and then emails you. In the chat each lead comes with buttons to reply and close it. Telegram works on the free plan.
One question for you
Did your AI tool or tutorial put the token in the page, and did you notice? If you've moved a Telegram form off the frontend, tell us in the comments what you used, or write to support@formgong.com. We'd especially like to hear what made it hard.

