Build a Telegram video downloader bot with grammY
grammY is the modern Node.js framework for Telegram bots, and a link-to-video bot in it is about thirty lines once resolving is an HTTP call. This guide is the entity filter that catches links, the reply methods that take a URL directly, the error sentences a user understands, and the one thing to cache.
Updated
Install
grammY only; fetch is built into Node 18+. There is no scraping stack to install and no browser to keep alive.
Terminalnpm install grammy
Resolve the link
One GET with the key in the Authorization header. AbortSignal.timeout keeps a slow source from holding your handler. The endpoint detects the platform from the URL, so the bot does not branch on the domain.
JavaScriptconst API = "https://api.saveapi.org/v1/download";
async function resolve(link) {
const res = await fetch(API + "?url=" + encodeURIComponent(link), {
headers: { Authorization: "Bearer sk_live_xxxx" },
signal: AbortSignal.timeout(30_000),
});
return res.json();
}Catch links and answer with media
message::url matches any message carrying a URL entity; ctx.entities("url") gives you the text. replyWithVideo and replyWithPhoto take the media URL directly — Telegram fetches it. Check medias[0].type: a slideshow or a carousel is images, not a video.
JavaScriptimport { Bot } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN);
bot.on("message::url", async (ctx) => {
const link = ctx.entities("url")[0].text;
const data = await resolve(link);
if (!data.success) {
return ctx.reply(EXPLAIN[data.error.code] ?? "Try another link.");
}
const media = data.medias[0];
return media.type === "video"
? ctx.replyWithVideo(media.url, { caption: data.meta?.title ?? "" })
: ctx.replyWithPhoto(media.url);
});
bot.start();Explain the errors people actually hit
Users paste private posts and dead links all day. Map the stable codes to sentences: a bot that says "That account is private" gets a different link, a bot that says "error" gets an angry message. Private and missing cost 0.1 credits, not the full 1.5.
JavaScriptconst EXPLAIN = {
PRIVATE_CONTENT: "That account is private — I can only fetch public posts.",
MEDIA_NOT_FOUND: "That post has no media, or it was deleted.",
UNSUPPORTED_PLATFORM: "I do not know that site yet.",
QUOTA_EXCEEDED: "Out of credits.",
UPSTREAM_TIMEOUT: "The source was slow. Send the link again.",
};Cache Telegram's file_id, not the media URL
After the first send, Telegram gives you a file_id; re-sending it uploads nothing and costs no credits. The media URL is the wrong thing to store — CDN links are signed and expire within hours.
Questions before you build
- Can the bot send files larger than 50 MB?
- Not by uploading them; 50 MB is Telegram's ceiling for a bot upload. Passing the media URL lets Telegram fetch it, and file_id re-sends have no size cost.
- Does this work with webhooks?
- Yes — the handler is the same; only bot.start() changes to webhookCallback. Keep the fetch timeout, since a webhook handler has a deadline too.
- What if Telegram refuses to fetch the URL?
- Download the bytes yourself and send them as an InputFile. It happens most with Instagram and always in media groups; keep it as the fallback, not the default.
Platform references used here
Other guides
Run this guide with your own key
Key in under a minute. 1,000 free credits, no card, no expiry.