FastSaverAPI moved. The API and dashboard now live at api.fastsaver.iowhat changed.

Facebook video downloader API for reels, Watch posts and share links

The Facebook video downloader API resolves a public video or reel to a direct fbcdn.net URL with one GET request. Every path shape on the platform's own hosts — /watch/?v=, /reel/, /share/v/, old permalink.php URLs — goes into the same parameter. The one link you must expand yourself is an fb.watch short link. Facebook is also the platform that rearranges its markup most often, and keeping up with that is the part you are handing over.

Last updated

Endpoint
GET /fetch
Credits
1.5 per video or reel
Auth
X-Api-Key header
Accepts
facebook.com & fb.com hosts · /watch/ · /reel/ · /share/v/ · permalink.php

usage

one GET, JSON back

No SDK, no cookie jar, no headless Chrome sitting in your container image. A query parameter and a header.

GET https://api.fastsaver.io/v1/fetch 1.5 credits
request
curl -G "https://api.fastsaver.io/v1/fetch" \
  --data-urlencode "url=https://www.facebook.com/reel/1234567890" \
  -H "X-Api-Key: fs_sk_•••••••••••"
200 OK · response
{
  "ok": true,
  "id": "https://www.facebook.com/reel/1234567890",
  "source": "facebook.com",
  "type": "video",
  "download_url": "https://video-waw2-1.xx.fbcdn.net/o1/v/...",
  "thumbnail_url": "https://scontent-waw2-1.xx.fbcdn.net/v/...",
  "width": null,
  "height": null,
  "duration": 34,
  "caption": "Sunday league, last-minute equaliser."
}

The -G plus --data-urlencode pairing is the least error-prone way to send a link that carries its own ? and &.

reference

the fields Facebook actually fills in

It is the same envelope every /fetch platform returns, which is the point when one worker handles links from several sources: ok, source, thumbnail_url, duration and caption behave exactly as they do in the full field reference on the Instagram page. Four other fields behave differently enough here to be worth spelling out.

FieldTypeNotes
idstringNot an ID: the video branch echoes back the URL you sent, character for character. Two link shapes for the same clip give two different id values, so it is no use as a cache key.
typestringvideo for videos and reels alike — there is no separate reel type to branch on.
download_urlstringSigned fbcdn.net URL. Time-limited — treat it as a one-shot ticket.
width / heightnullAlways null on a Facebook video. The keys are in the payload; the values never are.

