Skip to content

What does the Twitter API return as JSON?

Last updated August 24, 2026

Every response is a small envelope around one object or one array. List routes return tweets or users plus count and next_cursor, one page per call. Lookups return a named key instead: user/info gives you user, tweet/detail gives you id and tweet. Ids arrive as strings rather than numbers, so a 19-digit tweet id survives a JavaScript parse without precision loss.

Every rate here is the pricing TwitterAPIs publishes. The billed rate is $0.0008 per call; $0.04 per 1,000 tweets is derived from it at a full 20-tweet page, which is the default page size rather than a guaranteed yield (source: twitterapis.com/pricing).

The envelope around every payload

There is no generic data wrapper. Keys are named after what they hold, so a collection route puts its items under a name matching the type, tweets for posts and users for accounts, then adds count for the size of this page and next_cursor for the following one. A single-object route names the key after the thing it resolved: user for a profile lookup, tweet for a post lookup, and tweet/detail also echoes the id you asked for at the top level. So you cannot type one wrapper and reuse it everywhere. Type the envelope per route and share the item models underneath, which is where the real structure lives anyway.

Fields on a tweet object

A tweet carries id, text, created_at, author, favorite_count, retweet_count, reply_count and view_count. text is the full body with URLs already expanded, so there is no entities pass to run before you display it. author is nested and holds the poster's id, username, name, follower figures and verification state, which is why resolving who posted something costs no second request. The four counters are documented as values at fetch time rather than live figures. view_count is the conditional one: present when the source exposes it and absent otherwise, so parse it as optional even though the other three read as dependable integers. A quoted post, where there is one, arrives nested inside the same object rather than as a second lookup you have to make.

Fields on a user object

A full profile returns id, username, name, description, followers_count, following_count, verified and profile_image_url. username is the handle with the leading @ already stripped, description is the raw bio text, and profile_image_url points at an avatar on pbs.twimg.com. What you actually get varies by route rather than being fixed: the documented user search result carries id, username, name, description, followers_count and verified with no following_count and no avatar, and an author nested inside a thread response can be as small as an id and a username. Treat the full field set as the maximum a route may return, never as a guarantee. id is the stable field: handles change and numeric ids do not, which is why a by-id lookup sits alongside the by-username one.

Cursor fields and two different stop conditions

Paginated routes add count and next_cursor, where next_cursor is typed string or null and gets passed straight back as the cursor parameter on the following call. The stop condition is not the same everywhere, and that detail is what costs people a hanging loop. List and affiliate endpoints return null on the final page, so testing for a missing cursor works there. Follower-graph endpoints return a non-null cursor even on the last page, so the correct test is an empty collection array. Write both conditions into your paging helper rather than one, because the wrong one does not error out, it spins. The cursor value itself is opaque, a string like DAABCgABF, and there is nothing in it to parse or increment.

Nulls, optional keys and absent fields

Three different kinds of missing turn up here. A field can be typed nullable and always present, like next_cursor or the id and reason on a liveness check, where null is itself a real answer. A field can be conditionally present, like view_count, which appears only when the source exposes it. And a field can simply not be part of what a route returns, like the avatar missing from a trimmed search result. Model the first as nullable and the other two as optional, and resist defaulting an absent counter to zero, because an invented zero is indistinguishable later from a measured one. next_cursor is the clearest case of the first kind, since on most routes a null there is the end-of-data signal rather than data that went missing.

Two date formats live in this API

Post timestamps use Twitter's own format, a string reading like Tue Feb 20 14:02:11 +0000 2026, and feeding that to a strict ISO parser fails. Monitoring timestamps are ISO 8601 instead: created_at on a webhook or a monitor, last_poll_at on a health read, and detected_at and delivered_at on a delivery event. A single delivery record carries both at once, since its tweet_created_at is in the Twitter format while the timestamps around it are ISO. Parse each field with the format its own route documents rather than assuming one rule covers the whole response. The delivery record also carries three lag integers computed from those timestamps, so the arithmetic across the two formats is already done for you.

Id shapes, and why none of them are numbers

Post and account ids are numeric but typed as strings, because a 19-digit value exceeds what a JavaScript number holds exactly and a silent rounding produces an id that resolves to nothing. author.id is a string for the same reason. Management ids are not numeric at all: the documented create responses show wh_ and mon_ prefixed values, while a delivery event's own id and its monitor_id come back as UUIDs. So no id anywhere on this surface survives being parsed as an integer. Keep every one of them as text through the client, the queue and the database column. Type the nested ones as well, because a model that declares the outer id correctly and leaves the inner one numeric breaks on exactly the records you care about.

Where media lives in the payload

Attached images and video sit under extended_entities.media on the post, as an array. Each entry carries a type, photo for a still, and a media_url_https pointing at pbs.twimg.com. The block is not on every post, only on ones with attachments, which is why user/media exists: it returns exactly the timeline posts carrying one of those arrays, so pulling a profile's visual output does not mean fetching a whole timeline and filtering it yourself afterwards. Those pages are ordinary reads at $0.0008 and are cursor paginated like any other timeline route. Each entry carries its own type, so a still and a video are told apart inside the array rather than by any flag on the post, and one post can hold several entries.

