webhooks
two webhook handlers — a fetch-style one and a node http one — both feeding the same handleUpdate entry point, with a constant-time secret check and a body-size cap.
polling vs webhooks
for development, long polling is simplest: bot.start() loops getUpdates and dispatches each update through handleUpdate, retrying
after a short delay if a poll fails. it resolves only when bot.stop() is called.
// long polling — start() loops getUpdates and calls handleUpdate for each
const bot = new Bot(process.env.BOT_TOKEN!);
bot.on("message:text", (ctx) => ctx.reply(ctx.text));
await bot.start(); // resolves only when stop() is calledfor production you usually want webhooks: Telegram POSTs each update to your URL, and you hand the request straight to a yaebal handler. no polling loop, and it scales to serverless runtimes.
webhookCallback (fetch)
webhookCallback returns a (Request) => Promise<Response> function — the shape every fetch-based runtime expects. it only accepts POST,
parses the JSON update, and dispatches it.
import { webhookCallback } from "@yaebal/core";
// (Request) => Promise<Response>
const handler = webhookCallback(bot, { secretToken: process.env.WEBHOOK_SECRET });the secret check
when you set secretToken, the handler requires Telegram's X-Telegram-Bot-Api-Secret-Token header to match. the comparison is constant-time
(node:crypto timingSafeEqual on equal-length buffers), so the check can't
be timed.
| condition | response |
|---|---|
method is not POST | 405 |
secretToken set and header mismatched | 401 |
| body larger than 1 MiB | 413 |
| body is not valid JSON | 400 |
| update dispatched | 200 ok |
MAX_BODY) to avoid memory abuse — the fetch handler checks content-length; the node handler counts bytes as they stream and destroys the request
if it overflows.nodeWebhookCallback (node http)
for a plain Node server, nodeWebhookCallback returns an (req, res) handler you can drop into http.createServer. same secret
check and same body cap, streamed off the incoming request.
// Node http
import { createServer } from "node:http";
import { Bot, nodeWebhookCallback } from "@yaebal/core";
const bot = new Bot(process.env.BOT_TOKEN!);
bot.on("message:text", (ctx) => ctx.reply(ctx.text));
createServer(
nodeWebhookCallback(bot, { secretToken: process.env.WEBHOOK_SECRET }),
).listen(8080);handleUpdate
both handlers ultimately call bot.handleUpdate(update), which builds a Context and runs the middleware chain, sending any thrown error to your onError handler. you can also call it directly from a custom HTTP layer.
// both handlers ultimately call bot.handleUpdate — the single-update entry point.
// the chain is realized (and frozen) on the first call, so register every
// middleware / plugin before the first handleUpdate or start.
await bot.handleUpdate(update);handleUpdate (or start), so attach all middleware and plugins before
then.deploy: Cloudflare Workers
// Cloudflare Workers
import { Bot, webhookCallback } from "@yaebal/core";
const bot = new Bot(env.BOT_TOKEN);
bot.command("start", (ctx) => ctx.reply("hi from the edge"));
export default {
fetch: webhookCallback(bot, { secretToken: env.WEBHOOK_SECRET }),
};deploy: Bun
// Bun
import { Bot, webhookCallback } from "@yaebal/core";
const bot = new Bot(process.env.BOT_TOKEN!);
bot.on("message:text", (ctx) => ctx.reply(ctx.text));
Bun.serve({
port: 8080,
fetch: webhookCallback(bot, { secretToken: process.env.WEBHOOK_SECRET }),
});