That last row is the one that catches people. Reels are vertical and Watch uploads are usually landscape, but the response will not tell you which — retrying does not turn those nulls into pixels. If your UI needs a width, probe the file once you have the bytes (ffprobe, or the browser's own metadata event) rather than reserving space from the JSON.

code

resolving and saving in one pass

Because the CDN URL expires, the useful unit of work is resolve-then-store, not resolve-then- queue. Streaming straight to disk keeps memory flat even on a long video. The three-line expand helper is the fb.watch fix from above, and it is worth having in the path permanently — phone-copied links arrive in that shape constantly.

python
import shutil
import requests

API = "https://api.fastsaver.io/v1/fetch"
HEAD = {"X-Api-Key": "fs_sk_•••••••••••"}

def expand(link: str) -> str:
    # fb.watch is not a routed host — follow it to facebook.com before sending
    if "fb.watch/" in link:
        return requests.head(link, allow_redirects=True, timeout=30).url
    return link

def save(link: str, path: str) -> dict:
    meta = requests.get(API, params={"url": expand(link)}, headers=HEAD, timeout=60).json()
    if not meta.get("ok"):
        raise RuntimeError(meta.get("detail", "could not resolve"))

    # signed and short-lived: pull the bytes now, not in a later job
    with requests.get(meta["download_url"], stream=True, timeout=300) as r:
        r.raise_for_status()
        with open(path, "wb") as f:
            shutil.copyfileobj(r.raw, f)
    return meta

info = save("https://fb.watch/AbCdEf1234/", "clip.mp4")
print(info["source"], info["duration"], "seconds")

In Node, build the query with URL rather than string concatenation and the encoding problem disappears.

node.js
const endpoint = new URL('https://api.fastsaver.io/v1/fetch');
endpoint.searchParams.set('url', link);   // already expanded; encodes ? and & for you

const res = await fetch(endpoint, {
  headers: { 'X-Api-Key': process.env.FASTSAVER_KEY },
});

if (res.status === 429) {
  // plan rate limit reached — back off, do not retry immediately
  throw new Error('rate limited');
}

const data = await res.json();
if (!data.ok) throw new Error(data.detail ?? 'could not resolve');

// data.width and data.height are null on Facebook — do not lay out from them
// data.id is the URL you sent echoed back, not a video ID
console.log(data.duration, data.download_url);

boundaries

public means public

The API sees what a logged-out visitor sees. That boundary is deliberate and not configurable. These never resolve:

  • Posts limited to friends, or to a custom audience.
  • Anything inside a private or closed group, including videos a member reshared there.
  • Videos on pages that are unpublished, geo-blocked, or restricted to an age-verified audience.
  • Content that has been deleted — the ID survives in links long after the file does not.

Each returns ok: false with a reason, not a half-written file. Build the error path around that and most Facebook edge cases are already handled.

honesty

where Facebook links break

  • Facebook changes its markup more often than anything else we support. That churn is absorbed on our side and your call signature never moves, though a specific link shape can lag for a few hours after a change lands.
  • CDN URLs expire. Signed fbcdn.net links die on their own schedule. Store the file, or store the post URL and re-resolve — never the download URL.
  • A live broadcast has no finished file. Wait for the replay to be published before pointing anything at it.
  • Throughput is capped per plan. Ten requests a minute on the free tier, 900 on the largest. A bulk job needs a queue with backoff, not a tight loop.
  • Credits pay for the work, not the outcome. A resolved link costs 1.5 credits whether or not you keep the file, so deduplicate before you call. Key that cache on a URL you normalised yourself — expanded, lowercased host, tracking parameters stripped — because the id in the response is just your own URL handed back and will not collapse two shapes of the same clip into one entry.

What you do with the media is on you. A public URL is not a licence: the uploader holds copyright and Facebook's terms still apply to redistribution.

faq

questions about the Facebook API

Can the API download private or friends-only Facebook videos?

No. Only content a signed-out visitor can open is reachable — friends-only posts, private-group videos and unpublished pages all return an error. Quickest test: open the link in a private browser window. If it plays without a login prompt, the API can resolve it.

Do I need to expand an fb.watch link before sending it?

Yes, and this one bites people. Requests are routed by host, and only facebook.com (with its subdomains) and fb.com are matched. fb.watch is neither, so it never reaches the Facebook resolver — you get 400 with {"ok": false, "detail": "Invalid URL"}. Follow the short link yourself first: a HEAD request with redirects enabled lands on the canonical facebook.com URL, and that is what you send. /share/v/ links need no such step — they are already on facebook.com.

How much does one Facebook video cost?

1.5 credits per resolved link, videos and reels alike. The free tier ships with 1,000 credits, which is a little over 660 videos before any payment is involved. Checking your remaining balance costs nothing.

Why did a Facebook link that worked last week stop working?

Usually the post changed — deleted, made friends-only, or moved into a group. If it is still public and still failing, Facebook has probably shipped a markup change. Those are patched centrally; nothing in your integration changes.

Why are width and height null on every Facebook response?

Because Facebook does not expose them to us. The two keys are always in the payload and always null on a video, so treat them as unavailable rather than as something a retry will fix. If you need the dimensions, read them from the file after you have downloaded it.

Does the endpoint return the video file or a link to it?

A link. The response is JSON with a download_url pointing at Facebook's own CDN, plus metadata. Nothing is proxied through us, so transfers run at CDN speed — but the URL is signed and expires, so fetch the bytes in the same job.

Point it at a real Facebook link

The playground runs the call in the browser and shows you the raw JSON before you commit to any code.