Telegram bot media API — audio without the re-upload
The Telegram bot media API turns a YouTube video ID into a Telegram file_id, so
your bot answers with a string instead of a file. Millions of tracks are already cached and cost 7
credits; a track we have to fetch first is slower and costs 15. Charts, search and song
recognition from the same API all hand you an ID this call accepts.
- Endpoint
POST /youtube/audio/tg-bot- Credits
- 7 on a cache hit, 15 on a miss
- Returns
- a Telegram
file_id - Scope
- valid only for the
bot_usernameyou send
the primitive
what a file_id is, precisely
When a bot sends a file, Telegram stores it and returns an identifier: a
file_id, an opaque string like CQACAgIAAxkDAAIVKWk…. Not a URL, not a
hash of the audio — a pointer into Telegram's storage, and the cheapest thing a bot can send,
because sending it moves no bytes through your infrastructure.
Two properties shape the design. A file_id is bound to one bot; Telegram will not
accept one bot's identifier from another. The same file also carries a
file_unique_id that is stable across bots, but you cannot send or download
with it — it only tells you two references point at the same audio.
The trade: pay once for a bot-scoped reference, and every later send of that track is one ordinary Telegram call carrying a string. Store the ones you get — the first resolve is the only part that costs credits, and it costs more than you might expect when the track is new to us.
why it exists
the loop this replaces
Otherwise, delivering one song means: resolve the track, stream it from a CDN to your server,
hold it in memory or on disk, upload the same bytes to Telegram as multipart form data, then keep
the file_id so the next user is cheaper.
- Bandwidth is doubled. Every megabyte arrives at your server and leaves again. On metered egress that is the cost of the feature.
- The upload leg is the slow one. Pulling from a CDN is fast; pushing to Telegram from a small VPS usually is not.
- 50 MB is a hard ceiling. The Bot API refuses larger uploads, so long mixes cannot be delivered this way at all.
- Concurrency becomes capacity planning. Twenty requests at once means twenty in-flight files, temp-file cleanup and a worker queue.
The tg-bot endpoint removes all four: the file never touches your process.
usage
the request
One POST, two fields. video_id is the YouTube or YouTube Music ID — the eleven
characters, not the whole link — and bot_username is your bot, with the @.
https://api.fastsaver.io/v1/youtube/audio/tg-bot
7 cached · 15 on a miss
curl -X POST https://api.fastsaver.io/v1/youtube/audio/tg-bot \
-H "X-Api-Key: fs_sk_•••••••••••" \
-H "Content-Type: application/json" \
-d '{"video_id": "vk6014HuxcE", "bot_username": "@your_bot"}'
{
"ok": true,
"file_id": "CQACAgIAAxkDAAIVKWk..."
}
That is the entire response: a delivery handle, no title, no artist, no duration. Keep the
metadata from whatever call gave you the video_id — you will want it for the
caption.
The same response shape covers both prices. If we already hold the track the call is 7 credits
and returns almost immediately; if we do not, the track is downloaded first and the call is
billed at the YouTube download rate of 15. Nothing in the payload tells you which happened, so
read your spend from GET /balance or your dashboard rather than assuming.
Treat ok as the success flag rather than the status code. A refusal arrives as a
plain 400 with a detail string — an exhausted balance reads
Insufficient credits. Please top up to your account., and there is no
402 to branch on. 401 means the key never arrived, 429
that you are over your plan's per-minute limit. A failed resolve is not free either: every
failure path charges a flat 0.1 credits, so a retry loop over an ID that will never
resolve still costs something.
code
the smallest bot that sends one
The endpoint reduces to two lines of bot code: POST the video ID, pass the returned string to
answer_audio. Everything below is scaffolding around those two lines.
import asyncio, os
import httpx
from aiogram import Bot, Dispatcher, F
from aiogram.types import Message
API = "https://api.fastsaver.io/v1"
BOT_USERNAME = "@your_bot"
bot = Bot(os.environ["BOT_TOKEN"])
dp = Dispatcher()
http = httpx.AsyncClient(
headers={"X-Api-Key": os.environ["FASTSAVER_KEY"]}, timeout=120,
)
@dp.message(F.text.regexp(r"^[\w-]{11}$"))
async def on_video_id(msg: Message) -> None:
r = await http.post(
f"{API}/youtube/audio/tg-bot",
json={"video_id": msg.text, "bot_username": BOT_USERNAME},
)
data = r.json()
if not data.get("ok"):
await msg.answer(data.get("detail", "could not resolve that ID"))
return
await msg.answer_audio(data["file_id"]) # a string, not a file
if __name__ == "__main__":
asyncio.run(dp.start_polling(bot))
On python-telegram-bot the send becomes
await update.message.reply_audio(audio=file_id). Either way, the argument that would
normally be an open file object is a string.
This is deliberately the endpoint and nothing else. Search, an inline keyboard, storing the strings and the error paths belong to a real bot rather than to the call — the Telegram music bot guide builds that end to end.
composition
what else fits a bot
A bot is rarely just a downloader. Most other endpoints hand you a video_id that
goes straight into the tg-bot call.
GET /youtube/search— 2 credits. Ten results per page, pages 1 to 3, each withvideo_id,title,durationand thumbnails: enough to label a row without a second lookup.GET /shazam/top— 1 credit. Charts by country code or worldwide; results already carryvideo_id.POST /shazam/identify— 5 credits, mp3/m4a/ogg/mp4 up to 50 MB. Bots may download from Telegram up to 20 MB, so a forwarded voice note fits: pull it withgetFile, post it, offer theresultsback.GET /shazam/lyrics— 2 credits, bytitleandartist. Both are required query parameters — there is no lookup by ID — and the reply carriesokandlyricsand nothing else, so keep the title and artist you already have for the header of your "lyrics" button.POST /youtube/downloadwithformat: "audio"— 15 credits, when you need the file rather than a reference.GET /balance— free. Poll it from a health check; learn about an empty balance before your users do.
GET /fetch covers the other platforms with a direct download_url.
Telegram will fetch a remote URL for you, but its ceiling there is lower than the upload one — a
convenience, not a strategy.
honesty
what a file_id will not do for you
- One bot, one file_id. Swap or add a bot and every reference must be re-requested, and each re-request is charged again. Keep
bot_usernamealongside the string. - Public content only. Public YouTube and YouTube Music IDs resolve. Private, members-only and region-blocked videos do not.
- A miss is slower and costs 15 instead of 7. A cache hit hands back a track we already hold; a miss means downloading it first, and that is billed at the YouTube download rate. Storing the
file_idyourself therefore saves real money and not just latency: every re-resolve you avoid is 7 credits you keep, and the first resolve of a track nobody has asked for yet is 15. - Two rate limits apply. Ours is per plan — 10 requests a minute on Free, 60 on Pro, up to 900 on Mega. Telegram runs its own flood control on top of that.
- References are not permanent. A
file_idpoints into storage you do not own. On a failed send, request a fresh one. - Platforms move. When YouTube changes something the fix ships on our side; your request shape stays put.
faq
questions about Telegram bot delivery
What exactly is a Telegram file_id?
An opaque string Telegram issues for a file already sitting on its servers. A handle, not
a URL: nothing to open, nothing to download. You put it where the file would go in
sendAudio and Telegram serves the bytes.
Can I reuse the same file_id in a second bot?
No. It is scoped to one bot; hand it to another and Telegram rejects the send with a
bad-request error about the file identifier. Hence bot_username being required.
Two bots means requesting the track twice and keeping two strings.
What does sending a track through a Telegram bot cost?
7 credits per call to POST /youtube/audio/tg-bot when we already hold the
track, 15 when we do not and have to download it first — a miss is billed as a YouTube
download. Add 2 for GET /youtube/search if that is how you found the ID. Hold on
to the returned string and later sends are ordinary Telegram calls that spend nothing.
What happens when the track is not already cached?
It is downloaded fresh and you still get a file_id — but the call costs 15
credits instead of 7, because there is nothing to reference until the download finishes. It is
slower for the same reason. Millions of tracks are pre-cached, so misses are the exception —
but budget for them, and give the HTTP call a read timeout measured in minutes, not the
handful of seconds most clients default to.
Can a Telegram bot send audio larger than 50 MB?
Not by uploading it — the Bot API caps a bot upload at 50 MB, which a long mix or podcast
episode exceeds. A file_id sidesteps that: your bot uploads nothing, it references
a file Telegram already holds. Self-hosting a Bot API server raises the ceiling, but that
server is then yours to keep alive.
Send your first file_id
Free tier is 1,000 credits — around 140 cached tracks, or 66 if every one is new to the cache.