Documentation Index
Fetch the complete documentation index at: https://mintlify.com/KevxxAlva/tiktok-bot-downloader/llms.txt
Use this file to discover all available pages before exploring further.
TikTokSaver’s backend is a single Node.js file: api/index.js. It uses Express 4.x to expose three HTTP routes, integrates with the TikWM public API to resolve TikTok video metadata, and proxies all media through its own endpoints so the browser never contacts TikTok CDN directly. The file is written in CommonJS and exported as module.exports = app to satisfy Vercel’s serverless function convention, while a require.main === module guard keeps it runnable as a plain process during local development.
Entry point and setup
The application bootstraps in fewer than fifteen lines before any route is registered.
// api/index.js
const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
app.use(cors());
app.use(express.json());
// Request logger
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
CORS is enabled globally via app.use(cors()) with no origin restrictions. This is appropriate for a public downloader but means any origin can call the API. If you fork this project for a private deployment, consider narrowing the CORS policy to your own domain.
Environment variables
| Variable | Default | Purpose |
|---|
PORT | 3000 | TCP port the Express server binds to when running locally |
dotenv loads a .env file from the current working directory at startup. No API keys are required for TikWM — it is a public, unauthenticated endpoint.
Routes
All three routes are mounted directly on the app instance with no sub-router. A brief overview of each is given below; full parameter and response schemas are documented in the API Reference section.
GET /api/download
Accepts a url query parameter containing a TikTok video or slideshow URL. The handler sends a POST request to TikWM, maps the response into the DownloadResult shape, and returns JSON.
app.get('/api/download', async (req, res) => {
const { url } = req.query;
if (!url) return res.status(400).json({ status: 'error', error: 'URL is required' });
const formData = new FormData();
formData.append('url', url);
formData.append('hd', '1');
const tikResponse = await fetch('https://www.tikwm.com/api/', {
method: 'POST',
body: formData,
});
const tikData = await tikResponse.json();
// ... normalization logic
});
GET /api/proxy-download
Accepts url (CDN media URL) and filename query parameters. Buffers the full media file and sends it to the browser with Content-Disposition: attachment so a native file-save dialog is triggered.
GET /api/proxy-image
Accepts a url query parameter pointing to a TikTok CDN image. Fetches the image with a 10-second AbortController timeout and returns it with a one-year Cache-Control header, allowing browsers to cache thumbnails indefinitely.
TikWM integration
Every call to GET /api/download results in exactly one outbound POST to https://www.tikwm.com/api/ with two form fields:
| Field | Value |
|---|
url | The original TikTok URL provided by the user |
hd | "1" — requests HD video URLs when available |
A successful TikWM response has code === 0 and a populated data object. The handler reads the following fields:
| TikWM field | Used as |
|---|
data.play | Primary clean video URL (H.264, no watermark) |
data.hdplay | HD video URL, used as fallback when play is absent |
data.wmplay | Watermarked video URL |
data.music | Background audio URL |
data.images | Array of image URLs for slideshow posts |
data.cover | Cover/thumbnail image URL |
data.title | Video description text |
data.author.nickname | Author’s display name |
data.author.avatar | Author’s profile picture URL |
TikWM is a public third-party API not operated or maintained by TikTokSaver. It has no published SLA. Heavy or sustained usage may trigger rate limiting or IP blocks at the TikWM layer, causing GET /api/download to return a 500 error with "Could not fetch video info from provider." There is no built-in retry or caching layer for TikWM responses in the current implementation.
Slideshow handling
When data.images is a non-empty array, TikWM has identified the post as a photo slideshow rather than a standard video. In this case the handler deliberately skips adding any DownloadOption entries of type normal or watermark, because TikWM’s synthesized video render for slideshows is frequently a black or broken file. Only the music option is added alongside the images array.
if (!data.images || data.images.length === 0) {
// Add normal / watermark video options
if (data.play) {
downloads.push({ type: 'normal', label: 'Sin Marca', url: data.play, size: data.size });
}
if (data.wmplay) {
downloads.push({ type: 'watermark', label: 'Con Marca (HD)', url: data.wmplay, size: data.wm_size || data.size });
}
}
// Music is always included when available
if (data.music) {
downloads.push({ type: 'music', label: 'Audio MP3', url: data.music, size: null });
}
The frontend’s VideoResult component checks for the presence of result.images to decide whether to render an image grid or a video cover preview.
proxy-download details
The proxy download route handles several real-world edge cases that arise when streaming TikTok CDN media.
Filename sanitization — The filename query parameter may contain emojis, CJK characters, or other symbols that break Content-Disposition headers. Every non-alphanumeric character except ., -, and _ is replaced with _:
const safeFilename = (filename || 'tiktok_video.mp4').replace(/[^a-zA-Z0-9._-]/g, '_');
403 retry logic — The initial request includes a Referer: https://www.tiktok.com/ header. Some CDN nodes reject this and return HTTP 403. The handler detects this and retries the same URL without the Referer header:
let response = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 ...',
'Referer': 'https://www.tiktok.com/',
'Range': 'bytes=0-',
}
});
if (response.status === 403) {
response = await fetch(url, {
headers: { 'User-Agent': 'Mozilla/5.0 ...' }
});
}
Extension correction — After the response arrives, the handler inspects the Content-Type header and auto-corrects the file extension if it does not match:
| Condition | Correction |
|---|
Content-Type contains audio and filename ends with .mp4 | Extension changed to .mp3 |
Content-Type contains image and filename ends with .mp4 | Extension changed to .jpeg |
The full media response is buffered into memory before being sent so that Content-Length can be set accurately — preventing browsers from showing an indeterminate progress bar.
Serverless compatibility
The file uses a require.main === module guard to differentiate between local execution and Vercel serverless invocation:
// For local development — starts a listening HTTP server
if (require.main === module) {
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
});
}
// For Vercel — exports the app as a serverless handler
module.exports = app;
When Vercel imports api/index.js as a serverless function, require.main is not equal to module, so app.listen is never called. Vercel instead uses the exported app directly to handle incoming requests.
Dependencies
{
"dependencies": {
"@tobyg74/tiktok-api-dl": "^1.0.45",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.2"
}
}
@tobyg74/tiktok-api-dl is listed in api/package.json but the active download logic uses a direct fetch call to https://www.tikwm.com/api/ rather than calling this library. It is retained as a dependency in case fallback scraping logic is needed in a future version.
The type field in api/package.json is "commonjs", which is intentional — Vercel’s Node.js serverless runtime expects CommonJS for module.exports-style entry points.