Mass Unfollow on Twitter (X) — A Developer's API Tutorial

By Alex Chen•7 min read

Mass-unfollow workflows on Twitter (X) come up for several legitimate reasons — pruning a follower list down to a curated set, removing inactive accounts after years of following, cleaning up post-acquisition account migrations. The 'mass' in 'mass unfollow' just means programmatic + batched; the actual per-account unfollow is a single API call per target.

This guide walks the API workflow with runnable Python, the rate-limit + safe-pacing patterns, and the practical operational issues (followers-list iteration, dedup, dry-run mode). It covers two routes: the official X API with OAuth, and twitterapi.io's unfollow_user_v2, which runs through a logged-in session of your own account.

01 — Section

Two routes, both acting as the account owner

Unfollowing requires write access against the account owner's identity. The call acts AS the owner — you can only unfollow accounts that you (the authenticated user) follow.

Steps 1 to 3 use the official X API with OAuth user-context auth. twitterapi.io has an unfollow endpoint as well, POST /twitter/unfollow_user_v2; it has its own section after Step 3.

Pricing per docs.x.com/x-api/getting-started/pricing: an unfollow is billed as Interaction: Delete, $0.010 per request. Cost for unfollowing 1,000 accounts: 1,000 × $0.010 = $10. Math derived directly from the cited rate.

02 — Section

Step 1 — list the accounts you currently follow

Before unfollowing, get the current following list. X official: /2/users/{id}/following, returns followers up to 1,000 per page with cursor pagination.

Pricing per docs.x.com pricing: the Following/Followers: Read line, $0.010 per user returned (a following-list read is billed per resource, not per call). For a 5,000-follow list, that's 5,000 × $0.010 = $50.

python
# pip install tweepy
import tweepy

client = tweepy.Client(
    consumer_key="YOUR_KEY",
    consumer_secret="YOUR_SECRET",
    access_token="USER_TOKEN",
    access_token_secret="USER_TOKEN_SECRET",
)

def get_following(user_id: str) -> list:
    """Page through the entire following list."""
    all_following = []
    for page in tweepy.Paginator(
        client.get_users_following,
        id=user_id,
        max_results=1000,
    ):
        all_following.extend(page.data or [])
    return all_following

following = get_following("YOUR_USER_ID")
print(f"Currently following {len(following)} accounts")
03 — Section

Step 2 — filter to the unfollow targets

Most mass-unfollow workflows filter by some criterion before issuing the deletes:

Inactive accounts — fetch each followed account's created_at and last-tweet timestamp; unfollow if no posts in N days

No engagement back — find accounts you follow that don't follow you back (asymmetric relationship)

Below threshold — accounts under N followers, accounts that posted in a banned topic, accounts on a blocklist

Explicit list — you have a CSV of usernames to drop, just resolve each to user_id

Build the target list client-side from the previous step's result + your filter criterion. This is also a good place to add a dry-run mode that prints what would be unfollowed without actually issuing the deletes.

04 — Section

Step 3 — execute the unfollows with rate-limit safety

X publishes rate limits in its docs (separate from per-call price). The unfollow endpoint has its own rate limit; consult docs.x.com for current numbers.

Safe pacing: 1 unfollow per second is a conservative baseline that fits comfortably under most rate limits. Faster bursting risks 429 and (rarely) account-level review.

Backoff on 429: when the API returns 429 Too Many Requests, back off before retrying: start at about a second and double it each time, plus jitter (1-5 seconds random); twitterapi.io sends no Retry-After header to read. Don't retry tightly.

Don't unfollow during normal X UI activity: if you're using your account normally at the same time as the script, the combined activity can trip limits faster. Run the script during quiet hours.

python
import time, random

DRY_RUN = False  # flip to False when ready

def mass_unfollow(targets: list, my_user_id: str):
    success, fail = [], []
    for target_id in targets:
        if DRY_RUN:
            print(f"[dry-run] would unfollow {target_id}")
            continue
        try:
            client.unfollow_user(target_user_id=target_id)
            success.append(target_id)
        except tweepy.TooManyRequests:
            print("429 — sleeping 60s + jitter")
            time.sleep(60 + random.uniform(0, 5))
        except Exception as e:
            fail.append((target_id, str(e)))
        time.sleep(1.0 + random.uniform(0.1, 0.5))  # safe pacing: ~1 unfollow/sec
    return success, fail

