Bulk Unlike + Unretweet on X API — Complete Guide
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.
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}/ 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.
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.
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}")
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.
# 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)
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:
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
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.
Side-by-side — 3 paths to bulk unlike + unretweet
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.
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.
# 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")
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.
Continue
- X API — Likes (docs.x.com, 2026)
- X API — Retweets (docs.x.com, 2026)
- X API — pricing (docs.x.com, 2026)
- X API — rate limits (docs.x.com, 2026)
- Tweepy documentation
- twitterapi.io — pricing
- Twitter (X) API — cluster hub
- Mass Delete Tweets — Bulk API + Free Tools
- Twitter (X) history API — export timeline
- Rate Limit Exceeded on Twitter (X) — Fixes
- Mass unfollow Twitter (X) API tutorial
- 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