TwitterAPI.io is an independent third-party service. Not affiliated with X Corp.

Twitter (X) Lists API — Programmatic Guide

By Alex Chen•8 min read

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.

01 — Section

What you can and cannot do with Lists on twitterapi.io

Tasktwitterapi.ioWhere to do it otherwise
Read the tweets of a ListGET /twitter/list/tweets, GET /twitter/list/tweets_timeline
Read the members of a ListGET /twitter/list/members
Read the followers (subscribers) of a ListGET /twitter/list/followers
Add a memberPOST /twitter/list/add_member
Remove a memberPOST /twitter/list/remove_member
Create or delete a ListNot availableX app, or official X API POST /2/lists and DELETE /2/lists/{id}
Read List metadata (name, description, counts, owner)Not availableOfficial X API GET /2/lists/{id}
Find the Lists a user is a member ofNot availableOfficial X API GET /2/users/{id}/list_memberships

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.

02 — Section

Read endpoints: tweets, members, followers

All four read endpoints are GET requests to https://api.twitterapi.io with the header X-API-Key.

EndpointParametersReturnsPage size
/twitter/list/tweetslistId (required), sinceTime, untilTime, includeReplies, cursortweets, has_next_page, next_cursor, status, msg20 tweets, newest first
/twitter/list/tweets_timelinelistId (required), cursortweets, has_next_page, next_cursorNot stated in the reference
/twitter/list/memberslist_id (required), cursormembers, has_next_page, next_cursor, status, msg20 accounts
/twitter/list/followerslist_id (required), cursorfollowers, has_next_page, next_cursor, status, msg20 accounts

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.

03 — Section

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.

python
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)
04 — Section

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.

FieldRequiredMeaning
auth_sessionYesThe session of a logged-in X account
list_idYesThe List to change
proxyYesA proxy in the form http://username:password@ip:port; the reference asks for a residential proxy
user_idOne of the twoNumeric ID of the account to add or remove
user_nameOne of the twoHandle of the account to add or remove

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.

python
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)
05 — Section

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.

OperationOfficial X APIAuth (docs.x.com)
Create a ListPOST /2/lists with name, optional description and privateUser context; OAuth 2.0 needs the list.write scope
Delete a ListDELETE /2/lists/{id}User context
List metadataGET /2/lists/{id}Bearer token or user context
Lists a user is a member ofGET /2/users/{id}/list_membershipsBearer token or user context

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.

06 — Section

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.

Itemtwitterapi.ioOfficial X API pay-per-use (docs.x.com)
One tweet from a List$0.00015$0.005 per post read
One page of 20 tweets$0.003$0.10
1,000 tweets$0.15$5.00
One member or follower$0.00018$0.010 per user read
1,000 members$0.18$10.00
Add or remove a member$0.001 per call (trial price in the API reference)$0.005 per request (List: Manage)
Create a ListNot available$0.010 per request (List: Create)

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.

07 — Section

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.

python
# 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.
08 — Questions

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.

09 — Further reading

Continue

Sources & further reading
More from this series
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