Parsing into a typed model

Three rules cover most of it. Type every id as a string, including nested ones and the prefixed and UUID management ids. Mark view_count, description and profile_image_url optional rather than required, since routes trim them without warning. Keep created_at as a raw string in the model and parse it at the boundary with a formatter chosen per route. Then type the envelope per endpoint, because tweet/detail returning id plus tweet, and the full-history read returning count plus tweets with no cursor at all, are different shapes that no single generic wrapper describes honestly. Keep whatever you do not model rather than dropping it, because a route can gain a field and a parser that rejects unknown keys turns that addition into an outage. That figure is ours: $0.0008 a call, per our pricing page.

Top-level keys, route by route

RouteTop-level keysItem shape
tweet/advanced_searchtweets, count, next_cursorArray of tweet objects
user/tweetstweets, count, next_cursorArray of tweet objects
user/tweets/completecount, tweetsArray with no cursor field at all
tweet/detailid, tweetOne tweet object, id echoed above it
user/infouserOne user object
user/mediatweets, count, next_cursorTweets carrying extended_entities.media
user/statususerName, status, id, reasonFour scalars, id may be null
monitor/deliveriesdeliveries, countEvents with three lag integers each
This dictionary documents every available field for each object type.
X Developer Platform, data dictionary. Source

Questions and answers

Why are tweet ids returned as strings?
Because a 19-digit value exceeds what a JavaScript number can hold exactly, and a silent rounding turns a valid id into one that resolves to nothing. Every id field is typed string for that reason, including author.id nested inside a post. Keep them as text through your whole pipeline, including the database column, since a numeric column reintroduces the same rounding at the storage layer.
Do I have to request expansions to get the author?
No. The author object ships inside each post already, carrying id, username and name at minimum, so resolving a poster costs no extra request and no join. That differs from a shape where you request a field set and then match a separate includes block yourself. You only spend another call when you want the full profile, meaning the bio, both follow counts and the avatar.
What format is created_at in?
Twitter's own, reading like Tue Feb 20 14:02:11 +0000 2026 rather than an ISO timestamp, so parse it with a formatter that accepts that layout. The monitoring routes are the exception and use ISO 8601 for created_at, last_poll_at, detected_at and delivered_at. A delivery record carries one of each, which is the clearest reminder that the format belongs to the field, not to the API.
How do I tell the last page from a full one?
Read count and next_cursor together, and know which family you are in. List and affiliate routes set next_cursor to null when you are finished. Follower-graph routes hand back a non-null cursor even at the end, so the reliable stop there is an empty collection array. A loop that only checks for a missing cursor keeps asking a follower list for pages that do not exist.
Where are images and video in the response?
Under extended_entities.media on the post, as an array whose entries carry a type such as photo and a media_url_https on pbs.twimg.com. Not every post has the block, only ones with attachments. If media is the point of the job, user/media returns exactly the posts that carry it, which is cheaper than paging a whole timeline and discarding most of what comes back.
Are engagement counts a snapshot or live?
A snapshot. favorite_count, retweet_count, reply_count and view_count are documented as values at fetch time, so two reads of the same id minutes apart can disagree without either being wrong. Record the moment of each read beside the numbers, because without that a genuinely flat series and a collector that stopped running produce exactly the same rows in your table. view_count in particular can be absent on one read and present on the next, which is a source difference rather than a reset.
Which fields can be missing entirely?
view_count when the source does not expose it, plus anything a given route trims. The documented user search result has no following_count and no avatar, and an author nested in a thread response can be just an id and a username. Model those as optional and leave them absent rather than defaulting them, since an invented empty string reads as measured data a week later.
Can I write one generic response wrapper?
Not honestly. The top-level key is named after the payload, so a profile lookup returns user, a post lookup returns id and tweet, a timeline returns tweets with count and next_cursor, and the full-history read returns count and tweets with no cursor field. Type the envelope per route and share the tweet and user models underneath, which is where the fields you actually care about live.
What does the liveness check return that a profile lookup does not?
Four scalars: userName echoed back, status as one of alive, suspended, not_found or unavailable, id as the numeric id when the account is alive and null otherwise, and reason carrying X's own explanation when it offers one. Every outcome is an HTTP 200 and you branch on the field, where a profile lookup collapses a suspension, a deletion and a typo into one 404.
Does the full-history read hand back a cursor?
No. It returns count and tweets and nothing else, because it pages internally and gives you the slice in one response rather than one page at a time. That makes it the one read shape where a cursor loop is simply wrong: you set the max you want, you wait longer for the response, and you pay $0.0024 for the call instead of $0.0008 a page.

Start with $0.50 in free credits

No credit card. Roughly 12,500 tweets to test every endpoint.