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

Blogbulk unlike twitter

Bulk Unlike + Unretweet on X API — Complete Guide

By Alex Chen6 min read

Bulk unliking or undoing all your retweets on X is one of the most searched-but-under-documented dev workflows — most tutorials focus on tweet deletion, glossing over the parallel like/retweet cleanup that developers actually need when scrubbing an account before a rebrand, privacy pass, or account handoff. This guide walks the full workflow with runnable Python: enumerate your liked-tweet IDs and retweet IDs via twitterapi.io reads, then execute the DELETE loop against X official's write endpoints with rate-limit pacing and audit logging.

The API path scales in a way the web tools (Redact, Circleboom, TweetEraser) can't — no monthly batch caps, no paid tier at 10K volume, no manual UI clicking. But the responsibility for pacing, dry-run safety, and archive-before-execute is on you. This guide covers all three, plus the combined 'unlike + unretweet in one pass' pattern for full account cleanup.

01 — Section

The two endpoints — parallel structure, one workflow

Both unlike and unretweet share the same conceptual pattern on X's API: DELETE /2/users/{authenticated_user_id}//{target_tweet_id} with OAuth 2.0 user-context auth. Per docs.x.com/x-api/likes and docs.x.com/x-api/retweets:

Unlike: DELETE /2/users/{id}/likes/{tweet_id} — removes the authenticated user's like from tweet_id.

Unretweet: DELETE /2/users/{id}/retweets/{source_tweet_id} — removes the authenticated user's retweet of source_tweet_id.

Both return data.liked: false / data.retweeted: false on success. Both are idempotent (calling again on an already-unliked tweet returns success). Both are rate-limited per X's standard limits (see docs.x.com/x-api/fundamentals/rate-limits) — the practical operational pacing is 1-2 seconds per call for sustained loops.

For pricing, refer to docs.x.com/x-api/getting-started/pricing for the current per-call cost of DELETE operations on the tier you're using — X's pricing model has evolved and per-endpoint costs vary by tier. The workflow patterns below assume you've reviewed current pricing before executing at volume.

02 — Section

Step 1 — enumerate your liked tweet IDs

Before you can unlike, you need the list of tweets you've liked. X official offers /2/users/{id}/liked_tweets with paginated cursor. twitterapi.io offers /twitter/user/liked_tweets — same intent, typically cheaper per-tweet read cost at $0.00015 (see twitterapi.io/pricing).

Enumerate the full list with the cursor loop below. For a decade-old account with 10K liked tweets, the read cost is ~$1.50 — negligible relative to the write cost of the actual unlikes.

python
import os, requests, json

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

def list_liked_tweet_ids(handle: str) -> list[str]:
    """Get all tweet IDs the authenticated user has liked.
    Cost ~ $0.00015 per tweet returned.
    """
    ids = []
    cursor = None
    while True:
        params = {"userName": handle}
        if cursor:
            params["cursor"] = cursor
        r = requests.get(
            f"{BASE}/twitter/user/liked_tweets",
            headers=HEADERS, params=params, timeout=15,
        )
        r.raise_for_status()
        resp = r.json()
        for t in resp.get("tweets", []):
            ids.append(t["id"])
        cursor = resp.get("next_cursor")
        if not cursor:
            break
    return ids

liked_ids = list_liked_tweet_ids("your_handle")
print(f"found {len(liked_ids)} liked tweets to unlike")
print(f"list-read cost: ${len(liked_ids) * 0.00015:.4f}")
03 — Section

Step 2 — execute the unlike loop with pacing + audit

With the ID list in hand, loop DELETE /2/users/{id}/likes/{tweet_id} with 1-2 sec pacing. Always dry-run first — the delete is not truly undoable at bulk scale (you can re-like individual tweets but not restore an entire history).

The code below uses tweepy for X official auth handling. It logs every attempt to a timestamped JSONL for later review — if you regret specific unlikes, the log tells you what to re-like manually.

python
# pip install tweepy
import tweepy, time, json, random
from datetime import datetime, timezone

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

