Twitter (X) Lists API — Programmatic Guide
Twitter (X) Lists are user-curated collections of accounts — the platform's oldest and most under-appreciated organizing primitive. For developers building content-monitoring, competitive-intelligence, or curated-signal workflows, Lists are the single most efficient way to reduce noise vs polling the full firehose.
This guide walks the programmatic API surface for Lists: reading list metadata, enumerating members, pulling list-scoped tweet streams, and (with write auth) managing list membership. Runnable Python + cost math per typical workload.
Why Lists matter for programmatic monitoring
A well-curated List of 20-50 accounts in your niche produces a signal-dense tweet stream that would take 100× the API calls to reconstruct via keyword search. The trade-off: curation effort upfront.
Typical use cases:
Industry-analyst tweet monitoring — Follow the 30 analysts covering your industry via a List; pull their tweets every 15 min. Alerts your team to competitor moves, funding rounds, market signals before they hit mainstream news.
Competitor tracking — Add 5-10 competitor accounts to a private List; monitor their content velocity + engagement patterns as leading indicators of their strategy shifts.
Curated feeds for downstream products — Public-facing Lists (e.g. 'Top 50 dev tools voices') can power your own website's tweet-widget or newsletter section.
Audience segmentation — Group your customers, prospects, or partners into Lists; pull their content to understand what your ICP talks about.
Core endpoints — read
/twitter/list/info?list_id= — Get list metadata: name, description, member count, subscriber count, owner, private/public.
/twitter/list/members?list_id= — Enumerate members. Cursor-paginated for lists >1000 members. Each entry returns user_id, userName, followers_count, verified.
/twitter/list/tweets?list_id= — Pull recent tweets from all list members. Cursor-paginated. Chronologically-sorted. This is the workhorse endpoint for stream-monitoring workflows.
/twitter/list/subscribers?list_id= — Enumerate accounts that follow (subscribe to) the list. Signal for list influence.
All read endpoints are X-API-Key auth (no OAuth user-context needed for public lists). Private lists require the owner's session cookie.
Runnable — list-scoped tweet monitor
One end-to-end script: pull all new tweets from a List every 15 min, alert on high-engagement mentions of your brand.
import os, requests, time, json
from pathlib import Path
from datetime import datetime, timezone
HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
BASE = "https://api.twitterapi.io"
LIST_ID = os.environ["WATCH_LIST_ID"] # e.g. '1234567890'
BRAND = "stripe"
MIN_ENGAGEMENT = 20
STATE = Path(".state/list_seen.json"); STATE.parent.mkdir(exist_ok=True)
def load_seen() -> set:
return set(json.loads(STATE.read_text())) if STATE.exists() else set()
def save_seen(s: set):
STATE.write_text(json.dumps(sorted(s)[-2000:]))
def pull_list_tweets(list_id: str, max_pages: int = 5) -> list:
tweets, cursor = [], None
for _ in range(max_pages):
params = {"list_id": list_id, "count": 200}
if cursor: params["cursor"] = cursor
r = requests.get(f"{BASE}/twitter/list/tweets", 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
def poll_once():
seen = load_seen()
all_tweets = pull_list_tweets(LIST_ID)
new_tweets = [t for t in all_tweets if t["id"] not in seen]
print(f" {len(new_tweets)} new tweets from list · {len(all_tweets)} total in poll window")
for t in new_tweets:
text = (t.get("text") or "").lower()
engagement = t.get("favorite_count", 0) + t.get("retweet_count", 0)
if BRAND in text and engagement >= MIN_ENGAGEMENT:
author = t.get("author", {}).get("userName", "?")
print(f" \U0001F6A8 High-signal brand mention from @{author} ({engagement} engagement): {t.get('text', '')[:150]}")
seen.update(t["id"] for t in new_tweets)
save_seen(seen)
while True:
try:
poll_once()
except Exception as e:
print(f" ERROR: {e}")
time.sleep(900) # 15-min poll cycle
# Cost per twitterapi.io/pricing:
# Typical 30-account List = ~50-200 tweets/poll x 96 polls/day = ~5-20K tweets/day = $0.75-3/dayManage list membership (write endpoints)
Write operations require user-context auth (session cookie via login flow OR OAuth 2.0 on X official):
POST /twitter/list/create — Create a new List with name + description + private/public setting. Returns new list_id.
POST /twitter/list/add_member — Add a member to your List. Payload: list_id, user_id (or userName).
POST /twitter/list/remove_member — Remove a member. Same payload shape as add.
Rate-limit safe pacing: 1 add/remove per 3-5 seconds per authenticated account to stay well below X's write ceilings. For bulk List import (100+ members), distribute across hours.
# Bulk import competitor accounts to a private tracking List
import os, requests, time, random
BASE = "https://api.twitterapi.io"
SESSION = os.environ["TWITTERAPI_SESSION_COOKIE"]
def create_list(name: str, description: str, private: bool = True) -> str:
r = requests.post(
f"{BASE}/twitter/list/create",
cookies={"session": SESSION},
json={"name": name, "description": description, "mode": "private" if private else "public"},
timeout=15,
)
r.raise_for_status()
return r.json()["list_id"]
def add_member(list_id: str, user_name: str):
r = requests.post(
f"{BASE}/twitter/list/add_member",
cookies={"session": SESSION},
json={"list_id": list_id, "userName": user_name},
timeout=15,
)
r.raise_for_status()
COMPETITORS = ["paypal", "stripe", "adyen", "checkout", "square"]
list_id = create_list("Payment competitor tracking", "Auto-imported for competitive monitoring", private=True)
print(f"created list_id={list_id}")
for c in COMPETITORS:
try:
add_member(list_id, c)
print(f" added @{c}")
except Exception as e:
print(f" ERROR adding @{c}: {e}")
time.sleep(3 + random.uniform(1, 2)) # safe pacingComparison — 3 paths for List-based data
Cost math at 4 monitoring volumes
Practical rule: List-based polling is 30-90% more cost-efficient than keyword-search polling for the same signal density, because Lists are pre-filtered by curator judgment.
Common patterns + gotchas
Poll cadence sweet spot: 15-min for most curated Lists (dev analysts, industry watchers). Sub-minute polls waste credits without much added latency benefit. Longer polls (>60 min) risk missing high-velocity moments.
Private vs public Lists: private Lists are only visible to the owner. Access via API requires the owner's session cookie. Public Lists (anyone can view via twitter.com/i/lists/
Removed members disappear from stream: if a List member gets removed (by owner) mid-poll, their historical tweets stay retrievable via /twitter/user/last_tweets?userId=, but they stop appearing in list-tweets stream.
Suspended member handling: suspended accounts (X TOS) drop out of list-tweets automatically. Log as member_suspended events for compliance / audit trail.
Rate-limit at bulk write: creating a List then bulk-adding 100 members hits X's write ceiling around 30-50 adds/hour per authenticated account. For bulk import >100 members, pace across 3-5 hours or distribute across multiple accounts.
# Complete pattern: monitor 3 curated Lists + differential signals per List
import os, requests, time, json
from pathlib import Path
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
BASE = "https://api.twitterapi.io"
WATCHLISTS = {
"industry_analysts": os.environ["LIST_ANALYSTS_ID"],
"competitor_ceos": os.environ["LIST_COMPETITORS_ID"],
"customer_advocates": os.environ["LIST_ADVOCATES_ID"],
}
STATE_DIR = Path(".state"); STATE_DIR.mkdir(exist_ok=True)
def seen_file(list_name): return STATE_DIR / f"list_{list_name}_seen.json"
def load_seen(list_name):
f = seen_file(list_name)
return set(json.loads(f.read_text())) if f.exists() else set()
def save_seen(list_name, s):
seen_file(list_name).write_text(json.dumps(sorted(s)[-2000:]))
def pull_list_new(list_name: str, list_id: str) -> tuple[str, list]:
r = requests.get(f"{BASE}/twitter/list/tweets", headers=HEADERS, params={"list_id": list_id, "count": 200}, timeout=15)
r.raise_for_status()
all_tweets = r.json().get("tweets", [])
seen = load_seen(list_name)
new_tweets = [t for t in all_tweets if t["id"] not in seen]
seen.update(t["id"] for t in new_tweets)
save_seen(list_name, seen)
return (list_name, new_tweets)
def poll_all():
with ThreadPoolExecutor(max_workers=3) as ex:
results = list(ex.map(lambda kv: pull_list_new(kv[0], kv[1]), WATCHLISTS.items()))
for list_name, new_tweets in results:
if not new_tweets: continue
total_engagement = sum(t.get("favorite_count", 0) + t.get("retweet_count", 0) for t in new_tweets)
avg_engagement = total_engagement / max(len(new_tweets), 1)
top_engaged = max(new_tweets, key=lambda t: t.get("favorite_count", 0) + t.get("retweet_count", 0))
print(f" [{list_name}] {len(new_tweets)} new · avg_engagement {avg_engagement:.0f}")
author = top_engaged.get("author", {}).get("userName", "?")
print(f" TOP · @{author}: {top_engaged.get('text', '')[:120]}")
while True:
try:
poll_all()
except Exception as e:
print(f" ERROR: {e}")
time.sleep(900)
# Cost per twitterapi.io/pricing:
# 3 Lists x avg 30 members x ~50 new tweets/poll x 96 polls/day = ~15K tweets/day = ~$2.25/day = ~$67/month totalQuestions readers ask
Can I read a private List I don't own?
No — private Lists are only visible to the owner via the API. To read another user's private List, you'd need their session cookie (not typically shareable). For monitoring competitor Lists, use the public ones or build your own private List with the same members.
What's the max members in one List?
X's platform limit is 5,000 members per List. API pagination handles this transparently via cursor. For >5,000 tracked accounts, split across multiple Lists (grouped by topic or region).
How do I find Lists a user is a member of?
/twitter/user/memberships?userName= returns all Lists the user has been added to (public + private-that-they-own). Useful for reverse lookup: what curators consider this account authoritative in their niche.
Do List add/remove operations notify the added user?
Adding a member to a public List generates a notification on X. Private List adds do not notify. If you're building tracking workflows with sensitivity concerns, use private Lists.
Can I get engagement metrics scoped to a List?
Not directly via a single call. Pull tweets from the List with /twitter/list/tweets, then aggregate favorite_count + retweet_count + reply_count in your downstream code for List-scoped engagement analytics.
How does this differ from Following list monitoring?
Following = the accounts you personally follow (up to 5,000). Lists = curated collections of accounts (up to 5,000 per List, with unlimited Lists per account). Lists are the standard programmatic monitoring primitive — Following is your personal feed.
Any ToS concern with automated List management?
Standard rate limits + human-like pacing apply (see /blog/twitter-api-error-handling-best-practices). List reads are read-only public data — no ToS issue. List writes (create / add / remove) follow the same write-endpoint pacing rules as tweeting or following.
Continue
- twitterapi.io — pricing
- X — Lists API introduction
- X API — pricing (docs.x.com, 2026 verified)
- X — rate limits reference
- Twitter (X) API — cluster hub
- Twitter (X) monitoring tools developer comparison
- Twitter (X) account views tracking API
- Twitter (X) API error handling best practices
- Twitter (X) follower tracking API guide
- twitterapi.io pricing
Stop reading. Start building.
Starter credits cover real testing on real data. Google sign-in, no card, no application queue.
Get an API key