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
- A bot token from @BotFather in Telegram (
/newbot). - An API key for Shareglint and a template. The built-in
quote-ogtemplate takesquote,author,role,siteandphoto; any of your own templates works the same way. - Node.js 18 or later, which has
fetchbuilt 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:
getUpdateswithtimeout: 50is long polling: the request waits up to 50 seconds for new messages, so the loop does not spin.offsetconfirms the messages already handled. Telegram keeps undelivered updates for at most 24 hours.- The command pattern accepts
/quote textand/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. sendChatActionwithupload_photoshows "sending photo…" in the chat while the image renders — the Bot API docs suggest exactly this for an image bot.?wait=truereturns the finished image in the same request, usually in about a second; a non-2xx answer (a failed render, or503 sync_overloadedunder load) goes to thecatch.sendPhotowith 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: falsewith 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.