# success, fail = mass_unfollow(target_ids, "YOUR_USER_ID")
# print(f"unfollowed {len(success)}, errors {len(fail)}")
05 — Section

Second route — twitterapi.io /twitter/unfollow_user_v2

Write actions on twitterapi.io run through an authenticated session: you log the account in once with POST /twitter/user_login_v2 ($0.10 per login), passing your X username, email, password and a proxy you supply (a residential or ISP proxy works best), and it returns login_cookies. Store the returned login_cookies and reuse them until the session ends; do not log in on every run. Each post, delete, like, retweet, follow or unfollow is then one call at $0.002 that takes login_cookies and the same proxy. The call acts as that X account, so X's rules apply to it and X can rate-limit, lock or suspend the account; keep a human pace. The session can stop working and require a new login. The proxy is required on every write call. Billing is per call that returns HTTP 200, and that includes some refusals from X, which come back as HTTP 200 with status: error. Reads are different: they need only an API key and no X account at all.

POST /twitter/unfollow_user_v2 takes a JSON body with three required fields: login_cookies, user_id and proxy, plus your key in the X-API-Key header. user_id is the numeric ID, not the handle. The list of accounts you follow comes from GET /twitter/user/followings, which returns up to 200 accounts per page with id and userName for each and costs 1 credit per account at 200 per page ($0.01 per 1,000). The response to an unfollow has a status field (success or error); check it on every call.

Price, side by side: $0.010 per unfollow on the official API (Interaction: Delete) vs $0.002 here (0.010 / 0.002 = 5x), plus the $0.10 login, which is one per session and not one per unfollow. For 1,000 unfollows that is 1,000 × $0.010 = $10.00 official vs 1,000 × $0.002 + $0.10 = $2.10 here. For a handful the official side is cheaper in total: 5 unfollows cost 5 × $0.010 = $0.05 there and 5 × $0.002 + $0.10 = $0.11 here. The two totals cross at 13 unfollows per login.

The script below lists who you follow, applies your own filter, and unfollows a small batch with a dry run, a log line per action and a few seconds between calls. The bulk unfollow guide linked at the end of this page goes further on choosing targets (inactive accounts, mutual follows).

python
import os, json, time, random, requests
from datetime import datetime, timezone

BASE = "https://api.twitterapi.io"
HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
PROXY = os.environ["X_PROXY"]  # a proxy you supply: http://username:password@ip:port

def login() -> str:
    """Log the account in once ($0.10) and return login_cookies. Store and reuse it."""
    body = {
        "user_name": os.environ["X_USERNAME"],
        "email": os.environ["X_EMAIL"],
        "password": os.environ["X_PASSWORD"],
        "proxy": PROXY,
    }
    if os.environ.get("X_TOTP_SECRET"):
        body["totp_secret"] = os.environ["X_TOTP_SECRET"]
    r = requests.post(f"{BASE}/twitter/user_login_v2",
                      headers=HEADERS, json=body, timeout=120)
    r.raise_for_status()
    data = r.json()
    if data.get("status") != "success":
        raise RuntimeError(f"login failed: {data.get('message')}")
    return data["login_cookies"]

def list_following(handle: str) -> list:
    """Accounts the handle follows. A read: API key only, no session."""
    users, cursor = [], ""
    while True:
        r = requests.get(f"{BASE}/twitter/user/followings", headers=HEADERS,
                         params={"userName": handle, "pageSize": 200, "cursor": cursor},
                         timeout=30)
        r.raise_for_status()
        body = r.json()
        users.extend(body.get("followings", []))
        cursor = body.get("next_cursor")
        if not body.get("has_next_page") or not cursor:
            return users

def unfollow(login_cookies: str, user_id: str) -> dict:
    r = requests.post(
        f"{BASE}/twitter/unfollow_user_v2",
        headers=HEADERS,
        json={"login_cookies": login_cookies, "user_id": user_id, "proxy": PROXY},
        timeout=60,
    )
    r.raise_for_status()
    return r.json()  # carries "status": "success" or "error"

