Everything you need to resolve movies and TV episodes into playable streams — authentication,
the /stream endpoint, response shapes, player integrations, and plan limits.
| Component | Status | Notes |
|---|---|---|
| Faucet API | Operational | Requests are being accepted and answered normally. |
| Source 1 | Degraded | Facing issues with its sources — expect intermittent failures. |
| Movies / TV | Available | Coverage varies by title while Source 1 is degraded. |
| Multi-source fallback | Available on Plus | Plus plans fall back across sources automatically; free plans do not. |
Treat a failed request as normal, recoverable behaviour: retry with a short backoff, surface a "try again" state to the user, and never cache an error response. Retry logic that survives one bad source is covered in Full Examples.
We will update this page when Source 1 recovers.
Three steps from zero to a playing stream. The free tier is enough to build and ship a real integration, as long as you work within its limits.
faucet_live_… and is shown once.
GET request to
/stream with a TMDB ID and type, include your key as a Bearer token, and play the
url from the response.
curl "https://faucetapi.twnty.top/stream?id=550&type=movie" \
-H "Authorization: Bearer faucet_live_YOUR_KEY"
curl calls only work when the request carries an
Origin header that matches an authorized domain.
Every request is authenticated with an API key sent as a Bearer token in the
Authorization header. Keys are created and revoked from
your API dashboard — there is no other way to obtain one.
Authorization: Bearer faucet_live_YOUR_KEY
Accept: application/json
faucet_live_.401 Unauthorized.
A valid key is only half of the check. Requests must also come from a domain you have registered
in your API dashboard, matched against the browser's Origin header. This is what stops a
leaked key from being reused on someone else's site.
403 Forbidden.localhost and loopback addresses are permitted automatically for local development.example.com); http://, https:// and paths are stripped for you.All requests go to a single host and endpoint:
https://faucetapi.twnty.top/stream
The endpoint accepts GET with query parameters. There are no path segments, no
version prefixes, and no request body.
Resolves a title and returns a ready-to-play, proxied stream URL plus metadata about what was resolved. One call gives you everything needed for playback, including any alternate servers available on your plan.
GET /stream
| Parameter | Type | Status | Description |
|---|---|---|---|
id |
string |
Required | TMDB ID of the movie or TV series (numeric, as a string or number). |
type |
string |
Required | Either movie or tv. |
season |
number |
TV only | Season number (1-based). Required when type=tv. |
episode |
number |
TV only | Episode number (1-based). Required when type=tv. |
imdbId |
string |
Optional | IMDB ID such as tt0903747. Helps resolution when a title is ambiguous or missing from TMDB lookups. |
source |
string |
Optional | Force a specific source instead of automatic selection. Free plans are restricted to source-1; Plus plans may request any source or leave it unset to use automatic fallback. |
season and episode are ignored for movies and required for TV; omitting them on a TV request returns 400.S01E01 strings and no zero-padding.id even when using imdbId; the TMDB ID remains the primary lookup key.URLSearchParams as shown in the examples.source is effectively fixed to source-1. Passing another
source, or relying on multi-source fallback, is a Plus capability — see
Plans & Limits.
Successful requests return 200 OK with a JSON object. The url field is
the one you actually play; everything else is metadata you can use for UI.
{
"url": "https://…stream-url-returned-by-the-api…",
"source": "source-1",
"format": "hls",
"captions": [],
"sources": [
{
"name": "Source 1",
"format": "hls",
"servers": [
{ "name": "Server 1", "url": "https://…stream-url-returned-by-the-api…" }
]
}
]
}
| Field | Type | Description |
|---|---|---|
url |
string |
The playable stream URL. It is proxied and manifest-ready, so it can be handed straight to an HLS-capable player. Use it exactly as returned — do not rewrite the host or path. |
source |
string |
Identifier of the source that served this result, for example source-1. Useful for logging and for telling users which source produced the stream. |
format |
string |
Stream container. Almost always hls; treat unexpected values generically rather than assuming HLS. |
captions |
array |
Subtitle tracks, when the source provides them. May be empty. Check the length before offering a captions menu. |
sources |
array |
Every source/stream variant available for this request. Each entry has a name, a format, and a servers array of { name, url } pairs. On free plans this contains only your allowed source. |
sources lists more than one server, you can offer a quality or server picker in
your UI and swap the player's source between the returned url values without calling
the API again. Availability of extra servers depends on your plan and on the source itself.
Failures return a non-2xx status with a JSON body containing an error string:
{
"error": "No stream available for this title"
}
GET https://faucetapi.twnty.top/stream?id=550&type=movie
const res = await fetch(
"https://faucetapi.twnty.top/stream?id=550&type=movie",
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
const data = await res.json();
console.log(data.url); // playable stream URL
console.log(data.source); // which source served it
GET https://faucetapi.twnty.top/stream?id=1396&type=tv&season=1&episode=1
const params = new URLSearchParams({
id: "1396",
type: "tv",
season: "1",
episode: "1"
});
const res = await fetch(
`https://faucetapi.twnty.top/stream?${params}`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
const { url } = await res.json();
Add imdbId when you already have it. It improves resolution for titles with remakes,
alternate cuts, or awkward TMDB matches.
GET https://faucetapi.twnty.top/stream?id=1396&type=tv&season=1&episode=1&imdbId=tt0903747
const API_BASE = "https://faucetapi.twnty.top/stream";
const API_KEY = process.env.FAUCET_API_KEY; // never hardcode in client code
async function getStream({ id, type, season, episode, imdbId, source }) {
const params = new URLSearchParams({ id, type });
if (type === "tv") {
if (!season || !episode) throw new Error("season and episode are required for TV");
params.set("season", season);
params.set("episode", episode);
}
if (imdbId) params.set("imdbId", imdbId);
if (source) params.set("source", source);
const res = await fetch(`${API_BASE}?${params}`, {
headers: { Authorization: `Bearer ${API_KEY}` }
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || `Faucet API error ${res.status}`);
}
return res.json();
}
The returned url is an HLS manifest served through the proxy, so it plays without
extra CORS setup. Feed it to any HLS-compatible player.
hls.js is the common choice.video.src.format is something other than hls, assign it to video.src directly.<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<video id="player" controls></video>
<script>
const API_KEY = "faucet_live_YOUR_KEY";
const video = document.getElementById("player");
async function play(id, type, season, episode) {
const params = new URLSearchParams({ id, type });
if (type === "tv") {
params.set("season", season);
params.set("episode", episode);
}
const res = await fetch(
`https://faucetapi.twnty.top/stream?${params}`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
const { url, format } = await res.json();
if (format === "hls" && window.Hls && Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(url);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
hls.on(Hls.Events.ERROR, (e, data) => {
if (data.fatal) console.warn("Playback failed:", data.type);
});
} else {
video.src = url; // Safari native HLS, or a non-HLS format
video.play().catch(() => {});
}
}
play("550", "movie");
</script>
import { useEffect, useRef } from "react";
import Hls from "hls.js";
export default function Player({ url, format }) {
const ref = useRef(null);
useEffect(() => {
const video = ref.current;
if (!video || !url) return;
if (format === "hls" && Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(url);
hls.attachMedia(video);
return () => hls.destroy();
}
video.src = url;
return undefined;
}, [url, format]);
return <video ref={ref} controls playsInline style={{ width: "100%" }} />;
}
const video = document.querySelector("video");
video.src = streamUrl;
video.play().catch(() => {});
const player = videojs("my-video");
player.src({
src: streamUrl,
type: "application/x-mpegURL"
});
Faucet has two tiers. The free tier is fully functional for a single-domain project; Plus removes the ceilings and adds multi-source fallback. Everything below applies to the same endpoint — the plan only changes what the API will do for you.
| Feature | Faucet One (free) | Faucet Plus |
|---|---|---|
| Sources | Source 1 only | All available sources |
| Multi-source fallback | — | Automatic |
| Rate limit | 10 requests / second | 40 requests / second |
| API keys | 1 | 3 |
| Authorized domains | 1 | Unlimited |
| New sources | — | Early access |
| Support | Discord | Priority (Discord) |
| Price | Free, forever | $3.99 / month or $20 one-time |
Limits are enforced per account, so no single integration can starve the service,
and they are tier-based: Faucet One allows 10 requests per second and
Faucet Plus allows 40. Enforcement uses a token bucket rather than a hard
reset-every-second counter, so a short burst just above your rate is allowed and then your traffic is
smoothed back down. Exceeding it returns 429 Too Many Requests.
429 until a token refills — usually within a fraction of a second.Successful responses include your current position in the bucket:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 8
A 429 response adds a Retry-After header (whole seconds) and a JSON body
describing the limit that applied:
{
"error": "Too Many Requests",
"message": "Rate limit exceeded for your plan (10 requests/second). Retry in 120ms.",
"tier": "free",
"limit_per_second": 10,
"retry_after_ms": 120
}
| Plan | Sustained | Practical guidance |
|---|---|---|
| Faucet One | 10 requests / second | Cache each resolved URL; do not re-resolve on every play or seek. |
| Faucet Plus | 40 requests / second | Room for prefetching the next episode or warming several titles at once. |
429, back off and retry rather than
hammering the endpoint. Your live usage — requests per second, the last minute, the last 24 hours, and
the last 30 days — is shown in your API dashboard.
The API uses standard HTTP status codes and always returns JSON of the shape
{ "error": "…" }. Match on the status code for control flow and use the message for
logs, not for user-facing copy.
| Status | Meaning | Likely cause |
|---|---|---|
200 |
OK | Stream resolved; read url from the body. |
400 |
Bad Request | Missing or invalid parameters — no id, an unknown type, or a TV request without season/episode. |
401 |
Unauthorized | Missing, malformed, revoked, or unknown API key. |
403 |
Forbidden | Request came from a domain that is not authorized on your account. |
404 |
Not Found | No stream available for this title, or the source failed to resolve it. Also common while Source 1 is degraded. |
429 |
Too Many Requests | Rate limit exceeded for your tier (10/s free, 40/s Plus) — read Retry-After, then back off and retry. |
500 |
Server Error | Unexpected internal failure. Safe to retry once or twice. |
502 |
Bad Gateway | A source or the proxy was temporarily unreachable — retry later. |
429, wait for the Retry-After header (or the retry_after_ms field) before retrying; add jitter so parallel clients do not sync up.500 and 502 with exponential backoff and a little jitter; cap at two or three attempts.400, 401, or 403 unchanged — fix the request, the key, or the domain first.404 as "not available right now" rather than "never available", especially during a Source 1 outage.A complete page that resolves a movie and plays it.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
</head>
<body>
<video id="video" controls style="width:100%;max-width:820px"></video>
<script>
const API_KEY = "faucet_live_YOUR_KEY";
const API_BASE = "https://faucetapi.twnty.top/stream";
(async () => {
const params = new URLSearchParams({ id: "550", type: "movie" });
const res = await fetch(`${API_BASE}?${params}`, {
headers: { Authorization: `Bearer ${API_KEY}` }
});
if (!res.ok) {
document.body.textContent = "Stream unavailable — try again.";
return;
}
const { url } = await res.json();
const video = document.getElementById("video");
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(url);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
} else {
video.src = url;
video.play().catch(() => {});
}
})();
</script>
</body>
</html>
Survives a flaky source, a transient 502, or a brief rate limit — the practical fix
for the current Source 1 situation.
const API_BASE = "https://faucetapi.twnty.top/stream";
const API_KEY = process.env.FAUCET_API_KEY;
async function getStream(query, attempt = 0) {
const params = new URLSearchParams(query);
const res = await fetch(`${API_BASE}?${params}`, {
headers: { Authorization: `Bearer ${API_KEY}`, Accept: "application/json" }
});
if (res.ok) return res.json();
const body = await res.json().catch(() => ({}));
// 429 is tier-based rate limiting — honour Retry-After rather than blind backoff.
if (res.status === 429 && attempt < 3) {
const bodyMs = Number(body.retry_after_ms);
const headerMs = Number(res.headers.get("Retry-After")) * 1000;
const wait = Math.min(5000,
(Number.isFinite(bodyMs) ? bodyMs : 0) ||
(Number.isFinite(headerMs) ? headerMs : 0) ||
(400 * 2 ** attempt + Math.random() * 250));
await new Promise((r) => setTimeout(r, wait));
return getStream(query, attempt + 1);
}
if ([500, 502, 503, 504].includes(res.status) && attempt < 3) {
const delay = 400 * 2 ** attempt + Math.random() * 250;
await new Promise((r) => setTimeout(r, delay));
return getStream(query, attempt + 1);
}
throw new Error(body.error || body.message || `Faucet API error ${res.status}`);
}
// Usage
const { url, source, captions } = await getStream({
id: "1396", type: "tv", season: "1", episode: "1"
});
Every available stream is already in the response, so a picker costs no extra requests. On free plans the list contains only your permitted source; on Plus it can include several, each with its own servers.
const data = await getStream({ id: "550", type: "movie" });
// The default stream
console.log("Playing:", data.url, "from", data.source);
// Alternates, when your plan and the source provide them
for (const src of data.sources || []) {
for (const server of src.servers || []) {
console.log(src.name, "→", server.name, server.url);
}
}
Yes. Every request requires a Bearer token. If you do not have one yet, generate it from your API dashboard — it is free.
The request came from a domain that is not authorized on your account. Add the exact hostname in your API dashboard. Localhost is permitted automatically for development.
The numeric ID is in the TMDB URL for any title, for example
themoviedb.org/movie/550 → 550. For TV, use the series ID, not an episode
ID, and pass the season and episode separately.
In practice you need an Origin header that matches an authorized domain, so
browser-based calls are the straightforward path. Server-side calls work when they present a
matching origin.
For the playback session. Resolve once, cache it, and reuse it instead of requesting a fresh URL for every play, seek, or reload.
That is a source-side failure mid-stream, which is expected while Source 1 is degraded. Your integration should surface an error state and let the user retry; a fresh request will often succeed.
Yes — that is Faucet Plus. It adds every available source, automatic fallback, 40 requests per second, three keys, and unlimited authorized domains. See Plans & Limits.
You went over your per-second limit — 10 requests per second on free, 40 on Plus.
The bucket allows a short burst (up to 2× the rate) and then is smoothed. Read
Retry-After on the 429 response or the retry_after_ms field in
the JSON body, wait, and retry with jitter. Your tier, the limit, and your live usage are all shown in
your API dashboard. Long-term, cache the returned
stream URL within a session instead of re-resolving the same title.
No, and that is deliberate. This documentation covers the public HTTP surface completely — request, response, authentication, errors, and limits — so you can build against the API without depending on how it works internally.