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

X (Twitter) video downloader API for MP4s, GIFs and images

The X (Twitter) video downloader API turns a public x.com or twitter.com post link into a playable MP4 with one GET request. You get a direct download URL, dimensions, duration and the post text back as JSON — no developer account, no OAuth, no app review. The same /fetch call also resolves five other platforms.

Last updated

Endpoint
GET /fetch
Credits
1.5 per post
Auth
X-Api-Key header
Returns
direct MP4 URL + metadata

coverage

what this endpoint resolves

X carries two kinds of moving image and treats them identically. A native video comes back as the uploaded MP4. So does a GIF: X stopped storing real GIFs years ago, and what loops in the timeline is a silent video.

  • Native video — the uploaded file, not a re-encode.
  • GIFs — a couple of seconds of MP4, no audio stream.
  • Still images — the full-size attachment, not the thumbnail.
  • Quoted and threaded posts — the link you pass decides whose media you get.

A quote-post, the post it quotes and every reply in a thread are separate URLs with separate status IDs. Pass the outer link and you get the outer post's attachment — nothing at all, if the quoter added none. Copy the permalink of the post you actually want.

input

the link formats you can pass

Pass the URL exactly as you copied it — what matters is the status ID.

  • https://x.com/<handle>/status/<id> — the canonical form.
  • https://twitter.com/<handle>/status/<id> — still all over old databases.
  • https://x.com/i/web/status/<id> — the handle-less permalink.
  • Share links with ?t= and ?s=20 tracking parameters attached.

Since share links carry query strings, URL-encode the url parameter in code. An unencoded &s=20 becomes a parameter of your request instead of part of the link, and the API sees a truncated URL.

Normalise before you store anything. Rewrite the host to x.com, drop the query string, and pull the digits after /status/ — that number is your cache key. The id in the response is not: it is a verbatim echo of the URL you sent, so the same post arriving in four spellings becomes four cache entries and four paid calls.

usage

sending a status link

The same shape as every other platform on /fetch. No POST body, no SDK, no session to keep warm.

GET https://api.fastsaver.io/v1/fetch 1.5 credits
request
curl -G "https://api.fastsaver.io/v1/fetch" \
  --data-urlencode "url=https://x.com/SpaceX/status/1749286413298475123" \
  -H "X-Api-Key: fs_sk_•••••••••••"
200 OK · response
{
  "ok": true,
  "id": "https://x.com/SpaceX/status/1749286413298475123",
  "source": "x.com",
  "type": "video",
  "download_url": "https://video.twimg.com/ext_tw_video/...",
  "thumbnail_url": "https://pbs.twimg.com/ext_tw_video_thumb/...",
  "width": 1280,
  "height": 720,
  "duration": 34,
  "caption": "Static fire test complete."
}

Three fields shape your handler. type says whether you hold video or a still. duration is the cheapest tell that a "video" is really a GIF. source is an opaque platform label — do not compare it to a hardcoded hostname. One field shapes nothing: id is the URL you sent, echoed back, so it is an identifier in name only. Still images arrive without width, height or duration at all — read those with a default rather than a bracket lookup. Failures arrive as ok: false with a detail string, so branch on ok, not the HTTP status.

gotcha

gifs, and why yours is silent

If you are building a reposting bot or an archive, this is the detail that bites. A Twitter GIF has no audio track, and pipelines that assume one exists — ffmpeg concat, some Telegram and Discord upload paths — fail or emit a broken file. Branch on it. In Telegram, send the silent MP4 as an animation and it plays inline, muted and looping, like the original.

The API will not produce a real .gif for you:

mp4 → gif, locally
ffmpeg -i clip.mp4 \
  -vf "fps=15,scale=480:-1:flags=lanczos,split[a][b];[a]palettegen[p];[b][p]paletteuse" \
  -loop 0 out.gif

Expect the result to be several times larger than the MP4. That is the format, not your settings.

code

resolving and saving

Two steps, separate on purpose: the API call authenticates with your key, the download does not. Never attach X-Api-Key to the CDN request — that host is not ours.

python · httpx
import re
import httpx

KEY = "fs_sk_•••••••••••"
api = httpx.Client(headers={"X-Api-Key": KEY}, timeout=60)

STATUS_ID = re.compile(r"/status/(\d+)")