def keep(user: dict) -> bool:
    """Your own rule. Return True to keep following this account."""
    return True  # placeholder: nothing is unfollowed until you write a rule

DRY_RUN = True      # flip to False after reading the dry-run log
BATCH = 20          # your own choice; keep it small
LOG_FILE = "unfollow_v2_log.jsonl"

following = list_following(os.environ["X_USERNAME"])
targets = [u for u in following if u.get("id") and not keep(u)][:BATCH]
print(f"following {len(following)}, would unfollow {len(targets)}: "
      f"about ${len(targets) * 0.002:.3f} plus one $0.10 login")

cookies = "" if DRY_RUN else (os.environ.get("X_LOGIN_COOKIES") or login())
with open(LOG_FILE, "a", encoding="utf-8") as log:
    for u in targets:
        entry = {"user_id": u["id"], "handle": u.get("userName"),
                 "at": datetime.now(timezone.utc).isoformat()}
        if DRY_RUN:
            entry["result"] = "dry_run"
        else:
            try:
                result = unfollow(cookies, u["id"])
                entry["result"] = result.get("status")
                entry["message"] = result.get("message")
            except requests.RequestException as exc:
                entry["result"] = f"error: {exc}"
            time.sleep(random.uniform(4, 9))  # a few seconds, with jitter
        log.write(json.dumps(entry, ensure_ascii=False) + "\n")
06 — Section

Side-by-side comparison — 4 paths to mass unfollow

Same job (unfollow N accounts) framed across four options. Costs derived from cited pricing.

DimensionX official APITweepy wrapper (same path)twitterapi.io unfollow_user_v2UI dashboard tool
Per-unfollow cost$0.010 (Interaction: Delete, docs.x.com)$0.010 (same surface)$0.002, plus one $0.10 login per sessionbundled in tier
AuthOAuth user-contextOAuth user-contextAPI key + login_cookies + a proxy you supplyUI login
Programmaticyesyes (ergonomic wrapper)yesno (manual or scheduled-batch)
Write supportyes, OAuth user-contextyes, OAuth user-contextyes, via an authenticated sessionthrough the tool's interface
Best forcustom filter logic + large volumequick-start, smaller volumesscripted cleanup without an X developer appnon-dev users

Two patterns: (a) there are two programmatic routes, the official API and twitterapi.io's unfollow_user_v2, and dashboard tools are the no-code choice; (b) at any meaningful volume (100+ accounts), the programmatic path is materially faster + cheaper than UI clicking.

07 — Section

Operational notes — suspension risk + reversibility

Suspension risk: very aggressive unfollow patterns can get an account restricted by X, whichever route sends the calls. Don't unfollow >1,000 accounts in a single hour; spread across multiple days for large lists. The risk is low for small workflows; non-zero for very-large ones.

Reversibility: an unfollow is reversible — you can refollow the account if you change your mind. There's no soft-delete window; the unfollow is immediate.

Audit trail: log every unfollow with timestamp + reason (filter criterion). Useful if you need to refollow later or audit the workflow.

Communication: if you're cleaning up a brand account's following list, consider sending a one-time tweet explaining the cleanup — some followers feel slighted by silent unfollows and the explanation softens that.

08 — Section

Cost framing — 100 / 1,000 / 10,000 unfollows

Math from docs.x.com/x-api/getting-started/pricing, where an unfollow is billed as Interaction: Delete at $0.010 per request:

100 unfollows = $1.00 — single-script run, no concern

1,000 unfollows = $10.00 — split across 2-3 days for safety

10,000 unfollows = $100.00 — split across 7-14 days, monitor for 429s and account warnings

Plus the cost of reading the current following list to find targets — at $0.010 per user returned, 1,000 follows listed cost $10 (1,000 × $0.010). For a 10,000-follow account that's $100 to list, $100 to mass-unfollow.

The same volumes through twitterapi.io at $0.002 per unfollow plus one $0.10 login: 100 unfollows = 100 × $0.002 + $0.10 = $0.30, 1,000 = $2.10, 10,000 = $20.10. Listing 1,000 follows with GET /twitter/user/followings at 200 per page is 1,000 credits, which is $0.01.

