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

OSINT on Twitter (X): What Public Data You Can Pull, and How

By TwitterAPI.io Team•17 min read
Three-column diagram mapping seven OSINT questions about a Twitter (X) account (how old is it, is this the same account after a rename, where is it based, who does it follow, when does it post, when exactly was this posted, what did it say in a date range) to the public data point that answers each (createdAt, user ID, location and account-based-in label, followings and followers lists, post createdAt, snowflake tweet ID, search operators) and to the twitterapi.io endpoint or free tool that returns it.
The map this page follows: an investigative question, the public X data point that answers it, and where that data point comes from.

Most pages that rank for "osint twitter" are either a tree of tool links (the OSINT Framework, Bellingcat's toolkit) or a course outline about search operators. Both are useful, and both stop at the same point: they tell you a tool exists, not which field in X's data answers your question, what that field looks like, or what it costs to pull a thousand of them. This page is written from the other end. It starts with the questions investigators, analysts, trust-and-safety teams and journalists actually ask about an X account, and for each one names the data point, the free tool on this site that shows it for a single handle, and the API endpoint and response field that return it in bulk.

Everything here is public data: fields X displays on a profile or in search to anyone who looks. Nothing requires logging into an X account, nothing touches protected accounts or direct messages, and nothing on this page is about taking over, impersonating or interfering with anyone's account. The last section covers the lines that matter: X's terms, data minimisation, and the difference between a signal and a finding.

The code is Python, uses the X-API-Key header, checks the msg field on every error, and runs as written once TWITTERAPI_IO_KEY is set. Where the brief truth is "you cannot get this" (deleted posts, private data, a user's real-time position), the page says so rather than pointing at a tool that claims otherwise.

01 — Section

The question-to-endpoint map: what public X data answers which OSINT question

An X account exposes a fixed set of public facts. Each row below is one question an investigator asks, the data point that answers it, the free tool on this site that shows it for one handle, and the API route for doing it at scale. Endpoint paths are relative to https://api.twitterapi.io; every one of them is a GET with the X-API-Key header and returns JSON.

QuestionData pointFree tool (single lookup)Endpoint and field (bulk)
How old is this account?Creation timestampAccount age checker/twitter/user/info -> data.createdAt
Is this the same account I saw under another handle?Numeric user ID (never changes)ID finder/twitter/user/info -> data.id; /twitter/user/batch_info_by_ids to resolve IDs back to current handles
How many times has it been renamed?Username change countAccount age checker/twitter/user_about -> data.about_profile.username_changes.count
Where does it say it is, and where does X think it is?Profile location string; account-based-in labelAccount location checker/twitter/user/info -> data.location; /twitter/user_about -> about_profile.account_based_in, location_accurate
Who does it follow, who follows it?Followings and followers lists with full profiles-/twitter/user/followings, /twitter/user/followers -> screen_name, followers_count, created_at per user
When does it post?createdAt on each post, binned by hour-/twitter/user/last_tweets -> data.tweets[].createdAt
When exactly was this post published?Snowflake timestamp inside the tweet IDTweet timestamp toolArithmetic on the ID: (id >> 22) + 1288834974657 ms; or /twitter/tweets -> tweets[].createdAt
What did it post about X between two dates?Search results-/twitter/tweet/advanced_search with from:handle since:YYYY-MM-DD until:YYYY-MM-DD
Who replied to or quoted a post?Reply and quote lists-/twitter/tweet/replies, /twitter/tweet/quotes, /twitter/tweet/retweeters
Is this post or account still there?Current state as X serves itAccount age checker (shows suspended or missing)/twitter/user/info returns status: "error" with msg: "user not found"; /twitter/tweets returns an empty tweets list for a deleted ID

Two things the table does not contain, because X does not expose them publicly: a user's real-time position (X removed precise coordinates from its apps in 2019 and the location string is free text), and anything about a protected account beyond the fact that it is protected. The posts of protected accounts are not returned by any read endpoint here.

02 — Section

Account age, creation date and the user ID that outlives every handle change

The two most useful facts about an account are also the two least expensive to get: the creation timestamp and the numeric user ID. Both come from one call, GET /twitter/user/info?userName=<handle>, which puts the profile in data with camelCase fields: id, userName, name, createdAt, followers, following, statusesCount, location, description, isBlueVerified, protected, isAutomated (twitterapi.io docs, verified 2026). Profiles are billed at $0.18 per 1,000, so a watch-list of 500 handles costs $0.09 to refresh.

Creation date. data.createdAt is the moment the account was registered. On its own it answers "is this a fresh account?"; across a set of accounts it answers "were these created together?", which is the usual first step when a cluster of profiles pushes the same message. A posts-per-day lifetime average (statusesCount divided by age in days) is a second cheap signal from the same call. Treat both as leads: a brand-new account can be a real person who just joined, and an old one can be bought or renamed into a copy of someone else. For a single handle the free account age checker on this site shows the exact timestamp, the age in years and days and the username change count without an API key.

User ID. data.id is the string of digits X assigned at registration. X's own help page on changing a username states that a change does not affect followers, direct messages or replies, which is another way of saying the account underneath is the same object; the ID is that object's name. Store IDs, not handles, in any case file: x.com/i/user/<id> opens the profile whatever it is called today, and GET /twitter/user/batch_info_by_ids?userIds=<id1>,<id2> returns the current userName for a list of IDs in one call. The free ID finder converts in both directions for one handle at a time. The reverse lookup is what lets you prove that the handle a screenshot shows and the handle in front of you today are the same account, or that they are not.

Rename history. GET /twitter/user_about?userName=<handle> returns the panel X shows under About this account: about_profile.username_changes.count, the account_based_in region, a location_accurate flag and an affiliate_username for organisation-affiliated accounts. X rolled this panel out globally in November 2025 (announced by its head of product on X). The change count tells you an account has been renamed; it does not tell you the previous names, which X does not publish.

03 — Section

Reading the time out of a tweet ID: the Snowflake formula

Every post, user and direct message ID on X is a 64-bit Snowflake ID, and the high bits of a Snowflake ID are a timestamp. X's developer documentation describes the IDs as unsigned 64-bit integers composed of a timestamp, a worker number and a sequence number, which makes them roughly time-ordered: higher IDs are newer objects (docs.x.com, X IDs). The original generator, published by Twitter and archived at github.com/twitter-archive/snowflake, states the layout: 41 bits of milliseconds since a custom epoch, 10 bits of machine ID (5 datacenter + 5 worker in IdWorker.scala), and a 12-bit sequence. The custom epoch in that file is twepoch = 1288834974657, which is 2010-11-04 01:42:54.657 UTC.

The formula: unix_ms = (tweet_id >> 22) + 1288834974657. Shift the ID right by 22 bits (12 sequence + 5 worker + 5 datacenter) to drop the machine and sequence fields, add the epoch, and you have the publication time in milliseconds, with no API call and no rate limit. Worked example: ID 2109793921203093888 shifted right by 22 is 503014068890; add the epoch and you get 1791849043547 ms, which is 2026-10-12 23:50:43.547 UTC. The same arithmetic works on user IDs created after November 2010, which gives you a second, independent estimate of an account's creation time to check createdAt against.

Two caveats. IDs at or below 29700859247 predate Snowflake and are plain sequential numbers, so they carry no time; the script returns None for them. And the timestamp is when X assigned the ID, which for a scheduled post is the publish moment, not the moment the author wrote it. For bulk work paste a list of URLs or IDs into the free tweet timestamp tool on this site, which does the conversion, shows your local time next to UTC and exports CSV; for code, the function below is the whole thing.

python
"""Tweet ID -> UTC time, with no API call. The epoch and the 22-bit shift come from
twitter-archive/snowflake (IdWorker.scala): twepoch = 1288834974657, shift = 12 + 5 + 5."""
from datetime import datetime, timezone

TWEPOCH_MS = 1288834974657
LAST_PRE_SNOWFLAKE_ID = 29700859247   # older IDs are sequential and carry no time


def tweet_time(tweet_id):
    tid = int(tweet_id)
    if tid <= LAST_PRE_SNOWFLAKE_ID:
        return None                        # posted before Snowflake went live
    ms = (tid >> 22) + TWEPOCH_MS
    return datetime.fromtimestamp(ms / 1000, tz=timezone.utc)


for tid in ["20", "1288834974657000000", "2109793921203093888"]:
    t = tweet_time(tid)
    print(tid, "->", t.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + " UTC" if t else "pre-Snowflake, no timestamp")
04 — Section

Behavioural signals: the follow graph and posting-time patterns

Two endpoints return the graph. GET /twitter/user/followers?userName=<handle> returns the account's followers newest-first, and GET /twitter/user/followings?userName=<handle> returns who it follows; both page with cursor and pageSize (up to 200), both return has_next_page and next_cursor at the top level, and both carry a full profile per user in snake_case: id, screen_name, name, followers_count, friends_count, statuses_count, created_at, location, description, protected, verified (twitterapi.io response shapes, verified 2026). Note the casing: these lists use screen_name and created_at, while /twitter/user/info uses userName and createdAt. Followers and followings are billed at 1 to 3 credits per user returned, so a full first page of 200 costs between $0.002 and $0.006.

The graph questions practitioners ask are set operations: which accounts do two suspects both follow, which of an account's newest followers were themselves created this month, and which accounts in a follower list share a creation week and an empty bio. All of them key on the permanent id, never on the handle. Keep the raw lists as CSV with the ID as the first column; graph tools such as Gephi or Maltego import a two-column edge list directly, and the ID column is what lets you merge a list pulled today with one pulled next month after half the handles have changed.

The newest-first ordering of /twitter/user/followers is itself a signal: the first page is the most recent follows, which is where a sudden purchase of followers or a coordinated follow wave shows up as a run of accounts with adjacent creation dates. A follower audit that scores that first page is the subject of the Twitter audit guide linked below.

Posting-time patterns. GET /twitter/user/last_tweets?userName=<handle>&includeReplies=true returns the account's most recent posts, newest first, with the list nested at data.tweets and the pagination fields has_next_page and next_cursor at the top level. Each post carries createdAt in the classic format Tue Dec 10 07:00:30 +0000 2024, always with a +0000 offset, so parsing with %a %b %d %H:%M:%S %z %Y and reading .hour gives UTC hours directly. Bin a few hundred posts by hour and you get a histogram like the one below. Hour-of-day histograms are commonly used by practitioners to estimate the time zone an account operates in and to spot accounts that post around the clock with no sleep gap; the inference is weak for scheduled or multi-operator accounts, and a quiet band can be a work shift rather than a night, so calibrate it against accounts whose time zone you already know before you rely on it. Timeline pages are billed at $0.15 per 1,000 posts, so 400 posts (20 pages) cost $0.06.

python
"""Who do two accounts both follow? Shared followings are a common link signal in OSINT work.
Followings come back newest-first in snake_case: screen_name, followers_count, created_at.
Cost: 1 to 3 credits per account returned, so two 200-account pages cost well under $0.02."""
import os

import requests

HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}


def followings(handle, pages=1):
    out, cursor = {}, ""
    for _ in range(pages):
        r = requests.get("https://api.twitterapi.io/twitter/user/followings",
                         params={"userName": handle, "cursor": cursor, "pageSize": 200},
                         headers=HEADERS, timeout=30)
        r.raise_for_status()
        body = r.json()
        if body.get("status") == "error":
            raise SystemExit(f"{handle}: {body.get('msg')}")
        for u in body.get("followings") or []:
            out[u["id"]] = u          # keyed by permanent user ID, not by handle
        if not body.get("has_next_page"):
            break
        cursor = body["next_cursor"]
    return out


a, b = followings("NASA"), followings("ESA")
shared = set(a) & set(b)
print(f"{len(shared)} accounts followed by both (first page each)")
for uid in sorted(shared, key=lambda k: -(a[k].get("followers_count") or 0))[:15]:
    u = a[uid]
    print(f"@{u['screen_name']:<18} followers={u.get('followers_count'):<10} joined={u.get('created_at')}")
Bar chart of posts per hour of day in UTC for a single hypothetical X account, with a quiet band of near-zero bars across several consecutive hours and a peak in the afternoon; labelled as an illustrative distribution, not measured data.
Illustrative distribution of posts by UTC hour for one hypothetical account, drawn to show the shape the script prints. Not measured data; the quiet band is the feature practitioners read as a possible sleep window.
05 — Section

Search operators for investigators: author, date, content class and location

GET /twitter/tweet/advanced_search takes a query string in the same grammar as the X search box, a queryType of Latest or Top, and a cursor (empty string for the first page). Each page returns up to 20 posts in a top-level tweets array with has_next_page and next_cursor; every post carries id, createdAt, text, likeCount, retweetCount, replyCount, viewCount, isReply, source and an author object with userName, id, location and createdAt. Billing is $0.15 per 1,000 posts returned with a 15-credit ($0.00015) minimum per request, so a full page costs $0.003 and an empty one $0.00015.

OperatorWhat it doesOSINT use
from:handle / to:handlePosts by, or replies to, an accountEverything an account said in a window; everyone who replied to it
@handlePosts mentioning an accountWho talks about the target, including from accounts the target never engaged
since:YYYY-MM-DD until:YYYY-MM-DDDate bounds, UTCReconstruct a day; since_time: / until_time: take Unix seconds for finer bounds (the _HH:MM:SS_UTC suffix form is not supported)
-filter:retweets, -filter:repliesExclude retweets or repliesOriginal statements only
filter:links, filter:media, filter:imagesPosts with a link, any media, imagesFind the sources an account shares; collect imagery for verification
url:example.comPosts linking to a domainWho spreads a specific site
min_faves:N, min_retweets:NEngagement floorOnly the posts that travelled
lang:xxLanguage classificationSeparate an account's languages; cheap authorship signal
"exact phrase", OR, -word, (a OR b) cBoolean grammarNarrative tracking across many accounts in one query
near:"City" within:15mi, geocode:lat,lon,radiusLegacy location operatorsBest effort only: not in X's current operator reference and often ignored; run a control query without them

Keep queries under 512 characters: X's own operator reference gives 512 as the self-serve limit for recent search and 1,024 for the full archive (docs.x.com, verified 2026), and in practice that is about 20 from: clauses OR-ed together. The official reference documents from:, to:, url:, lang:, is:retweet, has:geo, place: and point_radius: for its own API; the search-box grammar accepted here is broader, including since: / until:, min_faves: and the filter: family, and the full list is on the search operators reference linked below.

Location. Four location signals exist and none is complete: a Place tag on the post (rare), the free-text profile location string on every author object, the account-based-in label from /twitter/user_about, and place names in the text. For an account you can read the first three from the responses above; for a search, bin results by author.location rather than trusting a near: operator. The search-by-location guide linked below walks through all four, with the control-query check that tells you when an operator was silently ignored.

06 — Section

Deleted and edited posts: what the API can and cannot return

This is the question every OSINT page gets asked and most answer badly, so here is the plain version: no read endpoint on twitterapi.io, and no endpoint on the official X API, returns a post that X no longer serves. When you call GET /twitter/tweets?tweet_ids=<id> for a deleted post the call succeeds and tweets is empty; when you call GET /twitter/user/info for a deleted or suspended account the call returns status: "error" and msg: "user not found" with data null, and never an HTTP 404. A suspended account that still exists comes back with unavailable set. Anything that claims to "recover" a deleted post is reading a copy someone saved earlier: a web archive, a quote-post that embedded the text, or a scheduled capture.

Edited posts are similar. X allows up to five edits inside a short window after posting (one hour per the X help center; the X API documentation describes each edit as creating a new post ID and lists the chain in edit_history_tweet_ids). The tweet objects returned here are the version X currently serves and do not include that chain, so a post you fetch after an edit is the edited one. If the original wording matters, you need the copy you took before the edit.

The only reliable record is therefore one you make yourself, on a schedule, before the post disappears. The simplest form is a search bound to the last run: from:handle since_time:<last_run_unix> on /twitter/tweet/advanced_search, stored as JSON with the post id, createdAt, text, the author.id and the snowflake time you computed. A check that finds nothing costs the 15-credit minimum, $0.00015, so polling one query every 5 minutes for a month is $1.30 and every 60 seconds is $6.48, plus $0.15 per 1,000 posts actually captured. Capturing the id matters more than it looks: a later GET /twitter/tweets on a stored ID that now returns nothing is itself evidence that the post was removed between the two timestamps. For what remains visible after an account is deleted, deactivated or suspended, see the guide on finding deleted Twitter accounts linked below.

07 — Section

Three ways to get the same data: X by hand, the official X API, TwitterAPI.io

Every data point on this page can be reached three ways. The differences are setup, cost at volume, which operators you can use and how far back you can go.

X web UI, by handOfficial X API (pay-per-usage)TwitterAPI.io
SetupAn X account you are logged into; screenshots and copy-pasteDeveloper account, an approved app, OAuth, a card on file in the developer consoleAn API key from the dashboard; no X account, no OAuth
CostFree in money, expensive in hours; no export$0.005 per post read, $0.010 per user read, capped at 3 million post reads a month before an Enterprise contract (docs.x.com pricing, verified 2026)$0.15 per 1,000 posts, $0.18 per 1,000 profiles, 1 to 3 credits per follower or following returned; 15-credit minimum per request
The dossier in this guide (1 profile + 20 timeline posts + 20 search results)About 15 minutes of clicking1 x $0.010 + 40 x $0.005 = $0.21$0.00018 + $0.003 + $0.003 = about $0.006
1,000 profiles (a watch-list refresh)Not practical$10.00$0.18
1,000 posts from searchNot practical$5.00$0.15 (about 33x less: $5.00 / $0.15)
OperatorsThe full search-box grammar, including since: / until:, min_faves:, filter:The documented v2 operators only (from:, to:, url:, lang:, is:retweet, has:geo, place:, point_radius:); 512-character queries on self-serveThe search-box grammar; near: / geocode: best effort; keep under 512 characters
Historical depthWhatever search shows, one scroll at a timeRecent search by default; full-archive search depends on access levelAs far back as X's search index serves the query, paginated with cursor; timelines newest-first with cursor
RateManualPer-endpoint limits set by X0.2 requests/s before the first payment, 20 once paid, 50 on Scale, 500 on Business

The ratios are arithmetic from the two published price lists, not a benchmark. What the official route buys you is official support and the documented operator set; what it costs you is the approval step and about 33 times the price per post. The by-hand route stays the right one for a single account you will look at once; the moment the question becomes "these 200 accounts, every week", it is an API job, and the dossier script below is the starting point.

08 — Section

Ethics and lawful use: public data, X's terms, and the line between a signal and a finding

Public data only. Every field on this page is one X displays to anyone viewing the profile or running a search. Nothing here reads direct messages, protected accounts' posts, email addresses or phone numbers, and the read endpoints cannot: a protected account's timeline is simply not returned. If a method needs a password, a session cookie from someone else's account, or a phishing page, it is not OSINT and it is not covered here.

No account takeover, no impersonation, no interference. Reading public data needs no X login. Do not log into the account you are investigating, do not create look-alike accounts to approach it, and do not use what you find to contact, harass or dox the person behind it. Keep automated reads at a sensible pace; the rate limits above are the ceiling, not a target.

Respect X's terms and the law where you operate. X's Terms of Service and developer rules govern access to the platform, and data protection law (the GDPR for EU residents' data, state laws in the US) applies to personal data regardless of how public it was. Collect the minimum the question needs, record why you collected it, delete what you do not need, and keep a chain of custody (timestamp, source URL, raw JSON) for anything that may be used as evidence.

A signal is not a finding. A creation date, a quiet band in a histogram, a shared set of followings, or a profile location that matches a city are each consistent with several explanations. Corroborate across independent signals and outside sources before attributing an account to a person, and never publish an attribution you cannot defend. The tools on this page make the data cheap; the judgement is still yours.

python
"""Profile dossier for one public X account, built from three read endpoints.

1. /twitter/user/info        -> creation date, user ID, followers, post count, location
2. /twitter/user/last_tweets -> posting-hour histogram (UTC) from each post's createdAt
3. /twitter/tweet/advanced_search -> a dated sample: from:<handle> since:<date>

Needs TWITTERAPI_IO_KEY in the environment (key from twitterapi.io/dashboard).
Cost at the defaults: one profile ($0.00018) + one timeline page (up to 20 posts,
$0.003) + one search page (up to 20 posts, $0.003), about $0.006 in total.
Reads only: no X login, nothing is posted, liked or followed.
"""
import os
import sys
from collections import Counter
from datetime import datetime, timezone

import requests

HANDLE = sys.argv[1] if len(sys.argv) > 1 else "NASA"
SINCE = sys.argv[2] if len(sys.argv) > 2 else "2026-01-01"   # YYYY-MM-DD, UTC
HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
TWEET_DATE = "%a %b %d %H:%M:%S %z %Y"   # createdAt on posts, e.g. Tue Dec 10 07:00:30 +0000 2024
TWEPOCH_MS = 1288834974657               # Snowflake custom epoch (twitter-archive/snowflake)


def get(path, params):
    """HTTP errors raise here. Endpoints that wrap the result (user/info, last_tweets)
    also report failures as status == "error" with the reason in msg; checked per call below."""
    r = requests.get("https://api.twitterapi.io" + path, params=params, headers=HEADERS, timeout=30)
    r.raise_for_status()
    return r.json()


def parse_profile_date(value):
    """Profile createdAt has been served as ISO and as the classic format; accept both."""
    try:
        return datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError:
        return datetime.strptime(value, TWEET_DATE)


def snowflake_time(tweet_id):
    return datetime.fromtimestamp(((int(tweet_id) >> 22) + TWEPOCH_MS) / 1000, tz=timezone.utc)


# 1. Profile: the facts that do not change with a rename
profile = get("/twitter/user/info", {"userName": HANDLE})
if profile.get("status") == "error" or not profile.get("data"):
    raise SystemExit(f"@{HANDLE}: {profile.get('msg')}")   # e.g. "user not found": suspended, renamed or deleted
user = profile["data"]
created = parse_profile_date(user["createdAt"])
age_days = (datetime.now(timezone.utc) - created).days
print(f"@{user['userName']}  user id {user['id']}  (x.com/i/user/{user['id']})")
print(f"created {created:%Y-%m-%d %H:%M} UTC  ({age_days} days ago)")
print(f"followers {user.get('followers'):,}  following {user.get('following'):,}  "
      f"posts {user.get('statusesCount'):,}  location {user.get('location')!r}")
print(f"lifetime average {user.get('statusesCount', 0) / max(age_days, 1):.1f} posts/day")

# 2. Timeline: when does this account post? (UTC hours, one page = up to 20 posts)
timeline = get("/twitter/user/last_tweets", {"userName": HANDLE, "includeReplies": "true"})
if timeline.get("status") == "error":
    raise SystemExit(f"last_tweets: {timeline.get('msg')}")
tweets = (timeline.get("data") or {}).get("tweets") or timeline.get("tweets") or []
hours = Counter(datetime.strptime(t["createdAt"], TWEET_DATE).hour for t in tweets if t.get("createdAt"))
print(f"\nposting hours (UTC) across {len(tweets)} recent posts:")
for h in range(24):
    print(f"{h:02d}:00 {'#' * hours.get(h, 0)}")
if tweets:
    newest = tweets[0]
    print(f"newest post id {newest['id']} -> snowflake time {snowflake_time(newest['id']):%Y-%m-%d %H:%M:%S} UTC")

# 3. Dated search sample: what did the account post on or after SINCE?
#    advanced_search has no status/msg wrapper: the page is tweets + has_next_page + next_cursor.
search = get("/twitter/tweet/advanced_search",
             {"query": f"from:{HANDLE} since:{SINCE} -filter:retweets", "queryType": "Latest", "cursor": ""})
sample = search.get("tweets") or []
print(f"\n{len(sample)} posts from:{HANDLE} since:{SINCE} on the first page "
      f"(has_next_page={search.get('has_next_page')})")
for t in sample[:5]:
    print(f"  {t['createdAt']}  likes={t.get('likeCount')}  {t.get('text', '')[:70]!r}")
09 — Questions

Questions readers ask

What is OSINT on Twitter (X)?

Open-source intelligence on X is the practice of answering an investigative question from data X already shows the public: an account's creation date, numeric user ID, profile and region labels, follower and following lists, posting times, the timestamps encoded in post IDs, and anything search operators can retrieve. It does not include private data, protected accounts or any form of account access.

How do I find when a Twitter (X) account was created?

For one handle, use the free account age checker on this site; it shows the exact creation timestamp, the age in days and the username change count. Programmatically, call GET /twitter/user/info?userName=<handle> and read data.createdAt. Accounts created after November 2010 also carry the creation time inside their numeric user ID, via the Snowflake formula.

Does a Twitter (X) user ID change when the username changes?

No. X's help page on changing a username states that followers, direct messages and replies are unaffected; the account underneath keeps its numeric ID. Read it from data.id on /twitter/user/info, open x.com/i/user/<id> to see the current handle, or resolve a list of IDs with /twitter/user/batch_info_by_ids.

How do I get the exact time a tweet was posted from its ID?

Tweet IDs are Snowflake IDs: shift the ID right by 22 bits and add the epoch 1288834974657 to get Unix milliseconds, so (id >> 22) + 1288834974657. The layout (41 bits of time, 10 bits of machine ID, 12 bits of sequence) and the epoch come from the archived twitter-archive/snowflake repository. IDs at or below 29700859247 predate Snowflake and carry no time. The free tweet timestamp tool on this site does this for a batch of URLs.

Can an API recover deleted tweets?

No. Both twitterapi.io and the official X API return only what X currently serves: /twitter/tweets returns an empty list for a deleted ID and /twitter/user/info returns msg: "user not found" for a deleted account. The only record of a deleted post is a copy made before deletion: a web archive, a quote-post, or your own scheduled capture with from:handle since_time:<last_run> on /twitter/tweet/advanced_search.

Which search operators work for Twitter (X) OSINT?

Through /twitter/tweet/advanced_search: from:, to:, @handle, since: / until: dates and since_time: / until_time: Unix seconds, -filter:retweets, -filter:replies, filter:links, filter:media, url:, min_faves:, min_retweets:, lang:, exact phrases and OR / - boolean grammar. near: and geocode: are legacy and best effort. Keep the query under 512 characters.

What does Twitter (X) OSINT cost through an API?

On twitterapi.io, profiles are $0.18 per 1,000, posts from search or timelines are $0.15 per 1,000 with a $0.00015 minimum per request, and followers or followings are 1 to 3 credits per user returned; the three-call dossier in this guide is about $0.006 per account. The official X API pay-per-usage plan charges $0.010 per user read and $0.005 per post read (docs.x.com, verified 2026), about $0.21 for the same dossier, after app approval.

Is Twitter (X) OSINT legal?

Reading data X displays publicly is generally lawful, but it is still bounded by X's Terms of Service and developer rules and by data protection law such as the GDPR when the data identifies a person. Collect the minimum, keep records of source and time, do not access accounts or private data, and treat every signal on this page as a lead to corroborate rather than a conclusion.

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