YouTube to MP3 without yt-dlp on your own box
YouTube to MP3 over HTTP is one POST request: /youtube/download with
format: "audio", and an MP3 you can fetch comes back in the response. This guide
covers the duration check that keeps you from wasting credits, the Telegram file_id path and its
two prices, working Python and Node, and a straight comparison against
running yt-dlp and ffmpeg on hardware you pay for.
- Main call
POST /youtube/download·format: "audio"- Credits
- 15 · Telegram path 7 cached, 15 on a miss · 2 for a duration check
- You need
- an API key and an HTTP client — no ffmpeg
- Returns
- a
download_urlonhttps://api.fastsaver.io/v1/tunnel
decide first
two ways out, and one of them is probably wrong for you
Audio leaves this API through one of two endpoints. Choosing badly is the most expensive mistake on this page, so decide first and write code second.
| Endpoint | Credits | You get | Pick it when |
|---|---|---|---|
POST /youtube/downloadformat: "audio" | 15 | A URL to an audio file you fetch yourself. | Anything that is not a Telegram bot — web app, worker, CLI. |
POST /youtube/audio/tg-bot | 7 / 15 | A Telegram file_id. No bytes. | The destination is a Telegram chat, and nothing else. |
Audio is not discounted against video: it bills the same 15 credits as a 720p download, because
the number tracks producing a file rather than its size. The Telegram path has two prices, not
one — 7 credits when we already hold that video_id, and the full 15 when we have to
fetch the track first. It is still the right call for a Telegram chat even at 15, because the
answer is a file_id rather than bytes you re-upload yourself, and the miss is what
makes every later request for that track cost 7.
step one
gate on duration before you queue an extraction
Anything that accepts a pasted link will eventually be handed a nine-hour rain-sound upload, and
pulling the audio off that bills exactly what a three-minute single does.
/youtube/info costs 2 credits, produces no file, and answers the one question a
length ceiling needs.
https://api.fastsaver.io/v1/youtube/info
2 credits · trimmed
curl "https://api.fastsaver.io/v1/youtube/info?url=https://youtu.be/8kZ3FvqM1nQ" \
-H "X-Api-Key: fs_sk_•••••••••••"
{
"ok": true,
"video_id": "8kZ3FvqM1nQ",
"title": "Night Bus - Full Mix",
"author": "Blue Hour Radio",
"duration": 512,
"thumbnails": {
"low": "https://i.ytimg.com/vi/8kZ3FvqM1nQ/mqdefault.jpg",
"max": "https://i.ytimg.com/vi/8kZ3FvqM1nQ/maxresdefault.jpg"
}
}
duration is an integer count of seconds, so the gate is one comparison: 512 here,
an eight-and-a-half minute mix, comfortably inside a fifteen-minute ceiling.
title and author come along for free, which spares you a second lookup
when you want to label the file or the chat message.
The untrimmed response also enumerates every resolution with its file size. That side of the call is a video concern and lives on the YouTube downloader API reference — for audio there is nothing to choose between, so duration is the only field this guide reads.
step two
the extraction call
Same endpoint as a video download; the only difference is the format value.
https://api.fastsaver.io/v1/youtube/download
15 credits
curl -X POST "https://api.fastsaver.io/v1/youtube/download" \
-H "X-Api-Key: fs_sk_•••••••••••" \
-H "Content-Type: application/json" \
-d '{"url": "https://youtu.be/8kZ3FvqM1nQ", "format": "audio"}'
{
"ok": true,
"video_id": "8kZ3FvqM1nQ",
"duration": 512,
"filename": "Night Bus - Full Mix (audio, youtube).mp3",
"download_url": "https://api.fastsaver.io/v1/tunnel?id=UJLUoLGd1m2t1ScSTW1Lw...",
"thumbnails": {
"low": "https://i.ytimg.com/vi/8kZ3FvqM1nQ/mqdefault.jpg",
"max": "https://i.ytimg.com/vi/8kZ3FvqM1nQ/maxresdefault.jpg"
}
}
One thing to be clear about before you build filenames or a player around this: the container follows the source, so you get mp3 or m4a depending on what YouTube served for that video. There is no parameter to force one, and no request that promises the other.
- That
filenameis an illustration, not a contract — it is generated per job and its extension is whichever container came back. Read the field rather than stapling one on, and sanitise it before it reaches a filesystem: a video title is arbitrary text chosen by a stranger, slashes and all. download_urlpoints athttps://api.fastsaver.io/v1/tunnel— the same API host you just called, on the/v1/tunnelpath, not a separate download domain. Theidis minted for this job and stops resolving about ten minutes later, so fetch it in the same worker run.thumbnailsis cover art for a player, with no second request.
Check ok rather than the status code alone. A private, deleted or region-locked
video returns a structured failure that retrying will not fix — and a failure is not free: every
failure path bills a flat 0.1 credits, which is nothing once and real money in a
retry loop. Running out of credits comes back as a plain 400 carrying a detail
string — the
shared error table lists the whole surface.
code
python and node, end to end
Python first: duration check, extraction, streamed write to disk. The tunnel URL does not need your API key, so fetch the file with a bare request and keep the header from travelling further than it must.
import os
import requests
BASE = "https://api.fastsaver.io/v1"
api = requests.Session()
api.headers["X-Api-Key"] = os.environ["FASTSAVER_KEY"]
MAX_SECONDS = 15 * 60
def extract_audio(url: str, out_dir: str = ".") -> str:
info = api.get(f"{BASE}/youtube/info", params={"url": url}, timeout=60).json()
if not info.get("ok"):
raise RuntimeError(info.get("detail", "info lookup failed"))
if info["duration"] > MAX_SECONDS:
raise ValueError(f"{info['title']} runs {info['duration']}s - skipped")
job = api.post(f"{BASE}/youtube/download",
json={"url": url, "format": "audio"},
timeout=600).json()
if not job.get("ok"):
raise RuntimeError(job.get("detail", "extraction failed"))
path = os.path.join(out_dir, job["filename"])
with requests.get(job["download_url"], stream=True, timeout=600) as r:
r.raise_for_status()
with open(path, "wb") as fh:
for chunk in r.iter_content(1 << 20):
fh.write(chunk)
return path
print(extract_audio("https://youtu.be/8kZ3FvqM1nQ"))
Node, same shape, streaming through pipeline so a long track never sits in memory:
import { createWriteStream } from 'node:fs';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
const BASE = 'https://api.fastsaver.io/v1';
const auth = {
'X-Api-Key': process.env.FASTSAVER_KEY,
'Content-Type': 'application/json',
};
const job = await fetch(BASE + '/youtube/download', {
method: 'POST',
headers: auth,
body: JSON.stringify({ url: 'https://youtu.be/8kZ3FvqM1nQ', format: 'audio' }),
}).then((r) => r.json());
if (!job.ok) throw new Error(job.detail ?? 'extraction failed');
const file = await fetch(job.download_url);
if (!file.ok) throw new Error('tunnel returned ' + file.status);
await pipeline(Readable.fromWeb(file.body), createWriteStream(job.filename));
console.log(job.filename, job.duration + 's');
Those ten-minute timeouts are not padding. A worker has to pull the source and produce a file before it can answer, and an impatient client hangs up on jobs that were seconds from finishing. Give the POST minutes, and read the key from the environment rather than the file you are about to commit.
the telegram path
when the answer is a file_id, not a file
If the audio is going into a Telegram chat, the generic path is the long way round: 15 credits,
bytes pulled down to your server, then the same bytes uploaded again while the user waits.
POST /youtube/audio/tg-bot collapses all of it into one call that answers
with a Telegram file_id — a reference to a copy already sitting on Telegram's own
servers. Hand it to sendAudio and the bot replies as fast as Telegram can look up its
own file.
Budget it as two numbers. When we already hold that video_id, the call is 7 credits
and returns almost at once. When we do not, we fetch the track first and the call bills the
ordinary download rate of 15 — then stores it, so the next request for the same track is 7. The
store is shared across callers, so a charting single is usually a hit before your bot ever asks
for it, while a bot that lives on obscure uploads should plan its budget nearer 15 than 7 and
treat the 7 as the reward for a catalogue that repeats.
Two constraints come with the path: you address the track by video_id rather
than by URL, and the reference is bound to the one bot username you sent with the request. The
request and response shape, and why Telegram scopes references that way, are documented on the
Telegram bot media API page. The
Telegram music bot guide wires the same call into a
search-and-send bot with an aiogram 3 handler.
honest comparison
should you just run yt-dlp yourself?
Sometimes, yes. For twenty albums on your laptop, install yt-dlp, point it at ffmpeg and keep every flag: exact bitrate, chapter splitting, embedded thumbnails. A local binary beats an HTTP call for a one-off, and this page will not pretend otherwise.
The trade changes when extraction is a feature other people rely on. The software is free; everything attached to running it in production is not.
| What you own | yt-dlp + ffmpeg, self-hosted | This API |
|---|---|---|
| Cost | Nothing to licence. | 15 credits a call. |
| Control | Total — every codec, container and flag. | One format value. |
| Blocking | Yours. Datacenter ranges get challenged, so residential proxies become a standing bill. | Ours. |
| Upstream changes | Yours to track and redeploy; a pinned version rots quietly. | Ours, behind an unchanged endpoint. |
| Compute and egress | Transcoding is CPU-bound, and every byte enters and leaves your host. | One fetch from the tunnel URL. |
Put a number on it. The 100,000 credits on the $9 Pro plan are roughly 6,600 extractions a month — about one small VPS plus a modest proxy subscription, before you count the afternoon you lose the week something upstream changes. Below a few hundred files a month the hosted call wins on total cost; far above it, self-hosting wins on unit cost and you get the flags back. Both answers are defensible. Guessing is not.
honesty
where YouTube audio extraction gets awkward
- A login is a wall. Members-only uploads, purchases, private links and age-gated videos have no audio to give you; the call comes back as a structured failure.
- The transfer link is disposable. It is a
https://api.fastsaver.io/v1/tunnelid minted for one job and good for roughly ten minutes, so write the bytes to your own storage while the worker is still awake, and key your cache byvideo_idrather than by URL. - Nothing is normalised. Loudness, bitrate and tags arrive however the source was encoded. If your product promises consistent audio, ffmpeg is still in your stack — it just no longer has to fetch anything.
- A DJ set is a longer job than a single. No duration ceiling is documented, but throughput falls with runtime, so size your queue and your client timeout for the worst link a user will paste.
- Your plan sets the pace. A free key is allowed 10 calls a minute and a Mega key 900, with Pro and Ultra in between. Seeing a 429 means the queue is running ahead of the plan, not that the link is bad.
- The upstream player is a moving target. When it shifts, the repair happens on our side of the endpoint and your code never learns about it — that maintenance is most of what the credits buy.
Cache by video_id. One track requested by fifty users is fifty 15-credit charges
unless you keep the first result — the easiest way to halve a music bot's bill. The Telegram path
is the one exception, because the cache there is ours, which is exactly why a repeat costs 7
instead of 15. And the unglamorous part: a
key buys extraction, not rights. Whether you may store, replay or monetise a particular recording
is a question for whoever owns it, and the answer stays between you and them.
faq
questions about YouTube audio extraction
How do I convert a YouTube video to MP3 with an API?
One POST to /youtube/download with a body of
{"url": "…", "format": "audio"} and your X-Api-Key header. The
response carries a download_url; fetch it and you have the file. No job id to
poll, no callback to register — the call returns when the audio is ready.
Does the endpoint return an actual .mp3 file?
Usually, but the container tracks the source rather than the request: mp3 for most videos,
m4a for some. Read the filename field instead of appending an extension yourself.
A guaranteed container, a fixed bitrate or written ID3 tags is a mastering step you still own —
this endpoint solves fetching, not encoding.
What does one audio extraction cost?
15 credits — the same as a 720p video, because the price tracks the work of resolving and
producing a file rather than how many bytes you end up with. The optional
/youtube/info lookup adds 2. The free tier's 1,000 credits cover about 66
extractions; Pro's 100,000 cover roughly 6,600.
Should I just run yt-dlp and ffmpeg myself?
For a handful of files on your own machine, yes — install yt-dlp and keep every flag. The calculation changes when extraction is a feature other people depend on, because you then own bot detection, proxy bills, player-change fallout, transcoding CPU and egress. The API rents that maintenance instead of staffing it.
Why does /youtube/audio/tg-bot take a video_id and a bot username instead of a URL?
Because it does not hand you bytes. It answers with a Telegram file_id, and
Telegram binds such a reference to a single bot — hence bot_username. The
video_id doubles as the cache key, and that cache is shared across every caller,
so an already-stored track comes back without anything being downloaded: 7 credits. A track
nobody has asked for yet gets fetched on the spot and bills the ordinary download price, 15.
The Telegram bot media API page documents the call.
Extract one track before you write the worker
Pick /youtube/download in the playground, set format to audio and read the JSON. Your 1,000 free credits are 66 tracks.