python
# Practical example: end-to-end mass unfollow with dry-run, safe pacing, audit log.
import os, time, random, json
from datetime import datetime, timezone
import tweepy

client = tweepy.Client(
    consumer_key=os.environ["X_CONSUMER_KEY"],
    consumer_secret=os.environ["X_CONSUMER_SECRET"],
    access_token=os.environ["X_USER_TOKEN"],
    access_token_secret=os.environ["X_USER_SECRET"],
)

MY_USER_ID = os.environ["X_MY_USER_ID"]
DRY_RUN = True  # start safe

def get_following():
    return [u for page in tweepy.Paginator(
                client.get_users_following, id=MY_USER_ID, max_results=1000)
            for u in (page.data or [])]

def filter_targets(following):
    # Replace with your real filter — example: inactive (none tweeted in 90d)
    return [u for u in following if False]  # placeholder

def mass_unfollow(targets):
    log_path = f"unfollow_log_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.jsonl"
    success, fail = 0, []
    with open(log_path, "a") as log:
        for u in targets:
            entry = {"target_id": u.id, "username": u.username, "at": datetime.now(timezone.utc).isoformat()}
            if DRY_RUN:
                entry["action"] = "dry_run"
            else:
                try:
                    client.unfollow_user(target_user_id=u.id)
                    entry["action"] = "unfollowed"
                    success += 1
                except tweepy.TooManyRequests:
                    entry["action"] = "rate_limited"
                    log.write(json.dumps(entry) + "\n")
                    time.sleep(60 + random.uniform(0, 5))
                    continue
                except Exception as e:
                    entry["action"] = f"error: {e}"
                    fail.append(u)
            log.write(json.dumps(entry) + "\n")
            time.sleep(1.0 + random.uniform(0.1, 0.5))  # safe pacing
    return success, fail, log_path

following = get_following()
print(f"following: {len(following)}")
targets = filter_targets(following)
print(f"would unfollow: {len(targets)}")
if targets:
    s, f, p = mass_unfollow(targets)
    print(f"done: {s} succeeded, {len(f)} errors. Log: {p}")

# Cost framing (math from cited pricing):
#   1,000 unfollow targets = 1,000 × $0.010 = $10.00 (Interaction: Delete)
#   + listing the 1,000 follows for the target-selection step = 1,000 × $0.010 = $10.00 (Following/Followers: Read)
# Total = $20.00 to mass-unfollow 1,000 accounts.
09 — Questions

Questions readers ask

Can I mass-unfollow via twitterapi.io?

Yes. POST /twitter/unfollow_user_v2 takes login_cookies, a numeric user_id and proxy, and costs $0.002 per call against $0.010 per unfollow on the official API (Interaction: Delete). You get login_cookies from one POST /twitter/user_login_v2 call ($0.10 per login, one per session). The unfollow is made as your own X account through a proxy you supply, so the conditions in the twitterapi.io section above apply; for fewer than 13 unfollows on a fresh login the official route costs less in total.

How fast can I unfollow without getting suspended?

Conservative baseline is 1 unfollow per second, spread across hours not bursted. Most operational practice keeps under 1,000 unfollows per hour. The suspension risk is non-zero at very-high volume — split very-large lists across days.

Will the people I unfollow be notified?

No — X does not send notifications for unfollows. Some third-party tools surface 'unfollower notifications' to subscribers, so people running those tools may see; X itself does not notify.

Can I batch multiple unfollows in one API call?

No — the X unfollow endpoint is per-target, and so is unfollow_user_v2. You issue N requests for N unfollows. Use sensible pacing + retry logic; don't try to parallel-burst many concurrent requests.

What if I unfollow an account I should have kept?

Just refollow. The unfollow is reversible. There's no soft-delete window; you can refollow immediately.

Are there X-rate limits on the read side (following-list pagination)?

Yes — /2/users/{id}/following has its own rate limit documented at docs.x.com. For a 10K-follow account that's 10 page reads, well under most rate-limit windows; no issue at typical mass-unfollow volumes.

10 — 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

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