Create your own
Lesson illustration

Securing Webhook Endpoints with Secret Tokens

Welcome to the next lesson in our journey to build and deploy Telegram bots. In our last session, you successfully transitioned your bot from long polling to a webhook-based architecture, using Bun.serve and ngrok to receive updates in real-time. However, we ended on a crucial cliffhanger: your webhook URL is currently public and unprotected. Anyone who finds it can send fake updates, potentially causing your bot to malfunction or incur unwanted costs.

This lesson directly addresses that vulnerability. You will learn how to secure your webhook endpoint using a secret token, the standard mechanism recommended by Telegram. We'll explore why this is critical, how the token-based header authentication works, and then implement it within your grammY application.

By the end of this lesson, you will have a robust and secure webhook that only accepts legitimate updates from Telegram, making your bot ready for its final deployment to the cloud.

The Critical Need for Webhook Security

Before we dive into the implementation, it's essential to understand the risks of an unsecured webhook. An open endpoint is a target. Malicious actors could bombard your bot with requests, and if your bot interacts with paid APIs (like an AI model), this could lead to significant and unexpected costs. It could also be used to inject bad data or trigger unintended actions.

The following video, while not specific to Telegram, provides an excellent overview of the general risks and common methods for securing webhooks.

n8n Webhook Security: Learn This Before It’s Too Late

This video from the Nate Herk channel clearly explains the financial and data privacy risks of unsecured webhooks.

Watch the introductory segment from the beginning to understand the potential consequences. Then, watch the explanation of Header Auth, as this is the exact type of mechanism that Telegram uses.

As the video explains, one of the most common and effective security methods is header-based authentication. This is precisely the approach Telegram has built into its Bot API.

The Secret Token Authentication Flow

The mechanism is a straightforward challenge-response system based on a shared secret:

  1. You Generate a Secret: You create a long, random, and unpredictable string. This is your secret_token.
  2. You Tell Telegram Your Secret: When you call the setWebhook API method, you include your secret_token as a parameter.
  3. Telegram Sends a Custom Header: From that point on, every single update that Telegram POSTs to your webhook URL will include a special HTTP header: X-Telegram-Bot-Api-Secret-Token. The value of this header will be your secret token.
  4. Your Server Verifies the Header: Your web server's first job upon receiving a request is to inspect the headers. It must check for the existence of X-Telegram-Bot-Api-Secret-Token and verify that its value matches the secret you configured.
    • If the header is present and the value matches, the request is legitimate. Your server processes the update.
    • If the header is missing or the value is incorrect, the request is from an unauthorized source. Your server must immediately reject it, typically by returning an HTTP 401 Unauthorized status code, without passing the update to your bot logic.

This flow ensures that only Telegram's servers, which know the secret you provided, can trigger your bot's logic.

The grammY documentation provides a clear definition of the options involved, including the secretToken.

1.38.0 • npm-grammy • tessl • Registry • Tessl

The grammY documentation details the options for configuring webhooks.

Please review the Webhook Options interface. Pay close attention to the secretToken property and its description; it explicitly states that this is used to validate the X-Telegram-Bot-Api-Secret-Token header.

Implementing the Secret Token in Your Bot

Now, let's secure your bot. The process involves updating both your server code and your webhook management script.

Step 1: Generate and Store Your Secret

First, we need a strong, random secret. You can use Bun to generate a UUID, which is perfectly suitable for this purpose.

bun -e "console.log(crypto.randomUUID())"

This will output a unique string like a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890. Copy this value.

Next, open your .env file and add this new secret. This keeps your credentials separate from your code.

.env

TELEGRAM_BOT_TOKEN="your_bot_token_here"






# Add your new secret token
TELEGRAM_WEBHOOK_SECRET="your_generated_uuid_here"

Step 2: Update Your Bot Server

Now, let's configure your Bun server to use this secret. grammY makes this incredibly simple. The webhookCallback function we are already using accepts an options object where we can provide the secret token. It will then handle the header verification for you automatically.

Modify your src/index.ts file to load the secret and pass it to the callback.

src/index.ts

import { webhookCallback } from "grammy";
import { bot } from "./bot";

const secret = process.env.TELEGRAM_WEBHOOK_SECRET;
if (!secret) throw new Error("TELEGRAM_WEBHOOK_SECRET is not set");

// Create a webhook callback that validates the secret token
const handleUpdate = webhookCallback(bot, "std/http", {
  secretToken: secret,
});

Bun.serve({
  // The fetch handler now uses the secured callback
  fetch: async (req) => {
    try {
      const url = new URL(req.url);
      if (url.pathname === "/") {
        // Use the handleUpdate function that includes the secret token check
        return await handleUpdate(req);
      }
      return new Response("Not Found", { status: 404 });
    } catch (err) {
      console.error(err);
      return new Response("Internal Server Error", { status: 500 });
    }
  },
  port: 3000,
  error() {
    return new Response("Internal Server Error", { status: 500 });
  },
});

