Twitter (X) Lists API — Programmatic Guide
An X List is a named set of accounts. Reading a List gives you the tweets of exactly those accounts in one call, so a List is a convenient unit for monitoring: you choose the accounts once, in the X app, and your code only needs the List ID.
This guide covers what twitterapi.io can do with a List and what it cannot. It can read a List's tweets, members and followers, and it can add or remove members. It cannot create a List, return a List's name or counts, or tell you which Lists an account belongs to. For those, the guide points to the X app and the official X API. All parameter names, response fields and page sizes below come from the twitterapi.io API reference.
What you can and cannot do with Lists on twitterapi.io
The practical consequence: you need a List ID before you can call anything. The ID is the number at the end of the List URL in the X app, for example x.com/i/lists/1846987139428634858.
The read endpoints accept an API key and a List ID and nothing else. The API reference does not describe any way to read a private List, so plan around public Lists.
Read endpoints: tweets, members, followers
All four read endpoints are GET requests to https://api.twitterapi.io with the header X-API-Key.
Mind the parameter casing. The two tweet endpoints take listId in camelCase. Members, followers, add and remove take list_id in snake_case. Sending the wrong form is the most likely reason for a failed first call. The error text is in msg on all four read endpoints.
Time filters. sinceTime and untilTime on /twitter/list/tweets are Unix timestamps in seconds. sinceTime means on or after, untilTime means before. includeReplies defaults to true. /twitter/list/tweets_timeline has no time filters, so use /twitter/list/tweets when you poll for new tweets.
Pagination. Send cursor as an empty string for the first page, then pass next_cursor while has_next_page is true. The reference warns that has_next_page on /twitter/list/tweets can be true when nothing is left, in which case the next page comes back empty. Stop on an empty page as well as on has_next_page being false.
Tweet fields. Each tweet carries id, url, text, createdAt, lang, likeCount, retweetCount, replyCount, quoteCount, viewCount, bookmarkCount, isReply and an author object.
Account fields. Members and followers use the same user object as author: id, userName, name, followers, following, isBlueVerified, description, location, createdAt, statusesCount and more.
Runnable: monitor the tweets of a List
This script polls one List every 15 minutes, asks only for tweets since the last poll with sinceTime, and prints tweets that contain a keyword and pass an engagement threshold. It stores the last poll time and the tweet IDs it has already handled in a local file.
You pay for the tweets returned, so a poll that asks only for new tweets costs less than re-reading the first pages every time. A poll that returns nothing is charged the minimum of $0.00015.
import json
import os
import time
from pathlib import Path
import requests
BASE = "https://api.twitterapi.io"
HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
LIST_ID = os.environ["WATCH_LIST_ID"] # the number at the end of the List URL
KEYWORD = "pricing" # word to look for in list tweets
MIN_ENGAGEMENT = 20 # likes + retweets
POLL_SECONDS = 900 # one poll every 15 minutes
MAX_PAGES = 10 # safety cap: 10 pages = 200 tweets per poll
STATE = Path(".state/list_monitor.json")
STATE.parent.mkdir(exist_ok=True)
def load_state() -> dict:
if STATE.exists():
return json.loads(STATE.read_text())
# first run: look back one poll interval
return {"since": int(time.time()) - POLL_SECONDS, "seen": []}
def save_state(state: dict) -> None:
state["seen"] = state["seen"][-2000:]
STATE.write_text(json.dumps(state))
def list_tweets_since(list_id: str, since_time: int) -> list:
"""All list tweets created on or after since_time (Unix seconds)."""
tweets, cursor = [], ""
for _ in range(MAX_PAGES):
r = requests.get(
f"{BASE}/twitter/list/tweets",
headers=HEADERS,
params={"listId": list_id, "sinceTime": since_time, "cursor": cursor},
timeout=30,
)
r.raise_for_status()
data = r.json()
if data.get("status") == "error":
raise RuntimeError(data.get("msg"))
page = data.get("tweets") or []
if not page: # has_next_page can be true with nothing left
break
tweets.extend(page)
cursor = data.get("next_cursor") or ""
if not data.get("has_next_page") or not cursor:
break
return tweets
def poll_once() -> None:
state = load_state()
seen = set(state["seen"])
started = int(time.time())
tweets = list_tweets_since(LIST_ID, state["since"])
new = [t for t in tweets if t["id"] not in seen]
print(f"{len(new)} new tweets ({len(tweets)} returned)")
for t in new:
engagement = (t.get("likeCount") or 0) + (t.get("retweetCount") or 0)
if KEYWORD in (t.get("text") or "").lower() and engagement >= MIN_ENGAGEMENT:
author = (t.get("author") or {}).get("userName", "?")
print(f" @{author} ({engagement}): {t.get('url')}")
state["seen"].extend(t["id"] for t in new)
state["since"] = started - 60 # one minute of overlap; 'seen' removes repeats
save_state(state)
while True:
try:
poll_once()
except Exception as exc:
print(f"poll failed: {exc}")
time.sleep(POLL_SECONDS)
Add and remove List members
POST /twitter/list/add_member and POST /twitter/list/remove_member take the same JSON body. Both still need the X-API-Key header.
The response has two fields, status (success or error) and msg.
Where auth_session comes from. The API reference says it is the value returned by /twitter/login_by_2fa. That is the second step of the two-step login: /twitter/login_by_email_or_username takes username_or_email, password and proxy and returns login_data; /twitter/login_by_2fa takes login_data, 2fa_code and proxy and returns session. Pass that session value as auth_session.
Two notes from the reference. It marks /twitter/login_by_2fa as due for deprecation in favour of login v2. It also says the login_cookies from login v2 only works with endpoints that have a v2 suffix, and the List endpoints have none. Check the current docs at docs.twitterapi.io before you build on this flow.
Price. The reference lists a trial price of $0.001 per add or remove call, and $0.003 per call for each of the two login steps.
Creating the List. There is no create endpoint, so create the List once in the X app (or with the official X API POST /2/lists), copy its ID, and then manage members from code.
The script below adds and removes accounts by handle. It waits a few seconds between writes; the reference does not publish a write limit for these endpoints, so treat the pause as a cautious default rather than a documented number.
import os
import time
import requests
BASE = "https://api.twitterapi.io"
HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
AUTH_SESSION = os.environ["TWITTER_AUTH_SESSION"] # "session" value from /twitter/login_by_2fa
PROXY = os.environ["TWITTER_PROXY"] # http://username:password@ip:port
LIST_ID = os.environ["WATCH_LIST_ID"] # a List the logged-in account owns
ENDPOINTS = {
"add": f"{BASE}/twitter/list/add_member",
"remove": f"{BASE}/twitter/list/remove_member",
}
def change_member(action: str, list_id: str, user_name: str) -> None:
"""action is 'add' or 'remove'. Both endpoints take the same body."""
r = requests.post(
ENDPOINTS[action],
headers=HEADERS,
json={
"auth_session": AUTH_SESSION,
"list_id": list_id,
"user_name": user_name, # or "user_id": "<numeric id>"
"proxy": PROXY,
},
timeout=60,
)
r.raise_for_status()
data = r.json()
if data.get("status") != "success":
raise RuntimeError(data.get("msg"))
TO_ADD = ["stripe", "paypal", "adyen"]
TO_REMOVE = ["square"]
for name in TO_ADD:
try:
change_member("add", LIST_ID, name)
print(f"added @{name}")
except Exception as exc:
print(f"could not add @{name}: {exc}")
time.sleep(5) # spread the writes out
for name in TO_REMOVE:
try:
change_member("remove", LIST_ID, name)
print(f"removed @{name}")
except Exception as exc:
print(f"could not remove @{name}: {exc}")
time.sleep(5)
What twitterapi.io does not cover, and where to do it
Three List operations are not in the twitterapi.io API. The official X API has all three. Paths and auth below are from docs.x.com.
If you only need one or a few Lists, creating them by hand in the X app is the shortest route: no developer account, no OAuth. The official API is worth the setup when your product creates Lists for its own users.
A workable split for most monitoring projects: create and name the List in the X app, then use twitterapi.io for the high-volume part, which is reading tweets and members.
Cost of reading Lists
twitterapi.io bills per item returned. $1 buys 100,000 credits; a tweet costs 15 credits and a member or follower costs 18. Every call costs at least 15 credits, including a call that returns nothing. Figures are list prices from twitterapi.io/pricing.
A worked example. One List polled every 15 minutes is 96 polls a day. If each poll returns one full page of 20 tweets, that is 1,920 tweets a day, or $0.288 a day and $8.64 over 30 days. A quiet List costs less, because you pay for tweets returned and the floor is $0.00015 per poll, about $0.43 over 30 days.
The X column is arithmetic on the per-resource prices on the X pricing page. That page also states a cap of 3 million post reads per monthly billing cycle on pay-per-use.
Practical notes
Use the right ID parameter. listId for tweets, list_id for everything else. Wrap the calls in two small helpers so the difference lives in one place.
Choose the poll interval from the List's activity. With 20 tweets per page, a busy List needs several pages per poll. sinceTime keeps each poll to the new tweets only.
Keep a list of seen tweet IDs. sinceTime is on-or-after, so overlapping polls return the boundary tweets twice. Deduplicate by id.
Member lists are paged at 20. A List with 1,000 members takes 50 calls to read in full. Cache the result and refresh it on a schedule instead of on every run.
A removed member's tweets are still reachable. After you remove an account from a List, its tweets no longer come back from the List endpoints. You can still read them with /twitter/user/last_tweets?userName=<handle>.
Compare members over time. There is no endpoint that reports membership changes. Export the members on a schedule and diff the id values to see who was added or removed.
# Export the members and followers of a List to CSV
import csv
import os
import requests
BASE = "https://api.twitterapi.io"
HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
LIST_ID = os.environ["WATCH_LIST_ID"]
MAX_PAGES = 100 # 100 pages x 20 accounts = 2,000 accounts per export
FIELDS = ["id", "userName", "name", "followers", "following", "isBlueVerified", "createdAt"]
ENDPOINTS = {
"members": f"{BASE}/twitter/list/members",
"followers": f"{BASE}/twitter/list/followers",
}
def list_accounts(kind: str, list_id: str) -> list:
"""kind is 'members' or 'followers'. Note: list_id, not listId."""
accounts, cursor = [], ""
for _ in range(MAX_PAGES):
r = requests.get(
ENDPOINTS[kind],
headers=HEADERS,
params={"list_id": list_id, "cursor": cursor},
timeout=30,
)
r.raise_for_status()
data = r.json()
if data.get("status") == "error":
raise RuntimeError(data.get("msg"))
page = data.get(kind) or [] # response key is "members" or "followers"
if not page:
break
accounts.extend(page)
cursor = data.get("next_cursor") or ""
if not data.get("has_next_page") or not cursor:
break
return accounts
def write_csv(path: str, accounts: list) -> None:
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS, extrasaction="ignore")
writer.writeheader()
writer.writerows(accounts)
members = list_accounts("members", LIST_ID)
followers = list_accounts("followers", LIST_ID)
write_csv("list_members.csv", members)
write_csv("list_followers.csv", followers)
member_ids = {m["id"] for m in members}
both = [f["userName"] for f in followers if f["id"] in member_ids]
print(f"{len(members)} members, {len(followers)} followers, {len(both)} accounts are both")
# Cost at list price: 18 credits ($0.00018) per account returned,
# so 1,000 members cost $0.18. Minimum 15 credits per call.
Questions readers ask
Can I create a Twitter (X) List through twitterapi.io?
No. twitterapi.io has no endpoint to create or delete a List. Create it in the X app, or with the official X API (POST /2/lists, per docs.x.com). Once the List exists you can add and remove members with /twitter/list/add_member and /twitter/list/remove_member.
How do I get a List's name, description or member count?
twitterapi.io has no List metadata endpoint. The official X API returns it from GET /2/lists/{id}. If you only need the member count, page through /twitter/list/members and count the results.
How do I find the Lists a user is a member of?
Not with twitterapi.io; there is no memberships endpoint. The official X API has GET /2/users/{id}/list_memberships (docs.x.com).
Is it listId or list_id?
Both, depending on the endpoint. /twitter/list/tweets and /twitter/list/tweets_timeline take listId. /twitter/list/members, /twitter/list/followers, /twitter/list/add_member and /twitter/list/remove_member take list_id.
How many items come back per call?
/twitter/list/tweets returns 20 tweets per page, newest first. /twitter/list/members and /twitter/list/followers return 20 accounts per page. Use cursor with next_cursor to read further pages.
Can I read a private List?
The API reference does not describe private-List access. The read endpoints take only an API key and a List ID, with no parameter for the owner's session, so plan around public Lists.
What do I need to add or remove a member?
An API key, plus a JSON body with auth_session, list_id, proxy, and either user_id or user_name. auth_session is the session value returned by /twitter/login_by_2fa after /twitter/login_by_email_or_username. The reference lists a trial price of $0.001 per call.
Can I get engagement numbers for a whole List?
Not in one call. Read the tweets with /twitter/list/tweets and sum likeCount, retweetCount, replyCount and quoteCount in your own code.
What is the difference between /twitter/list/tweets and /twitter/list/tweets_timeline?
/twitter/list/tweets is documented in more detail: 20 tweets per page, newest first, with sinceTime, untilTime and includeReplies filters. /twitter/list/tweets_timeline takes only listId and cursor, and the reference does not state its page size or order. Use /twitter/list/tweets for polling.
Continue
- twitterapi.io — pricing
- twitterapi.io — API reference
- X — List lookup introduction
- X — List members introduction
- X — Create List reference
- X API — pricing (docs.x.com, 2026 verified)
- 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
- 01What Is a Twitter (X) Chatbot, and How Do You Build One in 2026?
- 02How to Use Google Gemini with the Twitter (X) API
- 03OSINT on Twitter (X): What Public Data You Can Pull, and How
- 04n8n Twitter (X) API Workflow: Post New Tweets to Slack in 2026
- 05How to Use Cursor with the Twitter (X) API
- 06Twitter (X) Search Filters — Programmatic API Guide
- 07Count Tweets — Twitter (X) API Programmatic Guide
- 08Twitter (X) Reply API — A Programmatic Tutorial
Stop reading. Start building.
Starter credits cover real testing on real data. Google sign-in, no card, no application queue.
Get an API key