How to Search Twitter (X) Videos via API

By Alex Chen•4 min read
TL;DR
  • filter:videos OR has:media — filter for video-attached tweets
  • Combine with min_faves:N / since: / from:@handle for tight scope
  • Cost: $0.00015/tweet (twitterapi.io) vs $0.005 (X official) — about 33× cheaper per read
  • Video-attached tweets are ~1-5% of raw keyword volume = naturally cheap

Searching Twitter (X) for videos programmatically is one of the highest-signal workflows in social monitoring — video content typically has higher engagement, longer dwell, and stronger emotional payload than text-only tweets. Content moderators, OSINT analysts, brand-monitor teams, and journalists all need this endpoint pattern.

This guide walks the exact operator combos + endpoint call + cost math with runnable Python. Contrast with the twitter.com UI-based search which caps at manual page-by-page browsing — the API path unlocks bulk pulls, historical windows, and pipeline-ready JSON output.

Video-search cost efficiency

Video-attached tweets are typically 1-5% of raw keyword volume — filter:videos naturally cuts 95%+ cost vs unfiltered pull per twitterapi.io/pricing

01 — Section

The two operators — filter:videos vs has:media

filter:videos — narrower, matches only tweets with attached video content (uploaded native video, GIF-as-video). Best for pure video workflows.

has:media — broader, matches tweets with images OR videos OR GIFs. Use when you want ANY media attachment.

filter:native_video — narrowest, only X-native uploaded video (excludes linked YouTube/Vimeo URLs that render as video cards).

Combining: <keyword> filter:videos lang:en min_faves:50 -filter:retweets — high-signal English original video tweets on the topic.

Practical rule: start with filter:videos for video-only workflows; drop to has:media if you want combined image+video signal.

02 — Section

Runnable — video search with filters

One end-to-end example showing the operator combo pattern:

python
import os, requests

HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
BASE = "https://api.twitterapi.io"

def search_videos(topic: str, min_faves: int = 20, max_pages: int = 10) -> list:
    query = f"{topic} filter:videos lang:en min_faves:{min_faves} -filter:retweets"
    tweets, cursor = [], None
    for _ in range(max_pages):
        params = {"query": query}
        if cursor: params["cursor"] = cursor
        r = requests.get(f"{BASE}/twitter/tweet/advanced_search", headers=HEADERS, params=params, timeout=15)
        r.raise_for_status()
        resp = r.json()
        tweets.extend(resp.get("tweets", []))
        cursor = resp.get("next_cursor")
        if not cursor: break
    return tweets

# High-signal AI news videos with 50+ engagement
vids = search_videos("OpenAI OR Anthropic OR GPT", min_faves=50)
print(f"{len(vids):,} video tweets found")

# Access video URLs from extendedEntities.media (highest-bitrate MP4 variant)
for t in vids[:5]:
    media = (t.get("extendedEntities") or {}).get("media", [])
    for m in media:
        if m.get("type") in ("video", "animated_gif"):
            mp4s = [v for v in (m.get("video_info") or {}).get("variants", []) if v.get("content_type") == "video/mp4"]
            best = max(mp4s, key=lambda v: v.get("bitrate", 0), default={})
            print(f"  @{t.get('author', {}).get('userName')}: {best.get('url', 'no-url')}")

# Cost per twitterapi.io/pricing: len(vids) × $0.00015
03 — Section

4 real-world video-search combos

Use caseQuery
Crypto news videos (high-engagement)bitcoin filter:videos lang:en min_faves:100 -filter:retweets
Live event coverage (recent + video)"climate summit" filter:videos since:2024-11-01 -filter:retweets
Competitor product videos (brand watch)stripe filter:videos -paypal -square lang:en min_faves:20
Celebrity announcements (native only)from:@target_handle filter:native_video since:2024-01-01

Each pulls a signal-only stream at 1-5% of raw-keyword volume + high engagement density = cost-efficient by design.

04 — Section

What the response includes for video tweets

For each video tweet, the API returns:

Media object array (at extendedEntities.media): each media item has type (video / animated_gif / photo), media_url_https (image URL), and for videos and GIFs video_info.variants (multiple bitrate versions, each with content_type and url).

Author metadata: userName, followers, isBlueVerified — for filtering by author quality.

Engagement: likeCount, retweetCount, replyCount, quoteCount — standard tweet-level engagement fields.

Text + createdAt: the tweet text (often a caption/context for the video) + timestamp.

conversationId: link to the reply thread for context.

05 — Section

Bulk pattern — download video URLs for a large window

For workflows that need many video URLs across a date range (research corpus building, content moderation batch processing, competitive intel archives), use windowed pagination + concurrent workers within safe rate limits.

python
# 30-day bulk video pull for a topic — window-sliced pattern.
import os, requests, json
from datetime import date, timedelta
from pathlib import Path

HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
BASE = "https://api.twitterapi.io"
OUT = Path("video_corpus.jsonl")

def pull_day(topic: str, d: date) -> int:
    query = f"{topic} filter:videos lang:en min_faves:10 -filter:retweets since:{d} until:{d + timedelta(days=1)}"
    n, cursor = 0, None
    with open(OUT, "a") as f:
        for _ in range(50):
            params = {"query": query}
            if cursor: params["cursor"] = cursor
            r = requests.get(f"{BASE}/twitter/tweet/advanced_search", headers=HEADERS, params=params, timeout=15)
            r.raise_for_status()
            resp = r.json()
            for t in resp.get("tweets", []):
                f.write(json.dumps(t) + "\n"); n += 1
            cursor = resp.get("next_cursor")
            if not cursor: break
    return n

TOPIC = "climate change"
END = date.today()
START = END - timedelta(days=30)

