Telegram bot to generate video: a Sume job, then sendVideo
A Telegram bot can turn /video into a Sume job, reply at once, then call sendVideo with the artifact URL when Sume's signed webhook arrives.

To make a Telegram bot generate video, have its webhook turn a /video command into POST /v1/videos with model: "sume/auto" and a callback_url, reply right away, and when Sume's signed job.completed webhook arrives, call sendVideo with the video artifact's media.sume.com URL. Telegram fetches a video sent by URL only up to 20 MB, so upload bigger files with multipart/form-data, up to 50 MB, or send the link.
Telegram facts come from the Telegram Bot API reference; Sume facts come from Video Generation, Webhooks, and Verifying webhooks, plus the Sume API reference for artifact fields. All were read on 2026-09-27. Sume has no Telegram bot or integration: this is your bot's server calling Sume over HTTPS. The Discord version of this flow is in Discord bot AI video generation.
How does the bot take the /video command?
Register your HTTPS endpoint with setWebhook and a secret_token. Telegram then sends that token in the X-Telegram-Bot-Api-Secret-Token header of every update, so the route can refuse anything else. Telegram repeats an update your server answers with a non-2XX status, and update_id lets you spot repeats, so build Sume's Idempotency-Key from it: a replay returns the original job instead of starting another.
The reply can ride on the webhook response. Telegram runs a Bot API method named in the response body, though you cannot learn whether it succeeded.
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/telegram", express.json(), async (req, res) => {
const got = Buffer.from(req.get("X-Telegram-Bot-Api-Secret-Token") ?? "");
const want = Buffer.from(process.env.TELEGRAM_SECRET_TOKEN);
if (got.length !== want.length || !crypto.timingSafeEqual(got, want)) return res.sendStatus(401);
const msg = req.body.message;
if (!msg?.text?.startsWith("/video ")) return res.sendStatus(200);
const r = await fetch("https://api.sume.com/v1/videos", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUME_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `telegram-${req.body.update_id}`,
},
body: JSON.stringify({ model: "sume/auto", prompt: msg.text.slice(7), callback_url: "https://bot.example.com/hooks/sume" }),
});
const job = await r.json();
if (r.ok) await saveJob(job.id, msg.chat.id); // your store: job id -> chat id
// A Bot API call in the webhook response: Telegram runs it, you get no result.
res.json({ method: "sendMessage", chat_id: msg.chat.id, text: r.ok ? "Rendering your video..." : "Sume refused the request." });
});How does the bot send the finished video?
Sume POSTs one signed job webhook when the job reaches a terminal state, with up to 10 attempts of 10 seconds each. Verify the raw body with verifyWebhook from @sume-com/sdk, which also accepts either sume-v1= entry during a secret rotation. Claim the job_id so a retried delivery is handled once, answer 204, then call Telegram. On job.completed, payload.artifacts[] holds a video entry whose url is a public media.sume.com artifact, and the API reference gives each artifact a content_type and a nullable size_bytes. Two choices in the code are this post's, not Telegram's: it reads the 20 MB URL limit as 20,000,000 bytes, and when size_bytes is missing or sendVideo fails, it posts the link as text.
import { verifyWebhook } from "@sume-com/sdk";
const tg = (method, params) =>
fetch(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/${method}`, {
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params),
}).then((r) => r.json()); // { ok: true, result } or { ok: false, description }
app.post("/hooks/sume", express.raw({ type: "application/json" }), async (req, res) => {
const ok = await verifyWebhook({ body: req.body, headers: req.headers, secret: process.env.SUME_COM_WEBHOOK_SIGNING_SECRET });
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body);
const chatId = await claimJob(event.job_id); // your store: null if unknown or already handled
res.sendStatus(204); // Sume allows 10 s per attempt
if (!chatId) return;
const video = event.event === "job.completed" && event.payload.artifacts.find((a) => a.type === "video");
if (!video) return tg("sendMessage", { chat_id: chatId, text: "The video job did not complete." });
const byUrl = video.size_bytes != null && video.size_bytes <= 20_000_000; // our reading of "20 MB"
const sent = byUrl ? await tg("sendVideo", { chat_id: chatId, video: video.url }) : { ok: false };
if (!sent.ok) await tg("sendMessage", { chat_id: chatId, text: video.url }); // our fallback; or upload (50 MB)
});Which way should the bot send the file?
sendVideo takes a file_id, an HTTP URL, or a multipart upload, and Telegram clients support MPEG4 video. The size limit depends on the method:
| Method | Limit | Use it when |
|---|---|---|
HTTP URL in video | 20 MB | size_bytes is under the limit. Telegram downloads the file, which must have the correct MIME type. |
Upload with multipart/form-data | 50 MB | The file is larger than 20 MB: download it from media.sume.com, then upload it. |
file_id | No limit | The video is already on Telegram's servers. A file_id works only for the bot that received it. |
| Local Bot API Server | Uploads up to 2000 MB | You run Telegram's Bot API server yourself. Otherwise, send the link as text. |
What are the limits?
Plan for these before you open the bot to a chat:
- Sume sends terminal job events only, with no progress deliveries, and video generation typically takes 30 seconds to several minutes.
sendChatActionwithupload_videoshows a status for 5 seconds or less, so send a text reply for the wait. - Artifact URLs are public by link, so anyone who can read the chat can open the video.
- Every command starts a job paid from your workspace balance. Sume's docs say to validate user input and enforce your own authorization before forwarding requests, so decide which chats may use the bot.
- Telegram webhooks accept ports 443, 80, 88, and 8443, but current Sume code refuses a
callback_urlwith a non-default port, such as:8443. Serve Sume's route on the default HTTPS port. - A delivery that never lands does not change the job. Keep a
GET /v1/jobs/{id}/statusread as the backup.
Sources
Related posts
More in Integrations
- Test Sume webhooks locally with ngrok or a Cloudflare Tunnel
Sume refuses localhost webhook URLs. Expose your handler with ngrok or a Cloudflare Quick Tunnel, send a signed test, then redeliver real events.
- Text-to-video API in Python: submit, poll, and download
Call Sume's text-to-video API from Python with Requests: POST /v1/videos, poll with timeouts, then stream the MP4 the content route redirects to.
- Trigger.dev wait for webhook: waitpoint tokens for Sume runs
Create a Trigger.dev waitpoint token, send token.url as a Sume run's webhook_url, and wait.forToken() returns when Sume POSTs the run's result.
- Vercel AI SDK: generate video with a Sume API tool call
Generate video from the Vercel AI SDK with a tool() that calls Sume's POST /v1/videos on your server, returns the job id, and polls for the clip.
Written by Sume