# 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 2026-11-07 · https://shareglint.com/en/guides/image-bot-telegram

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](https://core.telegram.org/bots/api) documentation as of that date.

## What you need

1. **A bot token** from [@BotFather](https://core.telegram.org/bots/tutorial) 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

```javascript
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](https://shareglint.com/en/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](https://shareglint.com/en/use-cases/price-ticker-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](https://shareglint.com/en/guides/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](https://shareglint.com/en/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](https://shareglint.com/en/guides/image-bot-discord).

## 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.
