Menu
Sign up

A Telegram bot that makes images: quote cards, certificates and daily posts

A Telegram bot in plain Node.js that turns a command into a generated image: render a template, send it with sendPhoto, stay within Telegram's limits.

Published November 7, 2026 · 6 min read

A Telegram bot is a good front end for generated images: someone types /quote Simplicity is prerequisite for reliability. and the bot answers with a branded quote card; a course bot sends a certificate when a quiz is passed; a channel bot posts today's rates board at 9:00. The bot itself is small — it receives messages, asks Shareglint to render a template with the message's values, and hands the image URL to Telegram, which downloads and shows it.

This guide builds such a bot in plain Node.js with no dependencies. The code was run on 2026-11-07 against stand-ins for the Telegram and image APIs, which recorded every request it made; the limits quoted are from the Telegram Bot API documentation as of that date.

What you need

  1. A bot token from @BotFather in Telegram (/newbot).
  2. An API key for Shareglint and a template. The built-in quote-og template takes quote, author, role, site and photo; any of your own templates works the same way.
  3. Node.js 18 or later, which has fetch built in.

Keep both secrets in environment variables, never in the code or the chat.

The bot

const TG = `https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}`;
const API_KEY = process.env.IMAGE_API_KEY;

async function tg(method, body) {
  const res = await fetch(`${TG}/${method}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  const data = await res.json();
  if (!data.ok) throw new Error(`${method}: ${data.description}`);
  return data.result;
}

async function render(quote, author) {
  const res = await fetch('https://shareglint.com/v1/images?wait=true', {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      template: 'quote-og',
      size: 'square',
      format: 'jpeg',
      modifications: { quote, author },
    }),
  });
  if (!res.ok) throw new Error(`render failed: ${res.status}`);
  return (await res.json()).url;
}

let offset = 0;
for (;;) {
  const updates = await tg('getUpdates', { offset, timeout: 50, allowed_updates: ['message'] });
  for (const update of updates) {
    offset = update.update_id + 1;
    const msg = update.message;
    const match = msg?.text?.match(/^\/quote(?:@\w+)?\s+(.{1,280})$/s);
    if (!match) continue;
    try {
      await tg('sendChatAction', { chat_id: msg.chat.id, action: 'upload_photo' });
      const url = await render(match[1], msg.from.first_name);
      await tg('sendPhoto', { chat_id: msg.chat.id, photo: url, reply_parameters: { message_id: msg.message_id } });
    } catch (err) {
      console.error(err);
      await tg('sendMessage', { chat_id: msg.chat.id, text: 'Sorry, I could not make that image.' });
    }
  }
}

Save it as bot.mjs and run TELEGRAM_TOKEN=… IMAGE_API_KEY=… node bot.mjs. What each part does:

  • getUpdates with timeout: 50 is long polling: the request waits up to 50 seconds for new messages, so the loop does not spin. offset confirms the messages already handled. Telegram keeps undelivered updates for at most 24 hours.
  • The command pattern accepts /quote text and /quote@YourBot text (the form Telegram uses in groups with several bots) and caps the text at 280 characters, so nobody can send a novel to your template.
  • sendChatAction with upload_photo shows "sending photo…" in the chat while the image renders — the Bot API docs suggest exactly this for an image bot.
  • ?wait=true returns the finished image in the same request, usually in about a second; a non-2xx answer (a failed render, or 503 sync_overloaded under load) goes to the catch.
  • sendPhoto with a URL — Telegram downloads the file from our URL; your bot never handles the bytes.

In our recorded run, a /quote@MyBot … message produced exactly four requests — getUpdates, sendChatAction, the render, sendPhoto replying to the message — and a plain "hello" produced none.

Telegram's limits

From the Bot API documentation:

Limit Value
Photo sent by URL 5 MB
Photo uploaded as a file 10 MB
Photo dimensions width + height ≤ 10,000, ratio ≤ 20
Other files by URL (GIF, MP4, documents) 20 MB
Files uploaded by a bot (sendDocument, sendAnimation) up to 50 MB

Our raster images come out at twice the template size by default: a 1200×630 template is a 2400×1260 file. As PNG, the built-in templates we measured were 0.5–0.7 MB at that size, but the photo-heavy photo-collage was 2.8 MB — closer to the 5 MB URL limit than you might expect. For photos and photo-heavy templates, ask for "format": "jpeg"; for very large templates, add "scale": 1.

Telegram stores a photo in several sizes it makes itself (the message's photo field is a list of sizes), so what users see is Telegram's copy. When the exact file matters — a certificate someone will print — send it with sendDocument instead. By URL, Telegram's docs say sendDocument "will currently only work for .PDF and .ZIP files": render the certificate with "format": "pdf" and pass its URL as document, or download a PNG and upload it as a file.

Animated images

Templates with CSS animation render as GIF or MP4 with "format": "gif" or "mp4" (an animated render uses 5 renders of quota). Send them with sendAnimation, which takes "GIF or H.264/MPEG-4 AVC video without sound"; MP4 is usually much smaller than the same GIF. See HTML to GIF for what animates.

Posting to a channel on a schedule

A bot does not need commands to be useful. To post a daily image to a channel, add the bot to the channel as an administrator, then run a scheduled job (cron, a serverless schedule) that renders and calls sendPhoto with chat_id: '@yourchannel'. A rates board, a daily quote, a countdown to an event, a weekly digest cover: price and rate images shows the template side. Identical values return the identical file without using quota, so a job that runs every hour but finds nothing new costs nothing.

Polling or a webhook

Long polling, as above, works anywhere — a laptop, a small VPS — with no public address. For a bot on a serverless platform, use a webhook instead: setWebhook makes Telegram send each update to your HTTPS endpoint, which renders and replies. The two are mutually exclusive: while a webhook is set, getUpdates returns nothing. A webhook handler should answer quickly; render and reply inside the handler only when the render is fast, which ?wait=true usually is.

Keeping it safe and cheap

  • Limit per user. Every distinct request is a render. Keep a counter per user per hour in memory or Redis, and answer politely when it is exceeded.
  • Validate input. Cap lengths, as the regular expression does; template field values are escaped, so text cannot become markup, but a 5,000-character "quote" still makes an unreadable card.
  • Never expose the API key. It lives in the bot's environment only; do not put it in messages, inline buttons or links. If you want image links that anyone can open without a key, use signed image URLs.
  • Handle failures. Telegram returns ok: false with a description (for example, a blocked bot or a missing chat); log it and move on rather than crashing the loop.

Cost

One render per new image; a repeated /quote with the same text and name is free. A community bot making 20 cards a day uses about 600 renders a month. The Free plan's 100 renders carry a watermark; Starter is $9 a month for 2,000 renders (pricing).

What it does not do

  • We do not host or run your bot; the code above runs on your machine or server.
  • We do not connect to Telegram or hold your bot token; your bot calls Telegram's API itself.
  • Images come from templates and the values you send — this is not an AI image generator.

The same pattern in Discord, with slash commands and embeds, is in the Discord image bot guide.

FAQ

How does a Telegram bot send a generated image?

It calls sendPhoto with the image's public URL as the photo parameter, and Telegram downloads the file itself. For photos sent by URL, Telegram's limit is 5 MB; uploaded files may be up to 10 MB.

Do I need a library to build a Telegram bot in Node.js?

No. The Bot API is plain HTTPS with JSON, so Node's built-in fetch is enough for a small bot. Libraries such as grammY or Telegraf help once you have many commands, menus and conversations.

Can the bot post animated images?

Yes. Render with format gif or mp4 and send it with sendAnimation, which accepts GIF or H.264/MPEG-4 video without sound. Telegram fetches files by URL up to 20 MB for non-photo content.

How do I stop users from running up my render quota?

Limit commands per user and per chat in the bot, cap the text length, and only render values that changed. Identical values return the same file without using quota, so repeated requests for the same card are free.