Telegram Downloader API documentation
One endpoint turns a public Telegram post into a direct download link. This page covers everything you need to call it from a server, a bot or an app.
Introduction
The API is a JSON REST API served over HTTPS. The base URL is:
https://telegramdownloader.net/api/v1
It works with links to a single post in a public channel or public group, for example https://t.me/durov/537. It returns the file in the highest quality Telegram stores for that post, along with a download link and, for video and audio, a link that plays in the browser.
Authentication
Create a key in your dashboard and send it with every request in the X-Api-Key header. Authorization: Bearer <key> also works.
X-Api-Key: tdk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Keys start with tdk_. We store only a hash of each key, so we can show it to you once, when you create it. Keep it on your server: never ship it inside a mobile app or a web page, where anyone can read it. If a key leaks, revoke it in the dashboard and create a new one.
Quick start
curl -X POST https://telegramdownloader.net/api/v1/resolve \
-H "X-Api-Key: $TD_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://t.me/durov/537"}'
The response contains download_url. Open it in a browser and the file starts downloading.
POST /v1/resolve
Prepares a file from a public post and returns signed links to it. Send the body as JSON. Form-encoded bodies are accepted too.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | yes | Link to one public post: https://t.me/channel/123. Links in the t.me/s/channel/123 form are accepted and normalised. |
ip | string | no | IP address of the person who will download the file. Send it whenever the link is for an end user: per-IP limits then apply to them and not to your server. Defaults to the IP that made the call. |
Response 200
{
"download_url": "https://dl.telegramdownloader.net/dl/ab12cd34ef.1792484409.7bb34bb53cfc1d12",
"stream_url": "https://dl.telegramdownloader.net/s/ab12cd34ef.1792484409.7bb34bb53cfc1d12",
"preview_url": "https://telegramdownloader.net/api/v1/thumb?u=…",
"file_name": "interview-1080p.mp4",
"file_size": 734003200,
"mime_type": "video/mp4",
"expires_at": "2026-10-22T12:00:00+00:00",
"usage": {
"used": 41,
"limit": 1000,
"remaining": 959,
"period_ends_at": "2026-11-18T09:12:44+00:00"
}
}
| Field | Description |
|---|---|
download_url | Starts a download (Content-Disposition: attachment). |
stream_url | Same file, served inline for playback in a <video> or <audio> tag. May be null. |
preview_url | Thumbnail image when Telegram has one, otherwise null. Valid for 24 hours. |
file_name, file_size, mime_type | Original name, size in bytes and media type of the file. |
expires_at | ISO 8601 time after which both links stop working. |
usage | Your balance after this call. |
Using the download and stream links
- Resumable. Links support HTTP
Rangerequests, so download managers can pause and resume and players can seek.HEADreturns the headers without the body. - Protected. Each link is signed and checked on every request against its expiry time, a maximum number of downloads, a maximum number of different IP addresses and a limit on parallel transfers. A link shared publicly stops working for strangers quickly.
- Not counted. Downloading or streaming does not use requests from your plan, however large the file is.
# resume a download from 100 MB curl -H "Range: bytes=104857600-" -o file.mp4 "$DOWNLOAD_URL"
GET /v1/usage
Returns your current balance. It is free and never counted.
{
"plan": "Pro",
"used": 41,
"limit": 1000,
"remaining": 959,
"period_ends_at": "2026-11-18T09:12:44+00:00",
"rate_limit_per_minute": 120
}
Errors
Errors use standard HTTP status codes and always have the same shape:
{ "error": { "code": "quota_exceeded", "message": "You have used all requests in your plan." } }
| Status | code | Meaning | Counted |
|---|---|---|---|
| 401 | missing_api_key, invalid_api_key | No key, or the key is wrong or revoked. | No |
| 402 | no_active_plan, quota_exceeded | Buy a plan or add requests. The body includes buy_url. | No |
| 403 | channel_blocked | Downloads from this channel are disabled. | No |
| 403 | ip_blocked | The end-user IP is blocked or rate limited. | No |
| 403 | account_suspended | Contact support. | No |
| 404 | post_not_found | The post does not exist or was deleted. | No |
| 404 | no_media | The post has no file attached. | No |
| 406 | content_protected | The channel switched on Telegram's content protection. | No |
| 415 | file_type_blocked | This file type is not supported (for example APK). | No |
| 422 | invalid_url, invalid_ip | The link is not a public post link, or ip is not an IP address. | No |
| 429 | rate_limited | Too many calls this minute. Wait for Retry-After seconds. | No |
| 502 | upstream_error | The file could not be prepared right now. Retry after a short wait. | No |
| 503 | service_unavailable | Temporary outage. Retry with backoff. | No |
Quotas and rate limits
Each plan has a number of successful requests for its period and a per-minute rate limit. Every response from /v1/resolve carries these headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Calls allowed per minute on your plan. |
X-RateLimit-Remaining | Calls left in the current minute. |
X-Quota-Remaining | Successful requests left in your plan (on 200 responses). |
Retry-After | Seconds to wait (on 429 responses). |
When you buy a plan while one is active, the new requests are added to what you have left and the period starts again. We email you when you reach 80% and 100% of your requests.
Good practice
- Call from your server. Your key is a password. Keep it out of client code.
- Pass the end user's IP. Per-IP limits then protect your users from each other instead of blocking your server.
- Reuse links until they expire. If several users ask for the same post, cache the response until
expires_atand save requests. - Retry only on 429, 502 and 503, with a growing delay. The other errors will give the same answer again.
- Set a long timeout. Large files can take several seconds to prepare. Allow at least 60 seconds.
Code examples
Node.js
async function resolve(url, userIp) {
const res = await fetch("https://telegramdownloader.net/api/v1/resolve", {
method: "POST",
headers: { "X-Api-Key": process.env.TD_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ url, ip: userIp }),
signal: AbortSignal.timeout(90_000),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error.code}: ${data.error.message}`);
return data;
}
Python
import os, requests
def resolve(url, user_ip=None):
r = requests.post(
"https://telegramdownloader.net/api/v1/resolve",
headers={"X-Api-Key": os.environ["TD_KEY"]},
json={"url": url, "ip": user_ip},
timeout=90,
)
data = r.json()
if r.status_code != 200:
raise RuntimeError(f'{r.status_code} {data["error"]["code"]}')
return data
PHP
function td_resolve(string $url, ?string $userIp = null): array {
$ch = curl_init('https://telegramdownloader.net/api/v1/resolve');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 90,
CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('TD_KEY'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['url' => $url, 'ip' => $userIp]),
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 200) {
throw new RuntimeException($data['error']['message'] ?? 'Request failed');
}
return $data;
}
Telegram bot (python-telegram-bot)
async def on_message(update, context):
link = update.message.text.strip()
try:
data = resolve(link, None)
except RuntimeError as e:
await update.message.reply_text(f"Could not get that file: {e}")
return
await update.message.reply_text(f'{data["file_name"]}\n{data["download_url"]}')
Get a key and try it
Sign up, confirm your email and your first call is a minute away.