A more in-depth take a look at X’s API: fetching data, linking entities, and solving under-fetching.
When designing a system’s API, software engineers often evaluate various approaches, similar to REST vs RPC vs GraphQL, or hybrid models, to find out the most effective fit for a selected task or project. These approaches define how data flows between the backend and frontend, in addition to the structure of the response data:
- Should all data be packed right into a single “batch” and returned in a single response?
- Can the “batch” be configured to incorporate only the required fields for a selected client (e.g., browser vs. mobile) to avoid over-fetching?
- What happens if the client under-fetches data and requires additional backend calls to retrieve missing entities?
- How should parent-child relationships be handled? Should child entities be embedded inside their parent, or should normalization be applied, where parent entities only reference child entity IDs to enhance reusability and reduce response size?
In this text, we explore how the X (formerly Twitter) home timeline API (x.com/home) addresses these challenges, including:
- Fetching the list of tweets
- Returning hierarchical or linked data (e.g., tweets, users, media)
- Sorting and paginating results
- Retrieving tweet details
- Liking a tweet
Our focus might be on the API design and functionality, treating the backend as a black box since its implementation is inaccessible.
Showing the precise requests and responses here is likely to be cumbersome and hard to follow for the reason that deeply nested and repetitive objects are hard to read. To make it easier to see the request/response payload structure, I’ve made my try to “type out” the house timeline API in TypeScript. So on the subject of the request/response examples I’ll use the request and response types as a substitute of actual JSON objects. Also, keep in mind that the kinds are simplified and lots of properties are omitted for brevity.
Chances are you’ll find every kind in types/x.ts file or at the underside of this text within the “Appendix: Every kind at one place” section.
All images, unless othewise noted, are by the creator.
Fetching the list of tweets for the house timeline starts with the POST
request to the next endpoint:
POST https://x.com/i/api/graphql/{query-id}/HomeTimeline
Here’s a simplified request body type:
type TimelineRequest = {
queryId: string; // 's6ERr1UxkxxBx4YundNsXw'
variables: {
count: number; // 20
cursor?: string; // 'DAAACgGBGedb3Vx__9sKAAIZ5g4QENc99AcAAwAAIAIAAA'
seenTweetIds: string[]; // ['1867041249938530657', '1867041249938530659']
};
features: Features;
};type Features = {
articles_preview_enabled: boolean;
view_counts_everywhere_api_enabled: boolean;
// ...
}
Here’s a simplified response body type (we’ll dive deeper into the response sub-types below):
type TimelineResponse = {
data: {
home: {
home_timeline_urt: {
instructions: (TimelineAddEntries | TimelineTerminateTimeline)[];
responseObjects: {
feedbackActions: TimelineAction[];
};
};
};
};
};type TimelineAddEntries = TimelineModule)[];
;
type TimelineItem = {
entryId: string; // 'tweet-1867041249938530657'
sortIndex: string; // '1866561576636152411'
content: {
__typename: 'TimelineTimelineItem';
itemContent: TimelineTweet;
feedbackInfo: {
feedbackKeys: ActionKey[]; // ['-1378668161']
};
};
};
type TimelineTweet = {
__typename: 'TimelineTweet';
tweet_results: {
result: Tweet;
};
};
type TimelineCursor = {
entryId: string; // 'cursor-top-1867041249938530657'
sortIndex: string; // '1866961576813152212'
content: 'Bottom';
;
};
type ActionKey = string;
It’s interesting to notice here, that “getting” the information is completed via “POSTing”, which shouldn’t be common for the REST-like API but it surely is common for a GraphQL-like API. Also, the graphql
a part of the URL indicates that X is using the GraphQL flavor for his or her API.
I’m using the word “flavor” here since the request body itself doesn’t seem like a pure GraphQL query, where we may describe the required response structure, listing all of the properties we would like to fetch:
# An example of a pure GraphQL request structure that's *not* getting used within the X API.
{
tweets {
id
description
created_at
medias {
kind
url
# ...
}
creator {
id
name
# ...
}
# ...
}
}
The belief here is that the house timeline API shouldn’t be a pure GraphQL API, but is a mixture of several approaches. Passing the parameters in a POST request like this seems closer to the “functional” RPC call. But at the identical time, it looks as if the GraphQL features is likely to be used somewhere on the backend behind the HomeTimeline endpoint handler/controller. A mixture like this may additionally be brought on by a legacy code or some type of ongoing migration. But again, these are only my speculations.
Chances are you’ll also notice that the identical TimelineRequest.queryId
is utilized in the API URL in addition to within the API request body. This queryId is likely generated on the backend, then it gets embedded within the most important.js
bundle, after which it’s used when fetching the information from the backend. It is tough for me to know how this queryId
is used exactly since X’s backend is a black box in our case. But, again, the speculation here is likely to be that, it is likely to be needed for some type of performance optimization (re-using some pre-computed query results?), caching (Apollo related?), debugging (join logs by queryId?), or tracking/tracing purposes.
It is usually interesting to notice, that the TimelineResponse
comprises not an inventory of tweets, but relatively an inventory of instructions, like “add a tweet to the timeline” (see the TimelineAddEntries
type), or “terminate the timeline” (see the TimelineTerminateTimeline
type).
The TimelineAddEntries
instruction itself may additionally contain several types of entities:
- Tweets — see the
TimelineItem
type - Cursors — see the
TimelineCursor
type - Conversations/comments/threads — see the
TimelineModule
type
type TimelineResponse = {
data: {
home: {
home_timeline_urt: TimelineTerminateTimeline)[]; // <-- Here
// ...
;
};
};
};type TimelineAddEntries = TimelineModule)[]; // <-- Here
;
That is interesting from the extendability standpoint because it allows a greater diversity of what might be rendered in the house timeline without tweaking the API an excessive amount of.
The TimelineRequest.variables.count
property sets what number of tweets we would like to fetch without delay (per page). The default is 20. Nevertheless, greater than 20 tweets might be returned within the TimelineAddEntries.entries
array. For instance, the array might contain 37 entries for the primary page load, since it includes tweets (29), pinned tweets (1), promoted tweets (5), and pagination cursors (2). I’m unsure why there are 29 regular tweets with the requested count of 20 though.
The TimelineRequest.variables.cursor
is accountable for the cursor-based pagination.
“Cursor pagination is most frequently used for real-time data as a result of the frequency recent records are added and since when reading data you regularly see the newest results first. It eliminates the potential for skipping items and displaying the identical item greater than once. In cursor-based pagination, a continuing pointer (or cursor) is used to maintain track of where in the information set the following items ought to be fetched from.” See the Offset pagination vs Cursor pagination thread for the context.
When fetching the list of tweets for the primary time the TimelineRequest.variables.cursor
is empty, since we would like to fetch the highest tweets from the default (likely pre-computed) list of personalized tweets.
Nevertheless, within the response, together with the tweet data, the backend also returns the cursor entries. Here is the response type hierarchy: TimelineResponse → TimelineAddEntries → TimelineCursor
:
type TimelineResponse = {
data: {
homet: {
home_timeline_urt: TimelineTerminateTimeline)[]; // <-- Here
// ...
;
};
};
};type TimelineAddEntries = TimelineModule)[]; // <-- Here (tweets + cursors)
;
type TimelineCursor = {
entryId: string;
sortIndex: string;
content: 'Bottom';
;
};
Every page comprises the list of tweets together with “top” and “bottom” cursors:
After the page data is loaded, we are able to go from the present page in each directions and fetch either the “previous/older” tweets using the “bottom” cursor or the “next/newer” tweets using the “top” cursor. My assumption is that fetching the “next” tweets using the “top” cursor happens in two cases: when the brand new tweets were added while the user remains to be reading the present page, or when the user starts scrolling the feed upwards (and there are not any cached entries or if the previous entries were deleted for the performance reasons).
The X’s cursor itself might seem like this: DAABCgABGemI6Mk__9sKAAIZ6MSYG9fQGwgAAwAAAAIAAA
. In some API designs, the cursor could also be a Base64 encoded string that comprises the id of the last entry within the list, or the timestamp of the last seen entry. For instance: eyJpZCI6ICIxMjM0NTY3ODkwIn0= --> {"id": "1234567890"}
, after which, this data is used to question the database accordingly. Within the case of X API, it looks just like the cursor is being Base64 decoded into some custom binary sequence that may require some further decoding to get any meaning out of it (i.e. via the Protobuf message definitions). Since we do not know whether it is a .proto
encoding and in addition we do not know the .proto
message definition we may assume that the backend knows the best way to query the following batch of tweets based on the cursor string.
The TimelineResponse.variables.seenTweetIds
parameter is used to tell the server about which tweets from the currently energetic page of the infinite scrolling the client has already seen. This likely helps be sure that the server doesn’t include duplicate tweets in subsequent pages of results.
One among the challenges to be solved within the APIs like home timeline (or Home Feed) is to work out the best way to return the linked or hierarchical entities (i.e. tweet → user
, tweet → media
, media → creator
, etc):
- Should we only return the list of tweets first after which fetch the dependent entities (like user details) in a bunch of separate queries on-demand?
- Or should we return all the information without delay, increasing the time and the scale of the primary load, but saving the time for all subsequent calls?
- Do we want to normalize the information on this case to scale back the payload size (i.e. when the identical user is an creator of many tweets and we would like to avoid repeating the user data over and yet again in each tweet entity)?
- Or should it’s a mixture of the approaches above?
Let’s see how X handles it.
Earlier within the TimelineTweet
type the Tweet
sub-type was used. Let’s have a look at the way it looks:
export type TimelineResponse = {
data: {
home: {
home_timeline_urt: TimelineTerminateTimeline)[]; // <-- Here
// ...
;
};
};
};type TimelineAddEntries = TimelineModule)[]; // <-- Here
;
type TimelineItem = {
entryId: string;
sortIndex: string;
content: {
__typename: 'TimelineTimelineItem';
itemContent: TimelineTweet; // <-- Here
// ...
};
};
type TimelineTweet = {
__typename: 'TimelineTweet';
tweet_results: {
result: Tweet; // <-- Here
};
};
// A Tweet entity
type Tweet = {
__typename: 'Tweet';
core: {
user_results: {
result: User; // <-- Here (a dependent User entity)
};
};
legacy: {
full_text: string;
// ...
entities: { // <-- Here (a dependent Media entities)
media: Media[];
hashtags: Hashtag[];
urls: Url[];
user_mentions: UserMention[];
};
};
};
// A User entity
type User = {
__typename: 'User';
id: string; // 'VXNlcjoxNDUxM4ADSG44MTA4NDc4OTc2'
// ...
legacy: {
location: string; // 'San Francisco'
name: string; // 'John Doe'
// ...
};
};
// A Media entity
type Media = {
// ...
source_user_id_str: string; // '1867041249938530657' <-- Here (the dependant user is being mentioned by its ID)
url: string; // 'https://t.co/X78dBgtrsNU'
features: {
large: { faces: FaceGeometry[] };
medium: { faces: FaceGeometry[] };
small: { faces: FaceGeometry[] };
orig: { faces: FaceGeometry[] };
};
sizes: {
large: MediaSize;
medium: MediaSize;
small: MediaSize;
thumb: MediaSize;
};
video_info: VideoInfo[];
};
What’s interesting here is that the majority of the dependent data like tweet → media
and tweet → creator
is embedded into the response on the primary call (no subsequent queries).
Also, the User
and Media
connections with Tweet
entities should not normalized (if two tweets have the identical creator, their data might be repeated in each tweet object). However it looks as if it ought to be okay, since within the scope of the house timeline for a selected user the tweets might be authored by many authors and repetitions are possible but sparse.
My assumption was that the UserTweets
API (that we do not cover here), which is accountable for fetching the tweets of one particular user will handle it otherwise, but, apparently, it shouldn’t be the case. The UserTweets
returns the list of tweets of the identical user and embeds the identical user data over and yet again for every tweet. It’s interesting. Possibly the simplicity of the approach beats some data size overhead (perhaps user data is taken into account pretty small in size). I’m unsure.
One other remark concerning the entities’ relationship is that the Media
entity also has a link to the User
(the creator). However it does it not via direct entity embedding because the Tweet
entity does, but relatively it links via the Media.source_user_id_str
property.
The “comments” (that are also the “tweets” by their nature) for every “tweet” in the house timeline should not fetched in any respect. To see the tweet thread the user must click on the tweet to see its detailed view. The tweet thread might be fetched by calling the TweetDetail
endpoint (more about it within the “Tweet detail page” section below).
One other entity that every Tweet
has is FeedbackActions
(i.e. “Recommend less often” or “See fewer”). The way in which the FeedbackActions
are stored within the response object is different from the way in which the User
and Media
objects are stored. While the User
and Media
entities are a part of the Tweet
, the FeedbackActions
are stored individually in TimelineItem.content.feedbackInfo.feedbackKeys
array and are linked via the ActionKey
. That was a slight surprise for me because it doesn’t appear to be the case that any motion is re-usable. It looks like one motion is used for one particular tweet only. So it looks as if the FeedbackActions
could possibly be embedded into each tweet in the identical way as Media
entities. But I is likely to be missing some hidden complexity here (just like the undeniable fact that each motion can have children actions).
More details concerning the actions are within the “Tweet actions” section below.
The sorting order of the timeline entries is defined by the backend via the sortIndex
properties:
type TimelineCursor = {
entryId: string;
sortIndex: string; // '1866961576813152212' <-- Here
content: 'Bottom';
;
};type TimelineItem = {
entryId: string;
sortIndex: string; // '1866561576636152411' <-- Here
content: {
__typename: 'TimelineTimelineItem';
itemContent: TimelineTweet;
feedbackInfo: {
feedbackKeys: ActionKey[];
};
};
};
type TimelineModule = {
entryId: string;
sortIndex: string; // '73343543020642838441' <-- Here
content: {
__typename: 'TimelineTimelineModule';
items: {
entryId: string,
item: TimelineTweet,
}[],
displayType: 'VerticalConversation',
};
};
The sortIndex
itself might look something like this '1867231621095096312'
. It likely corresponds on to or is derived from a Snowflake ID.
Actually many of the IDs you see within the response (tweet IDs) follow the “Snowflake ID” convention and seem like
'1867231621095096312'
.
If that is used to sort entities like tweets, the system leverages the inherent chronological sorting of Snowflake IDs. Tweets or objects with the next sortIndex value (a more moderen timestamp) appear higher within the feed, while those with lower values (an older timestamp) appear lower within the feed.
Here’s the step-by-step decoding of the Snowflake ID (in our case the sortIndex
) 1867231621095096312
:
- Extract the Timestamp:
- The timestamp is derived by right-shifting the Snowflake ID by 22 bits (to remove the lower 22 bits for data center, employee ID, and sequence):
1867231621095096312 → 445182709954
- Add Twitter’s Epoch:
- Adding Twitter’s custom epoch (1288834974657) to this timestamp gives the UNIX timestamp in milliseconds:
445182709954 + 1288834974657 → 1734017684611ms
- Convert to a human-readable date:
- Converting the UNIX timestamp to a UTC datetime gives:
1734017684611ms → 2024-12-12 15:34:44.611 (UTC)
So we are able to assume here that the tweets in the house timeline are sorted chronologically.
Each tweet has an “Actions” menu.
The actions for every tweet are coming from the backend in a TimelineItem.content.feedbackInfo.feedbackKeys
array and are linked with the tweets via the ActionKey
:
type TimelineResponse = {
data: {
home: {
home_timeline_urt: {
instructions: (TimelineAddEntries | TimelineTerminateTimeline)[];
responseObjects: {
feedbackActions: TimelineAction[]; // <-- Here
};
};
};
};
};type TimelineItem = {
entryId: string;
sortIndex: string;
content: {
__typename: 'TimelineTimelineItem';
itemContent: TimelineTweet;
feedbackInfo: {
feedbackKeys: ActionKey[]; // ['-1378668161'] <-- Here
};
};
};
type TimelineAction = {
key: ActionKey; // '-609233128'
value: 'Not taken with this post' ;
};
It’s interesting here that this flat array of actions is definitely a tree (or a graph? I didn’t check), since each motion could have child actions (see the TimelineAction.value.childKeys
array). This is sensible, for instance, when after the user clicks on the “Don’t Like” motion, the follow-up is likely to be to point out the “This post isn’t relevant” motion, as a way of explaining why the user doesn’t just like the tweet.
Once the user would really like to see the tweet detail page (i.e. to see the thread of comments/tweets), the user clicks on the tweet and the GET
request to the next endpoint is performed:
GET https://x.com/i/api/graphql/{query-id}/TweetDetail?variables={"focalTweetId":"1867231621095096312","referrer":"home","controller_data":"DACABBSQ","rankingMode":"Relevance","includePromotedContent":true,"withCommunity":true}&features={"articles_preview_enabled":true}
I used to be curious here why the list of tweets is being fetched via the POST
call, but each tweet detail is fetched via the GET
call. Seems inconsistent. Especially keeping in mind that similar query parameters like query-id
, features
, and others this time are passed within the URL and never within the request body. The response format can be similar and is re-using the kinds from the list call. I’m unsure why is that. But again, I’m sure I is likely to be is likely to be missing some background complexity here.
Listed below are the simplified response body types:
type TweetDetailResponse = {
data: {
threaded_conversation_with_injections_v2: TimelineTerminateTimeline)[],
,
},
}type TimelineAddEntries = TimelineModule)[];
;
type TimelineTerminateTimeline = {
type: 'TimelineTerminateTimeline',
direction: 'Top',
}
type TimelineModule = {
entryId: string; // 'conversationthread-58668734545929871193'
sortIndex: string; // '1867231621095096312'
content: {
__typename: 'TimelineTimelineModule';
items: {
entryId: string, // 'conversationthread-1866876425669871193-tweet-1866876038930951193'
item: TimelineTweet,
}[], // Comments to the tweets are also tweets
displayType: 'VerticalConversation',
};
};
The response is pretty similar (in its types) to the list response, so we won’t for too long here.
One interesting nuance is that the “comments” (or conversations) of every tweet are literally other tweets (see the TimelineModule
type). So the tweet thread looks very much like the house timeline feed by showing the list of TimelineTweet
entries. This looks elegant. An excellent example of a universal and re-usable approach to the API design.
When a user likes the tweet, the POST
request to the next endpoint is being performed:
POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet
Here is the request body types:
type FavoriteTweetRequest = {
variables: {
tweet_id: string; // '1867041249938530657'
};
queryId: string; // 'lI07N61twFgted2EgXILM7A'
};
Here is the response body types:
type FavoriteTweetResponse = {
data: {
favorite_tweet: 'Done',
}
}
Looks straightforward and in addition resembles the RPC-like approach to the API design.
We now have touched on some basic parts of the house timeline API design by X’s API example. I made some assumptions along the solution to the most effective of my knowledge. I consider some things I might need interpreted incorrectly and I might need missed some complex nuances. But even with that in mind, I hope you bought some useful insights from this high-level overview, something that you might apply in your next API Design session.
Initially, I had a plan to undergo similar top-tech web sites to get some insights from Facebook, Reddit, YouTube, and others and to gather battle-tested best practices and solutions. I’m unsure if I’ll find the time to try this. Will see. However it could possibly be an interesting exercise.
For the reference, I’m adding every kind in a single go here. Chances are you’ll also find every kind in types/x.ts file.
/**
* This file comprises the simplified types for X's (Twitter's) home timeline API.
*
* These types are created for exploratory purposes, to see the present implementation
* of the X's API, to see how they fetch Home Feed, how they do a pagination and sorting,
* and the way they pass the hierarchical entities (posts, media, user info, etc).
*
* Many properties and kinds are omitted for simplicity.
*/// POST https://x.com/i/api/graphql/{query-id}/HomeTimeline
export type TimelineRequest = {
queryId: string; // 's6ERr1UxkxxBx4YundNsXw'
variables: {
count: number; // 20
cursor?: string; // 'DAAACgGBGedb3Vx__9sKAAIZ5g4QENc99AcAAwAAIAIAAA'
seenTweetIds: string[]; // ['1867041249938530657', '1867041249938530658']
};
features: Features;
};
// POST https://x.com/i/api/graphql/{query-id}/HomeTimeline
export type TimelineResponse = {
data: {
home: {
home_timeline_urt: {
instructions: (TimelineAddEntries | TimelineTerminateTimeline)[];
responseObjects: {
feedbackActions: TimelineAction[];
};
};
};
};
};
// POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet
export type FavoriteTweetRequest = {
variables: {
tweet_id: string; // '1867041249938530657'
};
queryId: string; // 'lI07N6OtwFgted2EgXILM7A'
};
// POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet
export type FavoriteTweetResponse = {
data: {
favorite_tweet: 'Done',
}
}
// GET https://x.com/i/api/graphql/{query-id}/TweetDetail?variables={"focalTweetId":"1867041249938530657","referrer":"home","controller_data":"DACABBSQ","rankingMode":"Relevance","includePromotedContent":true,"withCommunity":true}&features={"articles_preview_enabled":true}
export type TweetDetailResponse = {
data: {
threaded_conversation_with_injections_v2: TimelineTerminateTimeline)[],
,
},
}
type Features = {
articles_preview_enabled: boolean;
view_counts_everywhere_api_enabled: boolean;
// ...
}
type TimelineAction = {
key: ActionKey; // '-609233128'
value: 'Not taken with this post' ;
};
type TimelineAddEntries = TimelineModule)[];
;
type TimelineTerminateTimeline = {
type: 'TimelineTerminateTimeline',
direction: 'Top',
}
type TimelineCursor = {
entryId: string; // 'cursor-top-1867041249938530657'
sortIndex: string; // '1867231621095096312'
content: 'Bottom';
;
};
type TimelineItem = {
entryId: string; // 'tweet-1867041249938530657'
sortIndex: string; // '1867231621095096312'
content: {
__typename: 'TimelineTimelineItem';
itemContent: TimelineTweet;
feedbackInfo: {
feedbackKeys: ActionKey[]; // ['-1378668161']
};
};
};
type TimelineModule = {
entryId: string; // 'conversationthread-1867041249938530657'
sortIndex: string; // '1867231621095096312'
content: {
__typename: 'TimelineTimelineModule';
items: {
entryId: string, // 'conversationthread-1867041249938530657-tweet-1867041249938530657'
item: TimelineTweet,
}[], // Comments to the tweets are also tweets
displayType: 'VerticalConversation',
};
};
type TimelineTweet = {
__typename: 'TimelineTweet';
tweet_results: {
result: Tweet;
};
};
type Tweet = {
__typename: 'Tweet';
core: {
user_results: {
result: User;
};
};
views: {
count: string; // '13763'
};
legacy: {
bookmark_count: number; // 358
created_at: string; // 'Tue Dec 10 17:41:28 +0000 2024'
conversation_id_str: string; // '1867041249938530657'
display_text_range: number[]; // [0, 58]
favorite_count: number; // 151
full_text: string; // "How I'd promote my startup, if I had 0 followers (Part 1)"
lang: string; // 'en'
quote_count: number;
reply_count: number;
retweet_count: number;
user_id_str: string; // '1867041249938530657'
id_str: string; // '1867041249938530657'
entities: {
media: Media[];
hashtags: Hashtag[];
urls: Url[];
user_mentions: UserMention[];
};
};
};
type User = {
__typename: 'User';
id: string; // 'VXNlcjoxNDUxM4ADSG44MTA4NDc4OTc2'
rest_id: string; // '1867041249938530657'
is_blue_verified: boolean;
profile_image_shape: 'Circle'; // ...
legacy: {
following: boolean;
created_at: string; // 'Thu Oct 21 09:30:37 +0000 2021'
description: string; // 'I help startup founders double their MRR with outside-the-box marketing cheat sheets'
favourites_count: number; // 22195
followers_count: number; // 25658
friends_count: number;
location: string; // 'San Francisco'
media_count: number;
name: string; // 'John Doe'
profile_banner_url: string; // 'https://pbs.twimg.com/profile_banners/4863509452891265813/4863509'
profile_image_url_https: string; // 'https://pbs.twimg.com/profile_images/4863509452891265813/4863509_normal.jpg'
screen_name: string; // 'johndoe'
url: string; // 'https://t.co/dgTEddFGDd'
verified: boolean;
};
};
type Media = {
display_url: string; // 'pic.x.com/X7823zS3sNU'
expanded_url: string; // 'https://x.com/johndoe/status/1867041249938530657/video/1'
ext_alt_text: string; // 'Image of two bridges.'
id_str: string; // '1867041249938530657'
indices: number[]; // [93, 116]
media_key: string; // '13_2866509231399826944'
media_url_https: string; // 'https://pbs.twimg.com/profile_images/1867041249938530657/4863509_normal.jpg'
source_status_id_str: string; // '1867041249938530657'
source_user_id_str: string; // '1867041249938530657'
type: string; // 'video'
url: string; // 'https://t.co/X78dBgtrsNU'
features: {
large: { faces: FaceGeometry[] };
medium: { faces: FaceGeometry[] };
small: { faces: FaceGeometry[] };
orig: { faces: FaceGeometry[] };
};
sizes: {
large: MediaSize;
medium: MediaSize;
small: MediaSize;
thumb: MediaSize;
};
video_info: VideoInfo[];
};
type UserMention = {
id_str: string; // '98008038'
name: string; // 'Yann LeCun'
screen_name: string; // 'ylecun'
indices: number[]; // [115, 122]
};
type Hashtag = {
indices: number[]; // [257, 263]
text: string;
};
type Url = {
display_url: string; // 'google.com'
expanded_url: string; // 'http://google.com'
url: string; // 'https://t.co/nZh3aF0Aw6'
indices: number[]; // [102, 125]
};
type VideoInfo = {
aspect_ratio: number[]; // [427, 240]
duration_millis: number; // 20000
variants: 'video/mp4' ;
};
type FaceGeometry = { x: number; y: number; h: number; w: number };
type MediaSize = 'crop' ;
type ActionKey = string;