d = START
total = 0
while d < END:
    n = pull_day(TOPIC, d)
    print(f"  {d}: {n:,} video tweets")
    total += n
    d += timedelta(days=1)

print(f"\n30d total: {total:,} video tweets")
print(f"Cost per twitterapi.io/pricing: {total} × $0.00015 = ${total * 0.00015:.2f}")
06 — Section

twitterapi.io vs X official — same grammar, different price

Dimensiontwitterapi.ioX official /2/tweets/search/recent
Endpoint/twitter/tweet/advanced_search/2/tweets/search/recent
Video operatorfilter:videos / has:mediasame grammar
Per-tweet cost$0.00015 (twitterapi.io/pricing)$0.005 (docs.x.com)
Historicalback to 2006 with since:about 7 days on recent search
Video URL in response✓ MP4 URLs in extendedEntities.media[].video_info.variants✓ (requires media.fields=video_info expansion)
Best forany video-search workflowX-native OAuth-integrated apps
07 — Section

Common video-search workflows

Content moderation batch: pull videos matching harmful-content signal keywords, score each via computer-vision pipeline, queue for human review.

Brand crisis monitoring: watch for video tweets mentioning your brand + negative-sentiment keyword combos — video crises spread 2-3× faster than text.

OSINT event coverage: pull all videos from a specific date+location window for incident reconstruction (journalism, researcher, investigator use cases).

Competitor product demos: track videos where competitors demo their product — feature launches, price announcements, integration reveals.

Creator sourcing / UGC pull: pull high-engagement videos in your niche → build a curated list of creators to reach out to for partnerships.

python
# Video sentiment corpus pull for downstream ML pipeline.
import os, requests, csv, json
from pathlib import Path

HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
BASE = "https://api.twitterapi.io"

BRAND = "anthropic"
query = f"{BRAND} filter:videos lang:en min_faves:20 -filter:retweets"

tweets, cursor = [], None
for _ in range(20):
    params = {"query": query}
    if cursor: params["cursor"] = cursor
    r = requests.get(f"{BASE}/twitter/tweet/advanced_search", headers=HEADERS, params=params, timeout=15)
    r.raise_for_status()
    resp = r.json()
    tweets.extend(resp.get("tweets", []))
    cursor = resp.get("next_cursor")
    if not cursor: break

rows = []
for t in tweets:
    media = (t.get("extendedEntities") or {}).get("media", [])
    video_urls = []
    for m in media:
        if m.get("type") not in ("video", "animated_gif"): continue
        mp4s = [v for v in (m.get("video_info") or {}).get("variants", []) if v.get("content_type") == "video/mp4"]
        if mp4s: video_urls.append(max(mp4s, key=lambda v: v.get("bitrate", 0))["url"])
    if not video_urls: continue
    rows.append({
        "tweet_id": t.get("id"),
        "author": t.get("author", {}).get("userName"),
        "created_at": t.get("createdAt"),
        "favorite_count": t.get("likeCount", 0),
        "text": (t.get("text") or "").replace("\n", " ")[:200],
        "video_url": video_urls[0],
    })

out = Path(f"{BRAND}_videos.csv")
with open(out, "w") as f:
    w = csv.DictWriter(f, fieldnames=["tweet_id", "author", "created_at", "favorite_count", "text", "video_url"])
    w.writeheader()
    w.writerows(rows)

print(f"saved {len(rows)} video tweets to {out}")
# Cost per twitterapi.io/pricing: len(tweets) × $0.00015
# Downstream: pipe video_url through your CV/audio-transcription pipeline

Researching this?Free starter credits cover end-to-end testing — Google sign-in, no card. Get a key.

08 — Questions

Questions readers ask

How do I get the direct video download URL?

Each returned video tweet has a media array at extendedEntities.media; video items have video_info.variants (multi-bitrate, each with a url). Use the highest-bitrate video/mp4 variant for archive-quality downloads.

What's the difference between filter:videos and filter:native_video?

filter:videos includes any video-tagged tweet (native uploads + GIF-as-video). filter:native_video includes only X-native uploaded video (excludes GIFs + linked-out YouTube cards). For pure X video pulls, use filter:native_video.

Can I search for videos of a specific length?

Not via a search operator. Post-filter after pulling: each video item carries its length in milliseconds at extendedEntities.media[i].video_info.duration_millis, so keep the ones above 30000 for videos of 30 seconds or more. The field is usually missing on animated GIFs, so treat it as optional.

How much video content is on X vs image content?

Typically ~10-20% of engaged media tweets are video (rest images). Native uploaded video is ~5-10% of all engaged media. Ratios shift over time — worth measuring for your specific keyword rather than assuming.

Do video URLs expire?

The variant URLs on media objects point to X's CDN. URLs are stable long-term for public tweets but not guaranteed indefinitely. For archival, download the video within your pull workflow rather than storing just the URL.

Any rate-limit concern for large video pulls?

A paid twitterapi.io account gets 20 requests per second (0.2 before the first payment; see /qps-limits). For very large corpus pulls (10K+ videos), run parallel windowed queries with ThreadPoolExecutor(max_workers=10) and stay under that limit.

Can I filter by video content (not just presence)?

Not via the search API directly. Approach: pull videos matching your keyword, then run each video URL through a CV / speech-to-text pipeline (OpenAI Whisper for audio, CLIP for visual similarity) to filter by content.

09 — Further reading

Continue

Sources & further reading
More from this series

Writing about something you built with TwitterAPI.io? Our community rewards program pays API credits for honest tutorials, build logs and videos.

Build it

Stop reading. Start building.

Starter credits cover real testing on real data. Google sign-in, no card, no application queue.

Get an API key

Already building with TwitterAPI.io? Publish an honest tutorial or build log about it and earn up to $200 a month in API credits.