parth-dl serve starts a local JSON API and web UI, so any app — Node, Go, PHP,
Rust, a browser — can drive the downloader without shelling out or writing Python.
parth-dl serve # http://127.0.0.1:8000, opens your browser
parth-dl serve --port 9000 --dir ~/Videos --no-open| Flag | Default | Meaning |
|---|---|---|
--host |
127.0.0.1 |
Interface to bind. See Security before changing. |
--port |
8000 |
Port to listen on. |
--dir |
./downloads |
Where files are written. |
--no-open |
off | Don't open a browser on start. |
-v |
off | Log every request. |
curl localhost:8000/api/health
# {"ok": true, "version": "1.1.0"}Metadata, no download. The body is the metadata schema.
curl -X POST localhost:8000/api/info \
-H 'Content-Type: application/json' \
-d '{"url": "https://www.instagram.com/reel/Cxyz123AbCd/"}'Starts a job and returns immediately with 202.
curl -X POST localhost:8000/api/download \
-H 'Content-Type: application/json' \
-d '{"url": "https://www.instagram.com/reel/Cxyz123AbCd/", "quality": "best"}'
# {"job_id": "3f9a...e21"}quality is best (default) or worst.
Poll for progress.
{
"id": "3f9a...e21",
"url": "https://www.instagram.com/reel/Cxyz123AbCd/",
"state": "running",
"percent": 62,
"files": [],
"error": null
}state is running, done, or error.
percentis-1when the CDN sends noContent-Length. Show an indeterminate spinner, not0%.- On
done,filesis[{"name": "clip.mp4", "url": "/files/clip.mp4"}]. - On
error,errorholds the message.
Job history is capped at the 100 most recent, so a long-running server can't grow without bound. Poll and consume promptly.
Downloads a completed file. Only serves files directly inside --dir.
Errors are {"error": "message"} with a status you can branch on — you never have to
parse the message:
| Status | Means |
|---|---|
400 |
Bad request: not an instagram.com URL, malformed body, bad quality. |
403 |
Forbidden: path traversal, or a non-loopback Host header. |
404 |
Content is private, deleted, or unsupported. Also: unknown route or job. |
429 |
Instagram is rate limiting. Back off. |
502 |
The transfer failed upstream after retries. |
const BASE = "http://127.0.0.1:8000";
async function download(url) {
const res = await fetch(`${BASE}/api/download`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url }),
});
if (!res.ok) throw new Error((await res.json()).error);
const { job_id } = await res.json();
for (;;) {
const job = await (await fetch(`${BASE}/api/jobs/${job_id}`)).json();
if (job.state === "done") return job.files;
if (job.state === "error") throw new Error(job.error);
await new Promise((r) => setTimeout(r, 400));
}
}The server fetches URLs on your behalf, so it is deliberately locked down:
- Loopback only by default. It binds
127.0.0.1. - The
Hostheader must be loopback. This blocks DNS rebinding — without it, a website you happen to visit could point a hostname at127.0.0.1and drive your downloader. - No CORS header. Cross-origin JavaScript can fire requests but cannot read the responses.
/files/cannot escape the download directory.- Media URLs are host-allowlisted. The downloader will only fetch from Instagram's own CDNs, never an arbitrary host.
Passing --host 0.0.0.0 disables the loopback check and exposes the server to your
network — anyone who can reach the port can then download through your IP address.
There is no authentication. Don't do it on an untrusted network.