saveapi.org

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.

Terminal
npm 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.

JavaScript
const 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.

JavaScript
import { 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.

JavaScript
const 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.

Get a key and try itFull API reference