Faucet logo

Faucet API Docs

Everything you need to resolve movies and TV episodes into playable streams — authentication, the /stream endpoint, response shapes, player integrations, and plan limits.

Current Issues

Source 1 — degraded (known issue)
Source 1 is currently facing issues with its sources. Titles that resolve through Source 1 may intermittently fail, time out, or return a stream that stops partway through playback. This is an upstream problem on the source side, not a problem with your integration or your API key.
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.

What this means for you

How to handle it in your app

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.

Quick Start

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.

  1. Get an API key. Open your API dashboard, create an account, and generate a key. It looks like faucet_live_… and is shown once.
  2. Authorize your domain. Add the domain your site runs on in your API dashboard. Requests only work from authorized origins; localhost is allowed automatically for development.
  3. Call the endpoint. Send a 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"
No key yet?
If you do not have an API key, you can get one from your API dashboard. Signing up and generating a key takes less than a minute, and the free tier needs no payment details.
Where to send requests from
Calls must come from a browser page served on an authorized domain (or localhost during development). Server-side and curl calls only work when the request carries an Origin header that matches an authorized domain.

Authentication

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
Your key is secret and shown once
A key is displayed in full exactly once, at the moment it is created in your API dashboard. Store it in an environment variable or your platform's secret store. Never commit it, and never ship it to a browser bundle that untrusted users can read.

Key rules

Domain authorization (origin gating)

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.

Rotating a key safely
Generate the replacement key first, deploy it, then confirm traffic has moved over before discarding the old value. Because generation revokes the old key instantly, there is no overlap window — plan for a short gap or time the swap with low traffic.

Base URL

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.

Endpoint — Get Stream

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

Query parameters

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.

Parameter notes

Free tier behaviour
On Faucet One, source is effectively fixed to source-1. Passing another source, or relying on multi-source fallback, is a Plus capability — see Plans & Limits.

Response Format

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.
Multi-server playback
When 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.

Errors

Failures return a non-2xx status with a JSON body containing an error string:

{
  "error": "No stream available for this title"
}

Examples

Movie — Fight Club (TMDB ID 550)

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

TV episode — Breaking Bad S01E01 (TMDB ID 1396)

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();

Passing an IMDB ID as a hint

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

A reusable request helper

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();
}
Keep the key off the client
The helper above reads the key from the environment because it targets a server or edge runtime. If you call the API directly from browser JavaScript, the key is visible to anyone who opens devtools — pair it with domain authorization so a copied key is useless elsewhere, and remember that origin gating is your real protection.

Playing Streams

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 (recommended for browsers)

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

React component

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%" }} />;
}

Vanilla video element (Safari / iOS)

const video = document.querySelector("video");
video.src = streamUrl;
video.play().catch(() => {});

Video.js

const player = videojs("my-video");
player.src({
  src: streamUrl,
  type: "application/x-mpegURL"
});

Plans & Limits

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

Building on the free tier

Upgrading
Plan changes are handled through the Discord to keep billing simple. Once Plus is active, the same key immediately gains access to additional sources, a higher rate limit, and extra key and domain slots. See your API dashboard for the current status of your account and its expiry date.

Rate Limits & Caching

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.

How enforcement works

Reading your rate status

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.
Cache aggressively
A resolved URL stays good for the session, so store it keyed by content plus season/episode. Re-requesting the same title for every playback action is the most common way to run into a rate limit — and it is unnecessary. If you receive a 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.

Errors

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.

Retry strategy

Full Examples

Minimal HTML player

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>

Resilient fetch with retry and backoff

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"
});

Source and server picker

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);
  }
}

FAQ

Do I need an API key?

Yes. Every request requires a Bearer token. If you do not have one yet, generate it from your API dashboard — it is free.

Why does my request fail with 403?

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.

Where do I find TMDB IDs?

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.

Can I call the API from a server?

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.

How long is a stream URL valid?

For the playback session. Resolve once, cache it, and reuse it instead of requesting a fresh URL for every play, seek, or reload.

Playback stops halfway through — what now?

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.

Can I get more sources or a higher rate limit?

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.

Why am I getting 429 Too Many Requests?

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.

Is the underlying resolution logic documented?

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.