def bulk_unlike(tweet_ids: list[str], dry_run: bool = True) -> dict:
    """Unlike tweets with audit log + rate-limit safety."""
    log_path = f"unlike_log_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.jsonl"
    success, fail = 0, 0
    with open(log_path, "a") as log:
        for tid in tweet_ids:
            entry = {"tweet_id": tid, "action": "unlike", "at": datetime.now(timezone.utc).isoformat()}
            if dry_run:
                entry["result"] = "dry_run"
                log.write(json.dumps(entry) + "\n")
                continue
            try:
                client.unlike(tweet_id=tid)
                entry["result"] = "unliked"
                success += 1
            except tweepy.TooManyRequests:
                entry["result"] = "rate_limited"
                log.write(json.dumps(entry) + "\n")
                time.sleep(60 + random.uniform(0, 5))
                continue
            except Exception as e:
                entry["result"] = f"failed: {e}"
                fail += 1
            log.write(json.dumps(entry) + "\n")
            time.sleep(1.0 + random.uniform(0.1, 0.5))
    return {"success": success, "fail": fail, "log": log_path}

# Always run dry_run=True first
# result = bulk_unlike(liked_ids, dry_run=False)
04 — Section

Step 3 — enumerate + unretweet in parallel

Retweets follow the identical pattern. twitterapi.io: /twitter/user/retweets?userName= returns all tweets the user has retweeted (with the original source_tweet_id). X official: /2/users/{id}/tweets?tweet.fields=referenced_tweets and filter referenced_tweets.type == 'retweeted'.

For account-wide cleanup, run unlike + unretweet in the same session — they don't conflict, and pacing lets you interleave requests without hitting rate limits. Combined pattern:

python
def list_retweet_source_ids(handle: str) -> list[str]:
    """Get all source_tweet_ids the user has retweeted."""
    ids = []
    cursor = None
    while True:
        params = {"userName": handle}
        if cursor:
            params["cursor"] = cursor
        r = requests.get(
            f"{BASE}/twitter/user/retweets",
            headers=HEADERS, params=params, timeout=15,
        )
        r.raise_for_status()
        resp = r.json()
        for t in resp.get("tweets", []):
            src = t.get("referenced_tweet_id") or t.get("id")
            if src:
                ids.append(src)
        cursor = resp.get("next_cursor")
        if not cursor:
            break
    return ids

def bulk_unretweet(source_ids: list[str], dry_run: bool = True) -> dict:
    log_path = f"unretweet_log_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.jsonl"
    success, fail = 0, 0
    with open(log_path, "a") as log:
        for sid in source_ids:
            entry = {"source_tweet_id": sid, "action": "unretweet", "at": datetime.now(timezone.utc).isoformat()}
            if dry_run:
                entry["result"] = "dry_run"
            else:
                try:
                    client.unretweet(source_tweet_id=sid)
                    entry["result"] = "unretweeted"
                    success += 1
                except tweepy.TooManyRequests:
                    entry["result"] = "rate_limited"
                    log.write(json.dumps(entry) + "\n")
                    time.sleep(60 + random.uniform(0, 5))
                    continue
                except Exception as e:
                    entry["result"] = f"failed: {e}"
                    fail += 1
            log.write(json.dumps(entry) + "\n")
            time.sleep(1.0 + random.uniform(0.1, 0.5))
    return {"success": success, "fail": fail, "log": log_path}

rt_ids = list_retweet_source_ids("your_handle")
print(f"found {len(rt_ids)} retweets to undo")
# bulk_unretweet(rt_ids, dry_run=False)  # after dry-run verify
05 — Section

The combined 'full account cleanup' pattern

For a genuine account reset — often bundled with mass-tweet-delete (see /blog/delete-tweets-free-api-bulk-tutorial) — run all three workflows in sequence with a shared audit log directory. Order matters: unlike + unretweet first, then delete tweets last. Reason: if you delete your own tweets first, they still exist on the server for the rate-limit window but querying references gets weird; likes and retweets pointed at other users' tweets are unaffected by the ordering.

