Mass Delete Tweets — Bulk API Workflow + Free Tool Options
Mass deleting your own tweets — clearing an old account before re-use, removing pre-rebrand content, post-incident cleanup, or a full 'delete all my tweets' account reset — is one of the most common dev-adjacent workflows on X. The honest answer in 2026: there is no truly free programmatic mass-delete option that scales. X's API is consumption-based, and deleting a post is billed as Content: Manage, $0.005 per request; the third-party 'free tweet deleters' (Redact, TweetDeleter, TweetEraser, Circleboom) work only in small batches (typically ~3K/month) before paid tiers kick in.
This guide walks the API workflow with runnable Python: fetch tweet IDs (via advanced_search or user timeline), loop deletes with rate-limit pacing, dry-run + audit log per attempt. The free / cheap tier reality is at the bottom — what 'free' actually means, and when a mass delete crosses from 'small one-off' into 'API-only territory'.
The straight answer — no truly free path at scale
Four options for bulk delete in 2026:
1. X official API — DELETE /2/tweets/{id}, billed as Content: Manage at $0.005 per request per docs.x.com pricing. Programmatic, reliable, requires OAuth user-context auth. Best for any meaningful volume.
2. Third-party 'free' deleters (TweetDeleter, TweetEraser, similar) — usually free for small batches (a few hundred tweets) then paid. Web-UI based, may use X auth on your behalf. Useful for one-off small cleanups; doesn't scale.
3. Roll-your-own via OAuth + DELETE — same X official endpoint, but you write the code. No different cost — you're paying X per delete regardless of who's calling the endpoint.
4. twitterapi.io POST /twitter/delete_tweet_v2 — $0.0006 per call plus a $0.10 login per session, no X developer app. It runs through a logged-in session of your own X account; Step 2b below has the details and the code.
The myth of 'free bulk delete' usually points at option 2 with a hidden batch cap. The honest, scalable answers are options 1 and 4, both with a budget. If you only need to fix one recent post rather than clear many, deleting is not your only option: see how to edit a tweet for the edit window and its limits.
Step 1 — list the tweet IDs to delete
Most bulk-delete workflows have a target set: 'all tweets before 2024', 'tweets containing certain keywords', 'every tweet'. Fetch the IDs via the user timeline + filter client-side.
Via X official: /2/users/{id}/tweets with paginated cursor. Via twitterapi.io: /twitter/user/last_tweets or /twitter/tweet/advanced_search?query=from:<your_handle>.
import os, requests
HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
BASE = "https://api.twitterapi.io"
def list_my_tweet_ids(handle: str, before_date: str = None) -> list[str]:
"""Get tweet IDs to delete via advanced_search (cheap read)."""
ids = []
cursor = None
while True:
query = f"from:{handle}"
if before_date:
query += f" until:{before_date}"
params = {"query": query}
if cursor:
params["cursor"] = cursor
r = requests.get(
f"{BASE}/twitter/tweet/advanced_search",
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
old_tweets = list_my_tweet_ids("your_handle", before_date="2024-01-01")
print(f"found {len(old_tweets)} tweets to delete")
# Cost: ~$0.00015 × len(old_tweets) — read is cheap; delete is the bill
Step 2 — execute the deletes with rate-limit safety
Once you have the ID list, loop the DELETE calls. X publishes rate limits; pacing 1-2 seconds between deletes is the operational baseline.
Per docs.x.com/x-api/getting-started/pricing, deleting a post is billed as Content: Manage, $0.005 per request. 1,000-tweet cleanup = 1,000 × $0.005 = $5. 10,000-tweet cleanup = 10,000 × $0.005 = $50.
# 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_delete(tweet_ids: list[str], dry_run: bool = True):
"""Delete tweets with audit log + rate-limit safety."""
log_path = f"delete_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, "at": datetime.now(timezone.utc).isoformat()}
if dry_run:
entry["action"] = "dry_run"
log.write(json.dumps(entry) + "\n")
continue
try:
client.delete_tweet(id=tid)
entry["action"] = "deleted"
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"failed: {e}"
fail += 1
log.write(json.dumps(entry) + "\n")
time.sleep(1.0 + random.uniform(0.1, 0.5)) # safe pacing
return {"success": success, "fail": fail, "log": log_path}
# Always run dry_run=True first to verify the target set
# result = bulk_delete(old_tweets, dry_run=False)
Step 2b — the same deletes through twitterapi.io (/twitter/delete_tweet_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.0006 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/delete_tweet_v2 takes a JSON body with three required fields: login_cookies, tweet_id and proxy, plus your key in the X-API-Key header. The response has a status field (success or error); check it on every call instead of assuming the tweet is gone. Store login_cookies and reuse it across runs, so you pay for a login only when the session stops working.
Price, side by side: $0.005 per delete on the official API (Content: Manage) vs $0.0006 here (0.005 / 0.0006 = about 8.3x), plus the $0.10 login, which is one per session and not one per delete. For 1,000 deletes that is 1,000 × $0.005 = $5.00 official vs 1,000 × $0.0006 + $0.10 = $0.70 here. For a small job the official side is cheaper in total: 10 deletes cost 10 × $0.005 = $0.05 there and 10 × $0.0006 + $0.10 = $0.106 here. The two totals cross at 23 deletes per login.
The script mirrors Step 2: a dry run first, one log line per attempt, a 1-2 second pause between calls, and a stop after five failures in a row so you can read the log before going on.
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 bulk_delete_v2(tweet_ids: list[str], login_cookies: str,
dry_run: bool = True, max_consecutive_errors: int = 5) -> dict:
"""Delete tweets one call at a time, with a pause and a log line per tweet."""
stamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
log_path = f"delete_v2_log_{stamp}.jsonl"
ok = failed = streak = 0
with open(log_path, "a", encoding="utf-8") as log:
for tweet_id in tweet_ids:
entry = {"tweet_id": tweet_id, "action": "delete",
"at": datetime.now(timezone.utc).isoformat()}
if dry_run:
entry["result"] = "dry_run"
log.write(json.dumps(entry) + "\n")
continue
try:
r = requests.post(
f"{BASE}/twitter/delete_tweet_v2",
headers=HEADERS,
json={"login_cookies": login_cookies,
"tweet_id": tweet_id,
"proxy": PROXY},
timeout=60,
)
data = r.json()
if r.ok and data.get("status") == "success":
entry["result"] = "deleted"
ok += 1
streak = 0
else:
entry["result"] = f"error: http {r.status_code} {data.get('message')}"
failed += 1
streak += 1
except (requests.RequestException, ValueError) as exc:
entry["result"] = f"error: {exc}"
failed += 1
streak += 1
log.write(json.dumps(entry) + "\n")
log.flush()
if streak >= max_consecutive_errors:
print("too many errors in a row, stopping; read the log first")
break
time.sleep(1.0 + random.uniform(0.0, 1.0)) # 1-2 s between calls
return {"deleted": ok, "failed": failed, "log": log_path}
# tweet_ids.json: the ID list from Step 1, saved with json.dump(old_tweets, open("tweet_ids.json", "w"))
tweet_ids = json.load(open("tweet_ids.json"))
print(f"{len(tweet_ids)} deletes would cost about ${len(tweet_ids) * 0.0006:.2f} plus one $0.10 login")
print(bulk_delete_v2(tweet_ids, login_cookies="", dry_run=True)) # writes the log only
# When the dry-run log looks right:
# cookies = os.environ.get("X_LOGIN_COOKIES") or login()
# print(bulk_delete_v2(tweet_ids, cookies, dry_run=False))
Free / cheap alternatives — what 'free' actually means
Third-party tools advertised as 'free tweet deleter':
TweetDeleter, TweetEraser, Circleboom, Redact (formerly TweetDelete) — web UIs that bulk-delete via the user's X auth. Free tier typically caps at a small monthly batch (e.g. 3,000 tweets/mo); past that, paid plans apply.
Browser extensions — DIY scripts that programmatically scroll your timeline and click delete in the UI. Slow + fragile (UI changes break them). Some violate X's terms of service depending on implementation.
'Permanent free' deleters — usually have hidden batch caps or sustainable-volume limits. Try one for a small test before relying on it for a 10K cleanup.
The honest tradeoff: free options work for small one-time cleanups (< 1,000 tweets); paid / API options are the path for any meaningful scale or recurring workflow.
Side-by-side comparison — 4 paths to bulk delete
Two practical patterns: (a) the two API paths are the programmatic + reliable ones at scale; (b) free options are good for one-off small jobs, not for recurring workflows.
Cost framing + scaling
Math from docs.x.com pricing, where a post delete is billed as Content: Manage at $0.005 per request:
100 tweet cleanup = 100 × $0.005 = $0.50
1,000 tweet cleanup = 1,000 × $0.005 = $5.00
10,000 tweet cleanup = 10,000 × $0.005 = $50.00
The same volumes through twitterapi.io at $0.0006 per delete plus one $0.10 login: 100 deletes = 100 × $0.0006 + $0.10 = $0.16, 1,000 = $0.70, 10,000 = $6.10.
Plus the cost of listing target IDs — at twitterapi.io's $0.00015/tweet read, that's negligible (1,000 IDs to read = $0.15).
Rate-limit pacing means a 10K-tweet cleanup takes ~6 hours of wall-clock at 1.5 sec/delete. Plan accordingly.
Archive before you delete — the safe mass-delete workflow
Deletes are permanent. There is no undo API, no soft-delete recovery, no 30-day trash bin. Before a mass delete of any meaningful volume — especially a full 'delete all tweets' account reset — export the target set first. Storage is cheap; regret is not.
The 3-stage safe pattern — export → verify → delete:
Stage 1 · Export to local JSONL. Loop /twitter/tweet/advanced_search?query=from:<your_handle> with the next_cursor and write each tweet object to a file. At twitterapi.io's $0.00015/tweet read pricing, exporting 10,000 tweets costs $1.50 — one-off, worth it. See /blog/twitter-history-api-export-guide for the full export walk.
Stage 2 · Verify the export locally. Open the JSONL, scan for anything you don't want to lose (media URLs, quote-tweet contexts, reply threads). Media URLs in the export point at pbs.twimg.com CDN — download the ones you care about now, because a deleted tweet's media eventually 404s.
Stage 3 · Delete via the loop from Step 2 above. Same bulk_delete() code, or bulk_delete_v2() from Step 2b — the archive is your safety net if you regret anything.
For account-reset scale (10K+ tweets, 'delete all my tweets'): export first, keep the JSONL for at least 90 days, THEN delete. This is the same pattern the reputable web tools use internally (Redact and Circleboom both offer 'export before delete'); the API path just gives you full control over the archive format + storage location.
If the goal is to step away from the whole account rather than prune posts, deactivating a Twitter (X) account is a different operation with a 30-day window; the deactivation guide covers the timeline, what persists, and when the handle is released.
# Stage 1 — export to JSONL BEFORE running bulk_delete()
import os, json, requests
HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
BASE = "https://api.twitterapi.io"
def export_all_tweets(handle: str, out_path: str = "tweets_archive.jsonl") -> int:
"""Full-account export before mass-delete. Cost ~$0.00015 per tweet."""
cursor = None
count = 0
with open(out_path, "w") as f:
while True:
params = {"query": f"from:{handle}"}
if cursor:
params["cursor"] = cursor
r = requests.get(
f"{BASE}/twitter/tweet/advanced_search",
headers=HEADERS, params=params, timeout=15,
)
r.raise_for_status()
resp = r.json()
for t in resp.get("tweets", []):
f.write(json.dumps(t) + "\n")
count += 1
cursor = resp.get("next_cursor")
if not cursor:
break
return count
count = export_all_tweets("your_handle")
print(f"exported {count} tweets to tweets_archive.jsonl")
print(f"estimated cost: ${count * 0.00015:.4f} (read-only)")
# NOW verify tweets_archive.jsonl locally before running bulk_delete() from Step 2
# Practical example: end-to-end old-tweet cleanup with dry-run + 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
def list_targets(before_date: str):
ids = []
cursor = None
while True:
params = {"query": f"from:{HANDLE} until:{before_date}"}
if cursor: params["cursor"] = cursor
r = requests.get(
"https://api.twitterapi.io/twitter/tweet/advanced_search",
headers=TAPI_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
def bulk_delete(ids):
log_path = f"delete_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 ids:
entry = {"tweet_id": tid, "at": datetime.now(timezone.utc).isoformat()}
if DRY_RUN:
entry["action"] = "dry_run"
else:
try:
client.delete_tweet(id=tid)
entry["action"] = "deleted"
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"failed: {e}"
fail += 1
log.write(json.dumps(entry) + "\n")
time.sleep(1.0 + random.uniform(0.1, 0.5))
return success, fail, log_path
targets = list_targets("2024-01-01")
print(f"found {len(targets)} tweets to delete")
print(f"Estimated cost (X official): ${len(targets) * 0.005:.2f}")
print(f"Time at 1.5s pace: ~{len(targets) * 1.5 / 60:.0f} minutes")
if not DRY_RUN:
s, f, p = bulk_delete(targets)
print(f"Done: {s} deleted, {f} failed. Log: {p}")
Questions readers ask
How do I delete all my tweets programmatically?
The 'delete all tweets' workflow is a special case of mass-delete without a date filter: (1) fetch every tweet ID via /twitter/tweet/advanced_search?query=from:<your_handle> looping the next_cursor until exhaustion; (2) run the bulk_delete() loop from the code sample above with DRY_RUN=False; (3) monitor the audit log. At 10K tweets that's 10,000 × $0.005 = $50 (X API pricing, Content: Manage) + ~4 hours wall-clock at 1.5s pacing. Export first (see FAQ 'What about archiving before delete?') — the delete is permanent.
Can I delete tweets through twitterapi.io instead of the official API?
Yes. POST /twitter/delete_tweet_v2 takes login_cookies, tweet_id and proxy and costs $0.0006 per call, against $0.005 per delete on the official API (Content: Manage). You get login_cookies from one POST /twitter/user_login_v2 call ($0.10 per login, one per session). The delete is made as your own X account through a proxy you supply, so the conditions in Step 2b apply; for fewer than 23 deletes on a fresh login the official route costs less in total.
Is there really no free mass-delete API in 2026?
Not at scale. X's API is consumption-based with no free tier in 2026. Third-party 'free' tools (Redact / TweetDeleter / TweetEraser / Circleboom) work for small one-off cleanups (typically ~3,000 tweets/month cap on free tier) but don't scale to recurring or large workflows. The honest tradeoff: pay for the API or accept the batch caps.
Can I undo a bulk delete?
No — tweet deletes are permanent. There is no undelete API. Run dry_run=True first to verify your target set, and consider exporting the tweets you're about to delete first (see /blog/twitter-history-api-export-guide for the export workflow).
Will X suspend my account for bulk deleting?
Bulk-deleting your own tweets via the API is a standard supported workflow — not against X's terms. The risk is rate-limit violations if you go too fast. Pace 1-2 seconds between deletes; don't burst.
Can I delete other users' tweets?
No — you can only delete your own tweets. OAuth user-context auth scopes the call to the authenticated account. Reporting other users' content is a separate moderation workflow, not the delete API.
What about archiving before delete?
Strongly recommended for any meaningful cleanup. Export the tweets first via twitterapi.io advanced_search to JSONL (single-digit-dollar cost), then run the deletion. Archive lives outside X regardless of what happens to your account.
How long does a 10K-tweet bulk delete take?
At 1.5 second pacing per delete: 10,000 × 1.5s = 15,000 seconds = ~4.2 hours of wall-clock time. Plus any rate-limit pauses. Run overnight, monitor for completion.
Continue
- Twitter (X) API — cluster hub
- Mass unfollow Twitter (X) API tutorial
- Twitter (X) history API — export timeline
- Rate Limit Exceeded on Twitter (X) — Fixes
- twitterapi.io pricing
- How to edit a tweet on X: who can, for how long, and the workarounds
- Deactivating a Twitter (X) account: the 30-day timeline and what persists
- 01How to Build an MCP Server in Python for Twitter (X) Data
- 02OpenClaw vs Claude Code: which agent should run your Twitter (X) jobs?
- 03Hermes Agent vs OpenClaw: Which Agent Fits Twitter (X) Work?
- 04Claude Code Channels: Telegram, Discord and Twitter (X) Alerts in Your Session
- 05Codex MCP: How to Add Any MCP Server to OpenAI Codex (CLI, IDE and App)
- 06Get Twitter (X) Data in Python (2026 Tutorial)
- 07Twitter (X) Bulk Unfollow + Follower Remover — API Guide
- 08Mass Unfollow on Twitter (X) — A Developer's API Tutorial
Stop reading. Start building.
Starter credits cover real testing on real data. Google sign-in, no card, no application queue.
Get an API keyAlready building with TwitterAPI.io? Publish an honest tutorial or build log about it and earn up to $200 a month in API credits.