console.log("Bot server listening on port 3000...");

Notice that we've passed a secretToken option to webhookCallback. Under the hood, grammY will now perform the header check on every incoming request before handing it off to your bot's middleware.

Step 3: Update the Webhook Management Script

Finally, we need to tell Telegram what our secret is. We'll modify the src/webhook.ts script to include the secret_token parameter when it calls bot.api.setWebhook.

src/webhook.ts

import { bot } from "./bot";
import "dotenv/config"; // Ensure .env is loaded

const command = process.argv[2];

async function setupWebhook() {
  const url = process.argv[3];
  if (!url) {
    console.error("Please provide a URL for the webhook.");
    process.exit(1);
  }

  // Load the secret from the environment
  const secret = process.env.TELEGRAM_WEBHOOK_SECRET;
  if (!secret) {
    console.error("TELEGRAM_WEBHOOK_SECRET is not set in .env file.");
    process.exit(1);
  }

  try {
    // Pass the secret_token when setting the webhook
    await bot.api.setWebhook(url, {
      secret_token: secret,
    });
    console.log("Webhook set successfully to:", url);
    await getWebhookInfo();
  } catch (error) {
    console.error("Failed to set webhook:", error);
  }
}

// ... (deleteWebhook and getWebhookInfo functions remain the same)
async function deleteWebhook() {
    //...
}
async function getWebhookInfo() {
    //...
}

// ... (switch statement remains the same)
switch (command) {
    //...
}

We've added logic to load the TELEGRAM_WEBHOOK_SECRET and pass it inside an options object to setWebhook. The parameter name here is secret_token as specified by the Telegram Bot API.

Testing the Secure Endpoint

Let's verify that everything is working as expected.

  1. Terminal 1: Start Your Bot Server

    bun start
    

    Your secured server is now running.

  2. Terminal 2: Start ngrok

    ngrok http 3000
    

    Copy the public HTTPS URL from ngrok.

  3. Terminal 3: Register the Secured Webhook
    Use your updated script to set the webhook. It will now also register your secret.

    bun src/webhook.ts set <YOUR_NGROK_URL>
    

    Check the output of the getWebhookInfo call. It should now show has_custom_certificate: false (or true depending on ngrok), but importantly, it confirms the webhook is set.

Now, send a message to your bot in Telegram. It should reply just as before. The flow is working.

But is it secure? Let's prove it. In Terminal 3, use curl to send a request to your webhook URL without the secret header.







# Replace <YOUR_NGROK_URL> with the actual URL
curl -X POST -H "Content-Type: application/json" \
-d '{"update_id":999,"message":{"text":"fake message"}}' \
<YOUR_NGROK_URL>

You will receive a 401 Unauthorized response. Your server rejected the request! Check the ngrok terminal (Terminal 2); you will see a 401 status code for that request. grammY's webhookCallback has done its job.

Now, let's try again, this time with the correct header.







# Replace <YOUR_NGROK_URL> and <YOUR_SECRET>
curl -X POST -H "Content-Type: application/json" \
-H "X-Telegram-Bot-Api-Secret-Token: <YOUR_SECRET>" \
-d '{"update_id":999,"message":{"text":"fake message"}}' \
<YOUR_NGROK_URL>

This time, the request will succeed with a 200 OK status, and your bot will process it. (Note: Since this is a fake update object, your bot might not reply in Telegram, but the server will accept the request, which is what we want to test).

Conclusion

In this lesson, you have implemented a critical security measure for your webhook-based bot. By using a secret token, you ensure that your server only processes legitimate updates sent from Telegram, protecting it from unauthorized access and potential abuse.

Key Takeaways:

  • An unsecured webhook is a significant security risk.
  • Telegram's standard security mechanism involves a secret token passed via the X-Telegram-Bot-Api-Secret-Token HTTP header.
  • The implementation in grammY is straightforward:
    1. Store a secret in your .env file.
    2. Pass the secret to webhookCallback via the secretToken option in your server code.
    3. Pass the secret to bot.api.setWebhook via the secret_token option when registering the webhook.
  • grammY automatically handles the validation, rejecting requests that lack a valid secret token.

Your bot is now not only functional but also secure. With this final piece in place, you are ready for the last major step in our course: deploying your bot to a live production environment. In the next lesson, we will leave ngrok behind and learn how to deploy your bot to a cloud platform, making it permanently available on the internet.

Can't find a good explanation? Sign up and we'll make it for you

Sign up