def cache_key(post_url: str) -> str:
    # the response's "id" is just the URL you sent - parse the real one
    m = STATUS_ID.search(post_url)
    if not m:
        raise ValueError(f"no status id in {post_url}")
    return m.group(1)

def resolve(post_url: str) -> dict:
    r = api.get("https://api.fastsaver.io/v1/fetch", params={"url": post_url})
    if r.status_code == 429:
        raise RuntimeError("rate limited - back off, then retry")
    r.raise_for_status()
    data = r.json()
    if not data.get("ok"):
        raise RuntimeError(data.get("detail", "could not resolve post"))
    return data

def save(post_url: str, path: str) -> dict:
    media = resolve(post_url)
    # plain client: no API key header on the CDN request
    with httpx.stream("GET", media["download_url"], follow_redirects=True) as src:
        with open(path, "wb") as out:
            for chunk in src.iter_bytes():
                out.write(chunk)
    return media

info = save("https://x.com/SpaceX/status/1749286413298475123", "clip.mp4")
print(cache_key(info["id"]), info["type"], info.get("width"), info.get("duration"))

From a shell:

shell + jq
curl -sG "https://api.fastsaver.io/v1/fetch" \
  --data-urlencode "url=$1" \
  -H "X-Api-Key: $FASTSAVER_KEY" \
| jq -r 'select(.ok) | .download_url' \
| xargs -r curl -sL -o clip.mp4

trade-offs

how this compares to the official X API

Not competing products. X sells access to the platform; this sells one narrow capability — public post link in, playable file out.

what you skip

  • No developer account application, no use-case description, no approval wait.
  • No OAuth flow, no app registration, no token refresh, no request signing.
  • No monthly tier you pay for in a month you barely called it — 1.5 credits per post.

what you do not get

  • Posting, replying, liking, following. Nothing that writes.
  • Timelines, follower graphs, search, filtered streams, engagement metrics.
  • Anything behind an authenticated session, including your own protected content.

Building an analytics dashboard or a scheduler? Use X's own API and do the OAuth work. Need the video out of a link a user pasted into your app? One GET here — and GET /balance reports your credits and per-minute limit for free.

honesty

the X limits to design around

  • Public posts only. Protected accounts, sensitive-media interstitials and deleted posts return an error, not a file.
  • Download URLs expire. The link points at X's short-lived CDN — fetch the bytes in the same job and cache the status ID you parsed, never the CDN URL and never the response's id.
  • GIFs carry no audio. Handle zero-audio-stream video explicitly when you transcode or upload.
  • Rate limits are per plan — 10 requests per minute on Free, up to 900 on Mega. A 429 means slow down; the link is fine.
  • The platform keeps moving. X changes hosts and playback formats on its own schedule; we fix that behind the same endpoint.

What you download is someone else's work. Copyright, X's terms and the rights of whoever posted it are yours to respect.

faq

questions about the X (Twitter) API

Do x.com and twitter.com links both work?

Yes. Old twitter.com bookmarks, x.com shares and /i/web/status/… permalinks all resolve to the same media. One caveat if you are building a cache: the response's id is the URL you sent, echoed back verbatim — not the status ID. Three spellings of one post therefore give you three different id values. Parse the digits after /status/ yourself and key on those.

Do I need an X developer account, an app or an OAuth token?

No. No application form, no app review, no token exchange — you send your X-Api-Key header and the post URL over HTTPS. What you get back is media resolution only: this endpoint cannot post, read a timeline or search.

Why does a Twitter GIF download as an MP4?

Because it was never a GIF. X transcodes uploads to a short, silent MP4 and loops it in the client; the API hands you that file as stored. If you genuinely need a .gif, transcode it with ffmpeg after the download.

Can it download video from protected or age-restricted posts?

No. Anything needing a logged-in session — protected accounts, sensitive-media interstitials, deleted posts — returns an error instead of a file. The endpoint resolves only what an anonymous visitor could already see.

Can I pick a resolution, like 720p or 480p?

Not here. GET /fetch takes one parameter, url, and returns the file the platform serves — with width and height on video responses so you know what you got. Format selection exists only on the YouTube endpoints.

Resolve a post link right now

Paste an x.com URL into the playground and read the JSON before committing to anything.