Full sequence for 'delete all activity' account reset:

1. Export current state (see /blog/twitter-history-api-export-guide) — your permanent archive

2. bulk_unlike(liked_ids, dry_run=False) — remove all your likes

3. bulk_unretweet(rt_ids, dry_run=False) — remove all your retweets

4. bulk_delete(own_tweet_ids, dry_run=False) — remove all your tweets

5. Optional: run list_liked_tweet_ids + list_retweet_source_ids again to verify zero remaining

At 10K likes + 10K retweets + 10K tweets total: read cost ~$4.50 across three enumerations, write cost per docs.x.com pricing for each DELETE endpoint (verify current tier pricing), wall-clock ~12 hours at 1.5-sec pacing. Run overnight; sleep well knowing the archive is safe.

06 — Section

Side-by-side — 3 paths to bulk unlike + unretweet

PathAuthPer-action costProgrammaticBest for
X official API + scriptOAuth user-contextsee docs.x.com/x-api/getting-started/pricing (varies by tier)yesany scale, recurring workflows, full account cleanup
Third-party web tools (Redact, Circleboom)UI auth$0 within cap, then $5-15/moUI batch onlyone-off cleanups < 3K actions
Browser-extension scriptsyour X session$0 (your time)fragiletech-savvy users, small batches, some ToS risk

The API path is the only programmatic + reliable path at scale. Free web tools cap free-tier at small monthly batches (typically ~3K/mo for Redact, similar for Circleboom); past that, paid plans apply. Browser extensions work by scrolling + clicking in the UI — slow, fragile (UI changes break them), and some implementations violate X's developer terms.

07 — Section

Rate limits + pacing — what 'sustainable' means

Per docs.x.com/x-api/fundamentals/rate-limits, DELETE endpoints share the standard user-context rate-limit windows (typically a low-hundreds count per 15-minute window at Basic tier; higher at Pro/Enterprise). Practical operational pacing:

1-2 seconds between calls is the safe baseline. Faster and you'll trip the window; slower and you're wasting wall-clock.

Catch tweepy.TooManyRequests and back off 60 seconds + jitter. The code above shows the pattern.

Interleave unlike + unretweet + delete in the same session to keep the pipeline busy without stacking pressure on one endpoint.

Overnight runs are normal for 10K+ action counts. Log everything; check the log next morning; re-run any failed entries.

python
# End-to-end unlike + unretweet with shared archive + audit log.
import os, time, random, json
import tweepy, requests
from datetime import datetime, timezone

TAPI_HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
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"],
)

HANDLE = os.environ["X_HANDLE"]
DRY_RUN = True  # always start safe
RUN_ID = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')

def paged_get(path: str, param_name: str = "userName") -> list[dict]:
    """Generic paginated GET against twitterapi.io."""
    out, cursor = [], None
    while True:
        params = {param_name: HANDLE}
        if cursor: params["cursor"] = cursor
        r = requests.get(f"https://api.twitterapi.io{path}",
                         headers=TAPI_HEADERS, params=params, timeout=15)
        r.raise_for_status()
        resp = r.json()
        out.extend(resp.get("tweets", []))
        cursor = resp.get("next_cursor")
        if not cursor: break
    return out

def execute_action(action_name: str, ids: list[str], func):
    log = open(f"cleanup_{action_name}_{RUN_ID}.jsonl", "a")
    success, fail = 0, 0
    for tid in ids:
        entry = {"id": tid, "action": action_name, "at": datetime.now(timezone.utc).isoformat()}
        if DRY_RUN:
            entry["result"] = "dry_run"
        else:
            try:
                func(tid)
                entry["result"] = "ok"
                success += 1
            except tweepy.TooManyRequests:
                entry["result"] = "rate_limited"
                log.write(json.dumps(entry) + "\n")
                time.sleep(60 + random.uniform(0, 5))
                continue
            except Exception as e:
                entry["result"] = f"failed: {e}"
                fail += 1
        log.write(json.dumps(entry) + "\n")
        time.sleep(1.0 + random.uniform(0.1, 0.5))
    log.close()
    return success, fail

