If you build a TikTok archiver, a content moderation pipeline, a research dataset, or any tool that needs the raw MP4 from a TikTok post, the watermark is the first wall you hit. The visible TikTok logo and the rotating username burned into the corner make videos unusable for repost workflows, machine learning training sets, and clean editing pipelines. Stripping it after the fact with FFmpeg cropping is lossy, fragile, and produces obvious artifacts.
The cleaner approach is to fetch the original no-watermark MP4 URL directly from TikTok's CDN. That is exactly what the TikLiveAPI /download-video/ endpoint returns. You pass a TikTok video URL and you get back two clean MP4 links: video for the SD no-watermark file and video_hd for the no-watermark HD variant. No FFmpeg, no cropping, no quality loss.
This tutorial walks through the full pipeline a developer needs in production: authenticating, resolving a video URL, calling the endpoint, streaming the MP4 to disk without exhausting memory, doing bulk downloads with concurrency control, and persisting metadata alongside the file so your archive is searchable later.
/download-video/ costs 1 credit. Pricing is pay-as-you-go and unused credits do not expire. See /pricing/.requests, or Node.js 18+ (for native fetch).https://api.tikliveapi.com and to the TikTok CDN domains that serve the MP4 files.Every request is authenticated with the X-Api-Key HTTP header. The standard rate limit on most plans is 200 requests per minute, which matters for the bulk download section later.
Sign up at the dashboard, verify your email, and copy your API key from the profile screen. Treat it like a password: do not commit it to git, do not embed it in client-side JavaScript shipped to browsers, and rotate it if you suspect leakage. For server-side scripts, load it from an environment variable:
export TIKLIVE_API_KEY="your_real_key_here"
You can sanity-check the key by hitting any cheap endpoint first. The /userid/ endpoint returns just a numeric id and is a good liveness probe.
The download endpoint accepts a TikTok video url as its single required parameter. Any of the common URL shapes work: the canonical https://www.tiktok.com/@username/video/1234567890123456789, the short https://vm.tiktok.com/XXXX/ redirect, or the mobile https://m.tiktok.com/v/... form. URL-encode the value before putting it into a query string.
If you are starting from a username and need to discover recent videos, call /user-posts/ first. It returns an array of video objects with aweme_id and video_id fields you can use to reconstruct a canonical URL, plus the play field which is itself a no-watermark URL (useful if you only want the SD copy and want to save a credit).
The endpoint is GET /download-video/ on https://api.tikliveapi.com. It returns a flat JSON object with two keys: video (no-watermark SD MP4 URL) and video_hd (no-watermark HD MP4 URL). Full reference is at /documentation/download/video/.
curl
curl -G "https://api.tikliveapi.com/download-video/" \
--data-urlencode "url=https://www.tiktok.com/@username/video/1234567890123456789" \
-H "X-Api-Key: $TIKLIVE_API_KEY"
Expected response shape:
{
"video": "https://v16-webapp.tiktok.com/.../video.mp4?...",
"video_hd": "https://v16-webapp.tiktok.com/.../video_hd.mp4?..."
}
Python
import os
import requests
API_KEY = os.environ["TIKLIVE_API_KEY"]
BASE = "https://api.tikliveapi.com"
def get_download_urls(tiktok_url: str) -> dict:
r = requests.get(
f"{BASE}/download-video/",
params={"url": tiktok_url},
headers={"X-Api-Key": API_KEY},
timeout=15,
)
r.raise_for_status()
data = r.json()
return {"sd": data["video"], "hd": data.get("video_hd")}
urls = get_download_urls("https://www.tiktok.com/@username/video/1234567890123456789")
print(urls)
Node.js
const BASE = "https://api.tikliveapi.com";
const API_KEY = process.env.TIKLIVE_API_KEY;
async function getDownloadUrls(tiktokUrl) {
const qs = new URLSearchParams({ url: tiktokUrl });
const res = await fetch(`${BASE}/download-video/?${qs}`, {
headers: { "X-Api-Key": API_KEY },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return { sd: data.video, hd: data.video_hd };
}
const urls = await getDownloadUrls(
"https://www.tiktok.com/@username/video/1234567890123456789"
);
console.log(urls);
Both keys point to time-limited TikTok CDN URLs. Do not store them as permanent references; they expire. Download the bytes promptly.
A naive requests.get(url).content loads the entire MP4 into RAM before writing it. For one 5 MB clip that is fine; for a batch of 10,000 videos or for long-form content it will OOM your worker. Use streaming with a fixed chunk size and write the bytes as they arrive.
import requests
from pathlib import Path
def download_mp4(mp4_url: str, dest: Path, chunk_size: int = 1 << 15) -> int:
"""Stream an MP4 to disk. Returns total bytes written."""
dest.parent.mkdir(parents=True, exist_ok=True)
tmp = dest.with_suffix(dest.suffix + ".part")
total = 0
with requests.get(mp4_url, stream=True, timeout=60) as r:
r.raise_for_status()
with open(tmp, "wb") as f:
for chunk in r.iter_content(chunk_size=chunk_size):
if not chunk:
continue
f.write(chunk)
total += len(chunk)
tmp.rename(dest) # atomic publish
return total
urls = get_download_urls("https://www.tiktok.com/@user/video/1234567890123456789")
bytes_written = download_mp4(urls["hd"] or urls["sd"], Path("archive/1234567890123456789.mp4"))
print(f"Wrote {bytes_written} bytes")
Two production touches worth noting: writing to a .part file and renaming on success means a crashed download never leaves a half-written file under the canonical name. A 32 KB chunk size is a reasonable default; bump it to 256 KB if your disk is fast and your network is faster.
Prefer video_hd when it is present, and fall back to video if the response only returned the SD variant for that post.
For an archive job you will want to process hundreds or thousands of URLs. The two limits to respect are the API rate limit (200 requests per minute on standard plans) and your own network. A small thread pool that fans out work and a polite delay are usually enough.
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
def archive_one(tiktok_url: str, out_dir: Path) -> dict:
try:
meta = get_download_urls(tiktok_url)
mp4 = meta["hd"] or meta["sd"]
video_id = tiktok_url.rstrip("/").rsplit("/", 1)[-1]
dest = out_dir / f"{video_id}.mp4"
size = download_mp4(mp4, dest)
return {"url": tiktok_url, "ok": True, "bytes": size, "path": str(dest)}
except Exception as e:
return {"url": tiktok_url, "ok": False, "error": str(e)}
def archive_many(urls: list[str], out_dir: Path, workers: int = 8):
results = []
with ThreadPoolExecutor(max_workers=workers) as pool:
futures = [pool.submit(archive_one, u, out_dir) for u in urls]
for f in as_completed(futures):
results.append(f.result())
return results
Eight concurrent workers comfortably stays under the 200 req/min ceiling even with retries. If you need to push harder, add a token-bucket rate limiter around get_download_urls and back off on HTTP 429.
An MP4 with no context is a dead file. A month from now you will not remember who posted it or what song it used. The /post-detail/ endpoint returns the full metadata for a video in a single flat JSON object, and you can persist that next to the MP4 as a sidecar JSON file.
Useful fields on /post-detail/ include aweme_id, title (the caption), create_time (unix timestamp), duration, play_count, digg_count, comment_count, share_count, the music_info object with id, title, author, original, and duration, plus the author object with id, unique_id, and nickname.
import json
def fetch_post_detail(tiktok_url: str) -> dict:
r = requests.get(
f"{BASE}/post-detail/",
params={"url": tiktok_url},
headers={"X-Api-Key": API_KEY},
timeout=15,
)
r.raise_for_status()
return r.json()
def archive_with_metadata(tiktok_url: str, out_dir: Path):
detail = fetch_post_detail(tiktok_url)
video_id = str(detail["aweme_id"])
mp4_url = detail.get("hdplay") or detail["play"] # both no-watermark
download_mp4(mp4_url, out_dir / f"{video_id}.mp4")
sidecar = {
"id": video_id,
"caption": detail.get("title"),
"duration": detail.get("duration"),
"created_at": detail.get("create_time"),
"author": detail.get("author"),
"music": detail.get("music_info"),
"stats": {
"plays": detail.get("play_count"),
"likes": detail.get("digg_count"),
"comments": detail.get("comment_count"),
"shares": detail.get("share_count"),
},
}
(out_dir / f"{video_id}.json").write_text(json.dumps(sidecar, indent=2))
Note that /post-detail/ already exposes play (no-watermark) and hdplay (no-watermark HD), with wmplay being the watermarked version you almost never want. So a single call to /post-detail/ can replace both /download-video/ and a metadata lookup if you want to save credits.
Just because you can download a file does not mean you can do anything with it. A few principles worth following:
author.unique_id and the original TikTok URL visible. This is the bare minimum.TikLiveAPI does not store TikTok content on its servers; endpoints fetch and return data on demand. The legal responsibility for what you do with the bytes after the response lands on your disk sits with you.
ads.tiktok.com, profile pages, or hashtag pages will not work. Use /ads-detail/ for the ads center URLs.video and video_hd URLs are time-limited TikTok CDN URLs. If you sat on a URL for an hour and then tried to download, expect an HTTP error. Re-call /download-video/ to refresh./download-video/ costs 1 credit. Top up at /pricing/.Does the no-watermark MP4 lose any quality? No. The video_hd URL points at TikTok's original HD encode without any post-processing. It is not a cropped version of the watermarked file.
What is the difference between the video and video_hd fields? video is the SD no-watermark MP4 and video_hd is the HD no-watermark MP4. Use HD when available; fall back to SD when it is not.
Can I get the audio track separately? Yes. The /download-music/ endpoint takes the same TikTok video URL and returns an MP3 URL under the music key. It is a separate credit.
How many requests per minute can I make? Standard plans allow 200 requests per minute. If you need more, contact support.
Do I need a separate auth token or just the API key? Just the API key, sent as the X-Api-Key header on every request. No OAuth, no refresh tokens, no session state.
You now have a complete no-watermark download pipeline: a single endpoint, streaming I/O, bulk concurrency, and metadata sidecars. From here you can extend in a few directions.
X-Api-Key auth pattern shown here./user-posts/ with /download-video/ to build a complete creator archiver that pulls every post from a given username.The clean download path is one API call away. The rest is just disk space.
Ready to put what you read into code? Try our endpoints live or grab the full reference.