Fill these in to personalise every example on this page. Nothing is sent anywhere.
SPVD API guide
Extract downloadable media and rich metadata from YouTube, Instagram, Facebook, TikTok, Dailymotion and Pinterest, and turn separate video and audio streams into a single ready-to-play MP4.
Overview
SPVD is a REST API. You send a GET request with a video ID, URL or search query, and get back JSON with:
- Media: direct links to every available video, audio and image quality, with size, resolution and codec details.
- Render configs: optional links that ask our rendering servers to merge the best video and audio into one MP4 (or extract audio), hosted for you to download.
- Metadata: titles, authors, statistics, comments, playlists, channel pages, search results and transcripts.
| Platform | Endpoints | What you can get |
|---|---|---|
| YouTube | 17 | Video details, Video comments, Community post details, Community post comments, Playlist, Search (all types), Search videos, Search channels, Search playlists, Search movies, Search suggestions, Channel details, Channel videos / shorts / lives, Channel playlists / releases / podcasts, Channel community posts, Channel ID from handle, Hashtag feed |
| 4 | Post / reel details, Audio (reels using a sound), Profile details, User ID from username | |
| 6 | Post / video / reel details, Profile details, Profile ID, Profile “About”, Profile reels, Profile photos | |
| TikTok | 2 | Post details, User details |
| Dailymotion | 1 | Video details |
| 1 | Pin details |
Quickstart
- Subscribe and copy your key. Pick a plan on the API marketplace and copy your API key and host from the endpoint playground.
- Make your first call. Fetch a YouTube video's details:
const res = await fetch("https://api-host/youtube/v3/video/details?videoId=dQw4w9WgXcQ", { headers: { "X-RapidAPI-Key": "YOUR_API_KEY", "X-RapidAPI-Host": "api-host" }, }); const { error, contents, metadata } = await res.json(); if (error) throw new Error(error.message); console.log(metadata.title, contents[0].videos.length, "video formats");import requests data = requests.get( "https://api-host/youtube/v3/video/details", params={"videoId": "dQw4w9WgXcQ"}, headers={"X-RapidAPI-Key": "YOUR_API_KEY", "X-RapidAPI-Host": "api-host"}, timeout=120, ).json() if data["error"]: raise RuntimeError(data["error"]["message"]) print(data["metadata"]["title"], len(data["contents"][0]["videos"]), "video formats")<?php $ch = curl_init("https://api-host/youtube/v3/video/details?videoId=dQw4w9WgXcQ"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, CURLOPT_HTTPHEADER => ["X-RapidAPI-Key: YOUR_API_KEY", "X-RapidAPI-Host: api-host"], ]); $data = json_decode(curl_exec($ch), true); if ($data["error"]) { throw new RuntimeException($data["error"]["message"]); } echo $data["metadata"]["title"], " - ", count($data["contents"][0]["videos"]), " video formats", PHP_EOL;curl "https://api-host/youtube/v3/video/details?videoId=dQw4w9WgXcQ" \ -H "X-RapidAPI-Key: YOUR_API_KEY" \ -H "X-RapidAPI-Host: api-host" - Pick a file. Choose an item from
contents[0].videos(oraudios) and download itsurl, or request a rendered MP4 if you need video and audio in one file.
Authentication
Every data endpoint needs the two headers issued by the API marketplace:
X-RapidAPI-Key: YOUR_API_KEY
X-RapidAPI-Host: api-hostAnyone with your key can spend your quota. Call the API from your backend and pass only the results to browsers or apps.
These need no key: the health check, the OpenAPI specs, and the render URLs inside a renderConfig (the URL itself is the credential).
Making requests
- Base URL:
https://api-host— every path starts with the platform and version, e.g./youtube/v3/…,/tiktok/v3/…. - Method:
GET, parameters in the query string. URL-encode values, especiallyurlparameters (encodeURIComponentin JS,params=in Python requests). - Lists: multi-value parameters are comma-separated:
renderableFormats=720p,1080p. - Timeouts: most calls answer in 1–5 seconds, but a cold video with many formats can take longer. Use a client timeout of at least 60 seconds.
- Freshness: identical requests may be served from a short-lived cache (a couple of minutes), so repeated calls are fast and consistent.
GET https://api-host/facebook/v3/post/details?url=https%3A%2F%2Fwww.facebook.com%2Fwatch%2F%3Fv%3D1234567890Response format
All data endpoints return the same envelope. Full schema: ExtractionResult.
{
"error": null, // or { "message", "code", "statusCode" }
"contents": [ // downloadable media (absent for metadata-only endpoints)
{
"videos": [ /* MediaItem */ ],
"audios": [ /* MediaItem */ ],
"images": [ /* MediaItem */ ],
"renderableVideos": [ /* RenderableMediaItem */ ],
"renderableAudios": [ /* RenderableMediaItem */ ]
}
],
"metadata": { // non-file information
"title": "Never Gonna Give You Up",
"thumbnailUrl": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
"author": { },
"additionalData": { }
},
"notes": ["Endpoint cost: 1 credit."]
}error first.Extraction failures (private post, removed video, bad ID…) are reported inside the body, often with HTTP 200. Don't rely on the status code alone.
A media item
{
"label": "720p",
"url": "https://…", // temporary link
"repId": "5f1c…", // stable id for this quality
"metadata": {
"mime_type": "video/mp4; codecs=\"avc1.4d401f\"",
"width": 1280,
"height": 720,
"has_audio": false,
"content_length": 47395612,
"content_length_text": "45.2 MB",
"fps": 30,
"bitrate": 1512000
}
}Downloading media
Direct links
- Links are temporary. Download soon after you receive them. If a link returns
403,404or410, call the endpoint again for fresh links. Don't store links in a database. - Video-only vs. muxed. On YouTube and Facebook, qualities above 360p/SD are usually video-only (
has_audio: false), with audio inaudios. Either combine them yourself or use rendering. - Range requests are supported by most links, so you can resume or stream in chunks.
- Dailymotion
videosare HLS playlists (.m3u8). Use a player that supports HLS, or a render config to get an MP4.
YouTube urlAccess
| Value | Use it when | Cost |
|---|---|---|
normal (default) | You download from a server or device that YouTube serves normally. Fastest. | 1 credit |
proxied | Direct links return 403 in your environment (some cloud providers and regions). Links are relayed through the API; slower but reliable. | 2 credits |
Server-side rendering
Rendering produces a single, ready-to-play file that the platform doesn't offer directly: best video merged with audio (YouTube), a DASH stream turned into an MP4 (Instagram, Facebook), an HLS stream converted to MP4 (Dailymotion), or extracted audio (TikTok, Dailymotion). The file is hosted temporarily for you to download.
- Ask for renderable formats. Add
renderableFormatsto a details request, e.g.renderableFormats=1080p,720porhighres. - Pick an item. Each entry in
renderableVideosis either a success (hasrenderConfig) or an error (haserror, e.g. over your plan's size limit). - Start the render.
GETtherenderConfig.executionUrl. No API key is needed. Calling it again later is safe: you'll re-attach to the same job. - Watch progress. Open
statusUrl(WebSocket) orsseStatusUrl(Server-Sent Events). Events arrive about once per second untilstatusisdoneorfailed. - Download. When
done, downloadoutput.urlbeforeexpires_at.
// 1) Ask for a rendered 1080p file
const res = await fetch("https://api-host/youtube/v3/video/details?videoId=dQw4w9WgXcQ&renderableFormats=1080p", {
headers: { "X-RapidAPI-Key": "YOUR_API_KEY", "X-RapidAPI-Host": "api-host" },
});
const data = await res.json();
if (data.error) throw new Error(data.error.message);
// 2) Pick a successful renderable item
const item = data.contents[0].renderableVideos?.find((r) => r.renderConfig);
if (!item) throw new Error("No renderable format available");
// 3) Start the job (no API key needed)
await fetch(item.renderConfig.executionUrl);
// 4) Follow progress until done
const fileUrl = await new Promise((resolve, reject) => {
const ws = new WebSocket(item.renderConfig.statusUrl);
ws.onmessage = (e) => {
const job = JSON.parse(e.data);
console.log(job.status, job.progress + "%");
if (job.status === "done") { resolve(job.output.url); ws.close(); }
if (job.status === "failed" || job.status === "not_found") { reject(new Error(job.error)); ws.close(); }
};
ws.onerror = reject;
});
// 5) Download
console.log("Download:", fileUrl);import json, requests
# 1) Ask for a rendered 1080p file
data = requests.get(
"https://api-host/youtube/v3/video/details",
params={"videoId": "dQw4w9WgXcQ", "renderableFormats": "1080p"},
headers={"X-RapidAPI-Key": "YOUR_API_KEY", "X-RapidAPI-Host": "api-host"},
timeout=120,
).json()
if data["error"]:
raise RuntimeError(data["error"]["message"])
# 2) Pick a successful renderable item
item = next(r for r in data["contents"][0].get("renderableVideos", []) if "renderConfig" in r)
cfg = item["renderConfig"]
# 3) Start the job (no API key needed)
requests.get(cfg["executionUrl"], timeout=60)
# 4) Follow progress with Server-Sent Events until done
def watch_render(sse_url):
with requests.get(sse_url, stream=True, timeout=900) as stream:
for line in stream.iter_lines(decode_unicode=True):
if not line or not line.startswith("data:"):
continue
job = json.loads(line[5:])
print(job["status"], job.get("progress"))
if job["status"] == "done":
return job["output"]
if job["status"] in ("failed", "not_found"):
raise RuntimeError(job.get("error"))
output = watch_render(cfg["sseStatusUrl"])
# 5) Download
print("Download:", output["url"])<?php
// 1) Ask for a rendered 1080p file
$ch = curl_init("https://api-host/youtube/v3/video/details?" . http_build_query([
"videoId" => "dQw4w9WgXcQ",
"renderableFormats" => "1080p",
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
CURLOPT_HTTPHEADER => ["X-RapidAPI-Key: YOUR_API_KEY", "X-RapidAPI-Host: api-host"],
]);
$data = json_decode(curl_exec($ch), true);
if ($data["error"]) {
throw new RuntimeException($data["error"]["message"]);
}
// 2) Pick a successful renderable item
$item = null;
foreach ($data["contents"][0]["renderableVideos"] ?? [] as $candidate) {
if (isset($candidate["renderConfig"])) {
$item = $candidate;
break;
}
}
if ($item === null) {
throw new RuntimeException("No renderable format available");
}
$cfg = $item["renderConfig"];
// 3) Start the job (no API key needed)
file_get_contents($cfg["executionUrl"]);
// 4) Follow progress with Server-Sent Events until done
function watchRender(string $sseUrl): array
{
$buffer = "";
$final = null;
$ch = curl_init($sseUrl);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Accept: text/event-stream"],
CURLOPT_TIMEOUT => 900,
CURLOPT_WRITEFUNCTION => function ($ch, string $chunk) use (&$buffer, &$final) {
$buffer .= $chunk;
while (($pos = strpos($buffer, "\n\n")) !== false) {
$event = substr($buffer, 0, $pos);
$buffer = substr($buffer, $pos + 2);
foreach (explode("\n", $event) as $line) {
if (!str_starts_with($line, "data:")) {
continue;
}
$job = json_decode(trim(substr($line, 5)), true);
echo $job["status"], " ", $job["progress"] ?? "", PHP_EOL;
if (in_array($job["status"], ["done", "failed", "not_found"], true)) {
$final = $job;
return 0; // stop reading, the job has finished
}
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
if ($final === null) {
throw new RuntimeException("Status stream ended unexpectedly");
}
if ($final["status"] !== "done") {
throw new RuntimeException($final["error"] ?? "Render failed");
}
return $final["output"];
}
$output = watchRender($cfg["sseStatusUrl"]);
// 5) Download
echo "Download: ", $output["url"], PHP_EOL;Rendering is included in the request that returned the render config (see credits). Starting, watching and downloading the job costs nothing extra.
The three render URLs
| URL | How to call it | What you get |
|---|---|---|
executionUrl | GET, no headers | Starts the render (or re-attaches to it) and returns the job id. Details. |
statusUrl | WebSocket | A status event about every second until the job ends. Details. |
sseStatusUrl | GET as Server-Sent Events | The same events, for clients without WebSockets. Details. |
Treat these URLs as opaque: use them exactly as returned. They need no API key because the URL itself is the credential, so don't publish them.
Job statuses
| Status | Meaning |
|---|---|
pending | Accepted, about to start. |
queued | Waiting for capacity. Starts automatically; keep listening. |
downloading_inputs | Fetching the source streams (progress 0–50). |
processing | Merging / converting (progress 50–60). |
uploading_output | Storing the file for download (progress 60–100). |
done | Finished. output.url is ready. Final. |
failed | Stopped; see error. Final. |
not_found | The job was never started or has expired. Final. |
Good to know
- Safe to repeat. Calling
executionUrlagain attaches to the running job, or returns the finished file instantly. A failed or stalled job is restarted. - Reconnecting is fine. Jobs keep running whether or not anyone listens. If the socket drops, open the status URL again; a finished job immediately sends its final event.
- Expiry. The file can be downloaded until
expires_at(1–2 hours depending on your plan). After that the status streams reportnot_found. - Start soon. Render configs point at temporary source links. Start the render within a few minutes; if it fails because a source expired, request the details endpoint again for a fresh
renderConfig. - Timing. Seconds for short clips, typically 30–120 seconds for long HD videos, longer when
queued.
| Failure | What to do |
|---|---|
| A source link expired or was refused. | Call the details endpoint again and start the new executionUrl. |
| The file is larger than your plan allows. | Choose a smaller quality or use highres, which respects your limit. |
| Timeout / incomplete download. | Call the same executionUrl again; the job restarts. |
| Service at capacity (“try again later”). | Wait a few seconds and call executionUrl again. |
Choosing renderable formats
You can name exact qualities or let the API choose:
| You send | You get |
|---|---|
1080p | Exactly 1080p, if available and within your plan's size limit. |
highres | The best quality that fits your plan. Recommended default. |
lowres / midres | Smallest / middle quality that fits your plan: great for previews and mobile. |
all | Every resolution, smallest first, up to your plan's count. |
all_highres | Every resolution, largest first, up to your plan's count. |
1080p,720p,highres | Combinations work; each value produces one item. |
Full lists per platform: RenderableFormat.
Field filtering
YouTube video details accepts fields to return only what you need. Smaller responses are faster, and skipping contents avoids preparing download links at all.
| Example | Effect |
|---|---|
fields=metadata | Metadata only; no media links. |
fields=contents.videos,contents.audios | Only the video and audio lists. |
fields=-metadata.additionalData | Everything except the large additionalData block. |
fields=-contents,-metadata.transcript | Exclude several parts. |
Rules: when any field is listed without -, only listed fields are returned (whitelist). When every field starts with -, everything except those is returned (blacklist). Listing a child (contents.videos) automatically includes its parent. Allowed values: Field.
Pagination
Comments, playlists, channel tabs, hashtags and search results are paginated. Each page includes a nextToken; send it back unchanged with the same other parameters to get the next page. It is null on the last page.
async function* channelVideos(channelId) {
let nextToken;
do {
const qs = new URLSearchParams({ channelId, contentType: "videos", ...(nextToken && { nextToken }) });
const res = await fetch(`https://api-host/youtube/v3/channel/videos?${qs}`, {
headers: { "X-RapidAPI-Key": "YOUR_API_KEY", "X-RapidAPI-Host": "api-host" },
});
const { error, metadata } = await res.json();
if (error) throw new Error(error.message);
yield* metadata.channel.videos.contents;
nextToken = metadata.channel.videos.nextToken;
} while (nextToken);
}
for await (const video of channelVideos("UCX6OQ3DkcsbYNE6H8uQQuVA")) {
console.log(video);
}import requests
def channel_videos(channel_id):
next_token = None
while True:
params = {"channelId": channel_id, "contentType": "videos"}
if next_token:
params["nextToken"] = next_token
data = requests.get(
"https://api-host/youtube/v3/channel/videos",
params=params,
headers={"X-RapidAPI-Key": "YOUR_API_KEY", "X-RapidAPI-Host": "api-host"},
timeout=120,
).json()
if data["error"]:
raise RuntimeError(data["error"]["message"])
page = data["metadata"]["channel"]["videos"]
yield from page["contents"]
next_token = page["nextToken"]
if not next_token:
break
for video in channel_videos("UCX6OQ3DkcsbYNE6H8uQQuVA"):
print(video)<?php
function channelVideos(string $channelId): Generator
{
$nextToken = null;
do {
$params = ["channelId" => $channelId, "contentType" => "videos"];
if ($nextToken) {
$params["nextToken"] = $nextToken;
}
$ch = curl_init("https://api-host/youtube/v3/channel/videos?" . http_build_query($params));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
CURLOPT_HTTPHEADER => ["X-RapidAPI-Key: YOUR_API_KEY", "X-RapidAPI-Host: api-host"],
]);
$data = json_decode(curl_exec($ch), true);
if ($data["error"]) {
throw new RuntimeException($data["error"]["message"]);
}
$page = $data["metadata"]["channel"]["videos"];
yield from $page["contents"];
$nextToken = $page["nextToken"];
} while ($nextToken);
}
foreach (channelVideos("UCX6OQ3DkcsbYNE6H8uQQuVA") as $video) {
print_r($video);
}Tokens expire after about 15 minutes and are tied to the original query. An expired or altered token returns invalid_params / invalid_token; restart from the first page.
Language & region
| Parameter | Effect |
|---|---|
lang | Language for localized text (titles, labels, relative dates) where the platform supports it. Two-letter code: en, es, fr, de, pt… |
countryCode | Fetch as if from this country. Useful for geo-restricted videos and regional results. Two-letter code or worldwide. |
Lists of supported codes: countries & languages. On Dailymotion, geo-restricted videos are retried from an allowed country automatically when you don't send countryCode.
Transcripts
Add getTranscript=true to YouTube video details. The available languages are always listed in metadata.transcript.languages (even without getTranscript), so a common pattern is:
- Call video details once and read
metadata.transcript.languages. - Call again with
getTranscript=true&transcriptLanguage=<one of those values>. Addfields=metadata.transcriptto keep the response small.
If a transcript can't be fetched, metadata.transcript.error is set while the rest of the response is still returned.
Credits & billing
| Request | Credits |
|---|---|
| Most endpoints | 1 |
| YouTube video details, standard | 1 |
YouTube video details with urlAccess=proxied or at least one render config returned | 2 (never more) |
| Instagram profile details, Instagram user ID from username | 5 (successful requests) |
| Starting, watching and downloading a render (render URLs) | 0 |
The notes array in the response tells you what a request cost.
Plans & limits
Plans differ in how many formats you can render per request, how large each rendered file may be, and how long rendered files stay available.
| Plan | Renderable formats per request | Max size per render | Rendered file available for |
|---|---|---|---|
| BASIC | 1 | 500 MB | 1 hour |
| PRO | 3 | 1.5 GB | 1 hour |
| ULTRA | 6 | 2.5 GB | 2 hours |
| MEGA | 6 | 3.5 GB | 2 hours |
Request quotas and rate limits are those shown on the API marketplace for your plan. Exceeding them returns HTTP 429 from the marketplace.
Errors
HTTP status codes
| Status | Meaning |
|---|---|
200 | Request processed. Check error in the body: extraction errors are usually returned with 200. |
400 | A query parameter failed validation (unknown enum value, bad country code…). The body names the parameter. |
401 / 403 | Missing or invalid API key, or no active subscription. |
404 | Unknown path. |
429 | Rate limit or quota exceeded. |
5xx | Temporary problem. Retry with backoff. |
Error codes
error.code | What it means | What to do |
|---|---|---|
params_not_found | A required parameter is missing. | Check the endpoint's required parameters. |
invalid_params | A value is malformed (bad ID, wrong contentType, expired nextToken…). | Fix the value; restart pagination if needed. |
invalid_url / invalid_platform_url | The url isn't a valid link, or belongs to another platform. | Send a full https:// link for the right platform. |
invalid_token | Pagination token is invalid or expired. | Start again without nextToken. |
invalid_user_details | Subscription details could not be read. | Call through the marketplace with valid headers. |
not_found | The content doesn't exist or was removed. | Verify the ID/URL. |
unavailable | The content exists but can't be accessed (private, region-locked, members-only). | Try another countryCode, or skip. |
signin_required | The platform requires a signed-in user for this content. | Not supported; skip. |
age_restricted | Age-restricted content. | Not supported; skip. |
token_expired | A signed link or token has expired. | Request fresh data. |
non_2xx_response / connection_error / unexpected_redirect | The source platform didn't respond as expected. | Retry after a short delay. |
operation_timeout | The source took too long. | Retry after a short delay. |
unsupported_proxy_for_geo_targeting | The requested countryCode can't be used for this request. | Try another country or omit it. |
invalid_response_data | The platform returned data we couldn't understand. | Retry; report it if it persists. |
unexpected_error / other | Something went wrong on our side. | Retry with backoff; contact support with the request details if it persists. |
OpenAPI spec
Machine-readable OpenAPI 3 documents are served by the API itself. They are free and need no key:
| URL | Contents |
|---|---|
GET /openapi | All platforms in one document, tags grouped per platform. |
GET /openapi/platforms | List of available per-platform documents. |
GET /openapi/{platform} | One platform: youtube, instagram, facebook, tiktok, dailymotion, pinterest, or openapi (these spec routes themselves). |
import { writeFile } from "node:fs/promises";
const spec = await (await fetch("https://api-host/openapi")).json();
await writeFile("spvd.openapi.json", JSON.stringify(spec, null, 2));import requests
spec = requests.get("https://api-host/openapi", timeout=30).text
with open("spvd.openapi.json", "w", encoding="utf-8") as f:
f.write(spec)<?php
file_put_contents("spvd.openapi.json", file_get_contents("https://api-host/openapi"));curl "https://api-host/openapi" -o spvd.openapi.jsonImport the file into Postman, Insomnia, Bruno, or a generator such as openapi-generator to get a typed client.
Best practices
- Handle
erroron every response, and treat eachrenderableVideositem individually: some may succeed while others report anerror. - Retry smartly. Retry
5xx,429,connection_error,non_2xx_responseandoperation_timeoutwith exponential backoff (for example 1s, 2s, 4s). Don't retryparams_not_found,invalid_*,not_foundorsignin_required. - Request only what you need with
fields, and ask for render configs only when the user will actually download. - Prefer
highresover hard-coded qualities: it adapts to what the video offers and to your plan's limits. - Download promptly. Media links and rendered files are temporary.
- Don't parse IDs from our links. All URLs we return are opaque and may change format at any time.
FAQ
Why does my 1080p YouTube download have no sound?
High qualities are delivered as separate video and audio streams (has_audio: false). Use renderableFormats=1080p to get a merged MP4, or combine the streams yourself.
A link worked yesterday and now returns 403/410.
Links are temporary by design. Call the endpoint again for fresh ones.
I only get 403 on YouTube links from my server.
Some networks are blocked by YouTube. Use urlAccess=proxied.
My renderable item says “beyond the current subscription limit”.
You asked for more formats than your plan renders per request. Ask for fewer, or upgrade.
My renderable item says “No available video … within the subscription size limit”.
That quality doesn't exist for this video or is larger than your plan allows. Try highres, which picks the best allowed size.
Can I download private or members-only content?
No. Only publicly accessible content is supported.
Can I call the API from a browser or mobile app?
Technically yes (CORS is enabled), but your key would be exposed. Proxy requests through your own backend.