liked = [t["id"] for t in paged_get("/twitter/user/liked_tweets")]
retweets = [t.get("referenced_tweet_id") or t["id"]
            for t in paged_get("/twitter/user/retweets")]

print(f"targets: {len(liked)} likes + {len(retweets)} retweets")
print(f"list-read cost: ${(len(liked) + len(retweets)) * 0.00015:.4f}")
print(f"Estimated wall-clock: ~{(len(liked) + len(retweets)) * 1.5 / 60:.0f} min at 1.5s pacing")

if not DRY_RUN:
    unlike_s, unlike_f = execute_action("unlike", liked,
        lambda tid: client.unlike(tweet_id=tid))
    unrt_s, unrt_f = execute_action("unretweet", retweets,
        lambda tid: client.unretweet(source_tweet_id=tid))
    print(f"Unlike: {unlike_s} ok, {unlike_f} failed")
    print(f"Unretweet: {unrt_s} ok, {unrt_f} failed")
08 — Questions

Questions readers ask

Can I bulk unlike all tweets I've ever liked via the X API?

Yes — use DELETE /2/users/{id}/likes/{tweet_id} in a loop after enumerating your liked tweets via /2/users/{id}/liked_tweets or twitterapi.io's /twitter/user/liked_tweets. Pace at 1-2 sec per call for sustainable throughput. There's no dedicated 'unlike all' single-call endpoint — every unlike is a separate DELETE.

How do I undo all my retweets programmatically?

Enumerate via /twitter/user/retweets (twitterapi.io) or /2/users/{id}/tweets?tweet.fields=referenced_tweets (X official, filter type=='retweeted'), extract the source_tweet_id for each, then loop DELETE /2/users/{id}/retweets/{source_tweet_id} with the same pacing pattern. Runnable Python in Step 3 above.

Do I need OAuth 2.0 or OAuth 1.0a for these delete endpoints?

Both work per docs.x.com/x-api docs. OAuth 2.0 user-context is the modern recommendation. OAuth 1.0a is still supported. tweepy handles either — set consumer_key + consumer_secret (app credentials) plus access_token + access_token_secret (user credentials via 3-legged flow).

Is bulk unlike/unretweet against X's terms of service?

Doing it via the official API against your own account is a standard supported workflow. The API exists for exactly this. Terms concerns arise mainly when: (a) you're using DIY browser scripts that hit rate limits + get flagged as automation, (b) you're doing it against other users' accounts (you can't — auth scopes it to yourself), (c) you're using unofficial third-party tools that stored your credentials insecurely.

What's the practical rate-limit for these DELETE endpoints?

Per docs.x.com/x-api/fundamentals/rate-limits, DELETE operations share the standard user-context rate-limit windows — practical baseline is 1-2 sec pacing per call. For a 10K action count, that's ~4-5 hours wall-clock. At Basic tier, expect to hit TooManyRequests occasionally; catch it, sleep 60 sec + jitter, continue. Higher tiers get proportionally higher windows.

Can I re-like or re-retweet what I've bulk-unliked?

Individually, yes — the tweet still exists (unless the author deleted it), so you can POST a fresh like or retweet via POST /2/users/{id}/likes or /2/users/{id}/retweets. But 'bulk re-like everything I just unliked' requires keeping the ID list from the audit log — the API doesn't remember what you used to like. This is why the archive-before-execute pattern matters.

How does this compare to Redact or Circleboom for the same workflow?

Redact and Circleboom offer UI-based bulk unlike + unretweet within free-tier caps (typically ~3K actions/month), scaling to paid plans past that. The API path has no cap and gives you full control over the archive format, but you write the code + take responsibility for pacing. For a one-off small cleanup (< 3K actions), the web tools are faster; for anything larger or recurring, the API is the path.

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
    Bulk Unlike + Unretweet on X API — Guide | TwitterAPI.io