How to Download TikTok Videos Without Watermark via API

Published on May 29, 2026

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.

Prerequisites

  • A TikLiveAPI account with credits. Every request to /download-video/ costs 1 credit. Pricing is pay-as-you-go and unused credits do not expire. See /pricing/.
  • An API key (issued at registration, viewable on your profile page).
  • A development environment with curl, Python 3.8+ with requests, or Node.js 18+ (for native fetch).
  • Outbound HTTPS access to 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.

Step 1: Get an API Key

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.

Step 2: Find the Video URL or ID

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).

Step 3: Call the Download Endpoint

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.

Step 4: Stream the MP4 to Disk Efficiently

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.

Step 5: Bulk Download With Concurrency

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.

Step 6: Storing Metadata Alongside the File

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.

Legal and Ethical Considerations

Just because you can download a file does not mean you can do anything with it. A few principles worth following:

  • Credit the creator. If you redistribute clips, keep the author.unique_id and the original TikTok URL visible. This is the bare minimum.
  • Respect TikTok's terms of service. Read them. Bulk redistribution and commercial use of someone else's video without permission is generally not allowed.
  • Music licensing is separate from video licensing. The audio track embedded in a TikTok clip is usually licensed to TikTok for use on TikTok. Republishing the MP4 elsewhere may infringe the music rights even if the video creator is fine with it.
  • Honor takedowns. If a creator deletes a video or marks an account private, delete your local copy. Build the delete path into your pipeline from day one.
  • Fair use is jurisdictional. Research, commentary, criticism, and journalism have different protections than a clip aggregator that monetizes other people's content. Get legal advice for production use.

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.

Common Errors

  • Invalid URL. The endpoint expects a real TikTok video URL. URLs from ads.tiktok.com, profile pages, or hashtag pages will not work. Use /ads-detail/ for the ads center URLs.
  • Video deleted or private. If the creator removed the post or set their account to private, TikTok no longer serves the MP4. The API will surface this as an error or an empty response. Handle this by marking the URL as unavailable in your archive index and not retrying.
  • Region-locked content. Some videos are geo-restricted by the creator or by TikTok. The endpoint serves what TikTok exposes; if a video is unavailable in the region the upstream call resolves from, it may return without the expected fields.
  • Expired CDN link. The 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.
  • Rate limit hit. Standard plans cap at 200 requests per minute. Back off with a short sleep and resume.
  • Out of credits. Every call to /download-video/ costs 1 credit. Top up at /pricing/.

FAQ

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.

Next Steps

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.

  • Try the endpoint live in the /playground/ with your own API key before wiring it into a script.
  • Browse the full endpoint reference at /documentation/. There are 37 endpoints covering users, posts, music, hashtags, playlists, and search, all using the same X-Api-Key auth pattern shown here.
  • Combine /user-posts/ with /download-video/ to build a complete creator archiver that pulls every post from a given username.
  • If you need bigger limits, a specific region, or have a question that is not in the docs, reach out via /contact/. Responses land within one business day.

The clean download path is one API call away. The rest is just disk space.

Build with the TikTok API

Ready to put what you read into code? Try our endpoints live or grab the full reference.

Open Playground Read Documentation