Tweets
Create, delete, like, retweet, pin, schedule, and manage tweets.
tweets.create(text, opts?)
Create a new tweet. Uses the GraphQL CreateTweet mutation.
| Option | Type | Description |
|---|---|---|
text | string | The tweet text |
opts.mediaIds | string[] | Array of media IDs to attach |
opts.gif | object | Attach a GIF from search.gifs |
opts.replyTo | string | Tweet ID to reply to |
opts.quoteTweetId | string | Tweet ID to quote |
opts.sensitive | boolean | Mark media as possibly sensitive |
opts.aiGenerated | boolean | Show the "Made with AI" label |
opts.paidPartnership | boolean | Show the "Paid partnership" label |
opts.conversationControl | string | Who can reply: "ByInvitation" or "Community" |
opts.poll | object | Attach a poll |
opts.cardUri | string | A Twitter Card URI to attach (cannot be combined with poll) |
opts.variables | object | Additional GraphQL variables to merge |
const tweet = await client.tweets.create("Hello world!");
// Reply to a tweet
const reply = await client.tweets.create("Great point!", {
replyTo: "1234567890",
});
// Labelled as "Made with AI" and "Paid partnership"
await client.tweets.create("new ad creative", {
mediaIds: [mediaId],
aiGenerated: true,
paidPartnership: true,
});
// Quote tweet
const qt = await client.tweets.create("Check this out", {
quoteTweetId: "1234567890",
});
// With media
const media = await client.tweets.create("Photo time", {
mediaIds: ["9876543210"],
sensitive: false,
});
// Restrict replies
const limited = await client.tweets.create("Only mentioned can reply", {
conversationControl: "ByInvitation",
});
// With a poll
const poll = await client.tweets.create("Best language?", {
poll: {
choices: ["JavaScript", "Python", "Rust"],
duration_minutes: 1440,
},
});
// Upload media and tweet
const { media_id } = await client.media.create("./photo.jpg", {
alt_text: "A gorgeous sunset",
});
const mediaTweet = await client.tweets.create("Golden hour", {
mediaIds: [media_id],
});
// With a GIF from search
const { items } = await client.search.gifs("celebration");
const gifTweet = await client.tweets.create("🎉", { gif: items[0] });Returns: Parsed tweet object.
Polls
Create tweets with polls by passing a poll option to tweets.create().
opts.poll
| Field | Type | Required | Description |
|---|---|---|---|
choices | string[] | object[] | Yes | 2–4 choices - strings for text polls, objects for media polls |
duration_minutes | number | No | How long the poll stays open (default: 1440 = 24 h, max 7 days) |
Each choice can be:
- A string - for text-only polls (up to 25 characters)
- An object
{ label, image }- for media polls, whereimageis amedia_idfrommedia.create()
Text poll
await client.tweets.create("Tabs or spaces?", {
poll: {
choices: ["Tabs", "Spaces"],
},
});Media poll
Upload images first, then reference their media_id in each choice:
const [a, b] = await Promise.all([
client.media.create("./option-a.jpg"),
client.media.create("./option-b.jpg"),
]);
await client.tweets.create("Which design do you prefer?", {
poll: {
choices: [
{ label: "Design A", image: a.media_id },
{ label: "Design B", image: b.media_id },
],
duration_minutes: 2880, // 2 days
},
});Custom duration
// 7-day poll
await client.tweets.create("Best frontend framework in 2026?", {
poll: {
choices: ["React", "Vue", "Svelte", "Angular"],
duration_minutes: 10080,
},
});
// Quick 30-minute poll
await client.tweets.create("Should I mass-refactor right now?", {
poll: {
choices: ["Do it", "Don't you dare"],
duration_minutes: 30,
},
});Duration limits
| Min | Max | Default |
|---|---|---|
| 5 minutes | 10 080 minutes (7 days) | 1 440 minutes (24 h) |
Long tweets
You must use tweets.createNote(text, opts?) to write tweets longer than 280 characters, and your account needs Premium. This uses the GraphQL CreateNoteTweet mutation.
| Option | Type | Description |
|---|---|---|
text | string | The note tweet text |
opts.mediaIds | string[] | Array of media IDs to attach |
opts.replyTo | string | Tweet ID to reply to |
opts.sensitive | boolean | Mark media as possibly sensitive |
opts.aiGenerated | boolean | Show the "Made with AI" label |
opts.paidPartnership | boolean | Show the "Paid partnership" label |
opts.richtext_tags | array | Rich text formatting tags |
opts.variables | object | Additional GraphQL variables |
const note = await client.tweets.createNote("This is a very long note...");Returns: Parsed tweet object.
tweets.createThread(items)
Post a thread (multiple tweets chained as replies). Each item after the first automatically replies to the previous tweet.
| Param | Type | Description |
|---|---|---|
items | (string | object)[] | Array of 2+ tweets - strings for text-only, objects for full opts |
Each object item accepts the same options as tweets.create() (mediaIds, poll, sensitive, etc.) plus a text field.
// Simple text thread
const thread = await client.tweets.createThread([
"1/ Here's a mass thread about JavaScript engines",
"2/ V8 powers Chrome and Node.js",
"3/ SpiderMonkey powers Firefox",
"4/ JavaScriptCore powers Safari",
]);
// Mixed thread with media and polls
const img = await client.media.create("./diagram.png");
const thread = await client.tweets.createThread([
"Let me explain how this works 🧵",
{
text: "Here's the architecture diagram:",
mediaIds: [img.media_id],
},
{
text: "What do you think?",
poll: { choices: ["Love it", "Needs work"] },
},
]);
// Each item in the returned array is a parsed tweet object
console.log(thread[0].id); // first tweet
console.log(thread[thread.length - 1].id); // last tweetReturns: Array of parsed tweet objects, one per tweet in the thread.
tweets.delete(tweetId)
Delete a tweet. Uses the GraphQL DeleteTweet mutation.
await client.tweets.delete("1234567890");tweets.like(tweetId)
Like a tweet. Uses the GraphQL FavoriteTweet mutation.
await client.tweets.like("1234567890");tweets.unlike(tweetId)
Unlike a tweet. Uses the GraphQL UnfavoriteTweet mutation.
await client.tweets.unlike("1234567890");tweets.retweet(tweetId)
Retweet a tweet. Uses the GraphQL CreateRetweet mutation.
await client.tweets.retweet("1234567890");tweets.unretweet(tweetId)
Undo a retweet. Uses the GraphQL DeleteRetweet mutation.
await client.tweets.unretweet("1234567890");tweets.pin(tweetId)
Pin a tweet to your profile. Uses the GraphQL PinTweet mutation.
await client.tweets.pin("1234567890");tweets.unpin(tweetId)
Unpin a tweet from your profile. Uses the GraphQL UnpinTweet mutation.
await client.tweets.unpin("1234567890");tweets.get(tweetId)
Get a single tweet by ID. Uses the GraphQL TweetResultByRestId query.
const tweet = await client.tweets.get("1234567890");
console.log(tweet.text);
console.log(tweet.user.username);
console.log(tweet.stats.likes);Returns: Parsed tweet object.
tweets.getMany(tweetIds)
Get multiple tweets by their IDs. Uses the GraphQL TweetResultsByRestIds query.
const tweets = await client.tweets.getMany(["123", "456", "789"]);
tweets.forEach((t) => console.log(t.text));Returns: Array of parsed tweet objects.
tweets.detail(tweetId, opts?)
Get a tweet with its full conversation thread. Uses the GraphQL TweetDetail query.
| Option | Type | Description |
|---|---|---|
opts.variables | object | Additional GraphQL variables |
const detail = await client.tweets.detail("1234567890");tweets.editHistory(tweetId)
Get the edit history of a tweet. Uses the GraphQL TweetEditHistory query.
const history = await client.tweets.editHistory("1234567890");tweets.likers(tweetId, opts?)
Get users who liked a tweet. Uses the GraphQL Favoriters query. Twitter only returns likers to the tweet's author, so for anyone else's tweet the timeline comes back empty.
| Option | Type | Description |
|---|---|---|
opts.count | number | Number of results (default 20) |
opts.cursor | string | Pagination cursor |
const res = await client.tweets.likers(myTweetId);
const entries = res.data.favoriters_timeline.timeline.instructions;tweets.vote(tweetId, choice, card)
Vote in a poll. Sends a form-encoded POST to caps.x.com/v2/capi/passthrough/1, the same request x.com makes. Votes can't be changed or taken back.
| Param | Type | Description |
|---|---|---|
tweetId | string | The tweet that carries the poll |
choice | number | The 1-based option number, from 1 to 4 |
card.uri | string | The poll card's card:// URI, from tweet.poll.card_uri |
card.name | string | The card name, like poll2choice_text_only (default poll2choice_text_only) |
card.api | string | The card's api binding value (default capi://passthrough/1) |
const tweet = await client.tweets.get("1234567890");
await client.tweets.vote(tweet.id, 2, {
uri: tweet.poll.card_uri,
name: tweet.card_details.name,
api: tweet.card_details.values.api,
});The response is the updated card. Twitter answers with an error if the poll has ended or you already voted.
tweets.retweeters(tweetId, opts?)
Get users who retweeted a tweet. Uses the GraphQL Retweeters query.
| Option | Type | Description |
|---|---|---|
opts.count | number | Number of results (default 20) |
opts.cursor | string | Pagination cursor |
const retweeters = await client.tweets.retweeters("1234567890");tweets.highlight(tweetId)
Highlight a tweet on your profile. Uses the GraphQL CreateHighlight mutation.
await client.tweets.highlight("1234567890");tweets.unhighlight(tweetId)
Remove a tweet highlight. Uses the GraphQL DeleteHighlight mutation.
await client.tweets.unhighlight("1234567890");tweets.schedule(text, scheduledAt, opts?)
Schedule a tweet for future posting. Uses the GraphQL CreateScheduledTweet mutation.
| Option | Type | Description |
|---|---|---|
text | string | The tweet text |
scheduledAt | string|Date | When to post (ISO string or Date) |
opts.mediaIds | string[] | Media IDs to attach |
opts.replyTo | string | Tweet ID to reply to |
await client.tweets.schedule("Good morning!", "2025-01-01T09:00:00Z");
await client.tweets.schedule("With media", new Date("2025-06-01"), {
mediaIds: ["111222333"],
});tweets.editScheduled(scheduledTweetId, text, scheduledAt, opts?)
Untested
This mutation was built from the x.com web bundle and has not been run against a live account.
Replace the text, media and posting time of a scheduled tweet. Uses the GraphQL EditScheduledTweet mutation. It takes the same options as tweets.schedule and sends a full post request, so pass the media IDs again to keep them.
| Param | Type | Description |
|---|---|---|
scheduledTweetId | string | ID from tweets.getScheduled() |
text | string | New tweet text |
scheduledAt | Date | string | number | New posting time |
opts.mediaIds | string[] | Media IDs from media.create |
opts.replyTo | string | Tweet ID to reply to |
await client.tweets.editScheduled("9876543210", "new text", new Date(Date.now() + 3600_000));tweets.deleteScheduled(scheduledTweetId)
Delete a scheduled tweet. Uses the GraphQL DeleteScheduledTweet mutation.
await client.tweets.deleteScheduled("9876543210");tweets.getScheduled()
Fetch all your scheduled tweets. Uses the GraphQL FetchScheduledTweets query.
const scheduled = await client.tweets.getScheduled();tweets.moderate(tweetId)
Moderate a tweet (hide it from your replies). Uses the GraphQL ModerateTweet mutation.
await client.tweets.moderate("1234567890");tweets.unmoderate(tweetId)
Unmoderate a tweet. Uses the GraphQL UnmoderateTweet mutation.
await client.tweets.unmoderate("1234567890");tweets.report(tweetId, reason, opts?)
Report a post for a policy violation. Alias for report.tweet.
await client.tweets.report("1234567890", "spam");tweets.pinReply(tweetId)
Pin a reply under your tweet. Uses the GraphQL PinReply mutation.
await client.tweets.pinReply("1234567890");tweets.unpinReply(tweetId)
Unpin a reply. Uses the GraphQL UnpinReply mutation.
await client.tweets.unpinReply("1234567890");tweets.setConversationControl(tweetId, mode)
Change who can reply to your tweet. Uses the GraphQL ConversationControlChange mutation.
| Mode | Description |
|---|---|
"ByInvitation" | Only mentioned users |
"Community" | Only followers |
await client.tweets.setConversationControl("1234567890", "ByInvitation");tweets.removeConversationControl(tweetId)
Remove reply restrictions from a tweet. Uses the GraphQL ConversationControlDelete mutation.
await client.tweets.removeConversationControl("1234567890");tweets.unmention(tweetId)
Remove yourself from a conversation. Uses the GraphQL UnmentionUserFromConversation mutation.
await client.tweets.unmention("1234567890");tweets.similar(tweetId)
Find similar posts. Uses the GraphQL SimilarPosts query.
const similar = await client.tweets.similar("1234567890");Tweet schema
All methods that return tweets parse them into a consistent format:
{
id: "1234567890",
text: "Hello world!",
created_at: "Mon Jan 01 00:00:00 +0000 2025",
conversation_id: "1234567890",
in_reply_to_status_id: null,
in_reply_to_user_id: null,
in_reply_to_screen_name: null,
user: { /* parsed user object */ },
stats: {
retweets: 10,
likes: 42,
replies: 3,
quotes: 1,
bookmarks: 5,
views: 1000,
},
engagement: {
retweeted: false,
liked: true,
bookmarked: false,
},
media: [],
urls: [],
hashtags: [],
user_mentions: [],
source: "Twitter Web App",
lang: "en",
quoting: null, // parsed quoted tweet or null
retweeting: null, // parsed original tweet or null, set when this tweet is a retweet
is_note_tweet: false, // true for long posts; text holds the full note
poll: null, // { choices: [{ label, count, image }], ends_at, counts_are_final, selected_choice, card_uri }
community_note: null, // { title, text, entities, url, note_id }
limited_actions: [], // e.g. ["Reply"] when Twitter restricts actions on the post
subscriber_only: false, // true for posts only the author's subscribers can read
subscription: null, // see "Subscriber-only posts" below
labels: { /* see "Labels" below */ },
visibility: { /* see "Visibility limited and other notices" below */ },
cta: null, // { title, label, url, display_locations }, the button some posts carry under the text
edit_control: {},
card: null, // the raw card
card_details: null, // the parsed card, see "Cards" below
misc: { ... },
}Tweets from a timeline can also have promoted: true and ad, see Ads, and pinned: true on a profile's pinned tweet.
Unavailable posts (deleted, suspended, age-restricted) parse to { id, tombstone: true, text }, where text is Twitter's explanation.
To check whether a tweet is a retweet, check retweeting instead of matching text against /^RT @/. Twitter sends retweets as their own tweet object whose text is a truncated "RT @user: ..." copy and whose retweeting field holds the full original tweet, parsed the same way as any other tweet (recursively, so a retweet of a quote tweet keeps retweeting.quoting populated too).
Labels
tweet.labels has the tags x.com shows on a post. Every field is there on every tweet, set to false when x.com didn't send it.
const tweet = await client.tweets.get("2109630571148439781");
// tweet.labels
// {
// ai_generated: true,
// ai_generated_source: "UserDeclared",
// paid_partnership: false,
// promoted: false,
// grok_imagine: false,
// made_with_grok: false,
// nsfw: false,
// sensitive_reasons: [],
// visibility_limited: false,
// }| Field | Type | x.com shows | Comes from |
|---|---|---|---|
ai_generated | boolean | "Made with AI" | content_disclosure.ai_generated_disclosure |
ai_generated_source | string | null | "UserDeclared" when the author ticked the box | |
paid_partnership | boolean | "Paid partnership" | content_disclosure.advertising_disclosure |
promoted | boolean | "Ad" | The timeline entry. tweets.get can't tell, so it is always false there |
grok_imagine | boolean | "Made with Grok Imagine" | Every attached image or video has a grok_post_id |
made_with_grok | boolean | The Grok badge on an image | An attached image has a grok_image_annotation |
nsfw | boolean | The sensitive content blur | possibly_sensitive, a media warning, or an adult content interstitial |
sensitive_reasons | string[] | Media warnings: "adult_content", "graphic_violence" or "other" | |
visibility_limited | boolean | "Visibility limited" | Same as visibility.limited |
Set the AI and paid partnership labels when you post with opts.aiGenerated and opts.paidPartnership on tweets.create, or add them later with tweets.addContentDisclosure. opts.sensitive sets nsfw.
Subscriber-only posts
Creators can post things only their subscribers can read. If you aren't subscribed, x.com sends a preview instead of the post, and emusks parses it into a normal tweet object with subscriber_only: true:
const { tweets } = await client.users.tweets("1625804189661286400");
const locked = tweets.find((t) => t.subscriber_only);
// {
// id: "2109541383438782475",
// text: "Day 2 https://…",
// user: { username: "21dineshemiron", ... },
// stats: { replies: 1, views: 7, ... },
// subscriber_only: true,
// subscription: {
// locked: true,
// creator: { username: "21dineshemiron", ... },
// unlock: { label: "Subscribe to unlock", url: "https://x.com/21dineshemiron/superfollows/subscribe" },
// },
// limited_actions: ["Reply", "Retweet", "QuoteTweet", "Like", ...],
// ...
// }The preview text is cut short and ends in …, and media, cards and polls are left out. limited_actions lists everything x.com blocks until you subscribe. tweets.get returns the same preview.
When you are subscribed, you get the full post with subscriber_only: true and subscription.locked: false. Public posts have subscriber_only: false and subscription: null.
Visibility limited and other notices
x.com wraps moderated posts in extra data. tweet.visibility has it, and is empty on normal posts:
const tweet = await client.tweets.get("2052142556088352993");
// tweet.visibility
// {
// limited: true,
// notice: {
// type: "Fosnr",
// label: "Visibility limited",
// text: "This Post may violate X's rules against Abuse.",
// url: "https://help.x.com/rules-and-policies/enforcement-options",
// },
// interstitial: null,
// media_interstitial: null,
// nudges: [],
// appealable: null,
// }| Field | Description |
|---|---|
limited | true when x.com limited the reach of the post. x.com calls this FOSNR, "freedom of speech, not reach" |
notice | The notice under the post. type is one of the values below and label is the heading x.com shows |
interstitial | A warning over the whole post that the viewer has to click through, with type, text and reveal_text |
media_interstitial | The blur over the media, with title such as "Content warning: Adult Content", text, action and opacity |
nudges | Prompts shown before you reply or repost, such as "read the article first" |
appealable | Appeal details shown to the author, unparsed |
notice.type | notice.label |
|---|---|
Fosnr | Visibility limited |
Misleading | Misleading |
GovernmentRequested | Legally required notice |
GetTheLatest | Get the latest |
StayInformed | Stay informed |
limited_actions lists what x.com turned off on the post, such as "Reply" or "Retweet".
Cards
tweet.card_details flattens the card attached to a post: link previews, polls, app cards and ad cards.
{
name: "unified_card", // or "summary_large_image", "poll2choice_text_only", ...
url: "https://t.co/...",
title: "Come build with us.",
description: null,
domain: "github.com",
image: "https://pbs.twimg.com/media/....jpg",
destination: "https://github.com/Tracer-Cloud/opensre/",
cta: { type: "cta", action: "shop_now", label: "Shop now", url: "https://..." },
unified: { /* only for unified cards */ },
values: { /* every binding value, flattened */ },
}cta is the card's button. Cards without a button, like most website cards, have label: null and the whole card links to url. action is one of x.com's button types, such as install, open, play, shop, shop_now, book, order_now, learn_more, sign_up_now, get_tickets or donate. label is the English text x.com shows for it, or the advertiser's own text when type is "custom".
unified is set for unified cards, which is what ads and app install cards use. It has the card type (such as "image_website", "video_website", "image_app" or "image_carousel_website"), title, subtitle, url, vanity, cta, every buttons entry, apps with store details for app install cards, the media entities, and components with each destination resolved.
Ads
Promoted tweets only come from timelines, such as timelines.home, search and profile timelines. They have promoted: true, labels.promoted: true and an ad object:
const { tweets } = await client.timelines.home({ count: 40 });
for (const tweet of tweets.filter((t) => t.promoted)) {
console.log(tweet.ad.advertiser.username, tweet.ad.cta?.label, tweet.ad.destination);
}// tweet.ad
{
advertiser: { /* parsed user object */ },
impression_id: "181b1bd609ad0b72",
click_tracking: { twclid: "263onvgbybrq9aonemiossvzhl" }, // x.com appends these to the link on click
destination_params: { utm_campaign: "...", utm_term: "..." }, // UTM parameters the advertiser set
quick_promote: false, // true for posts boosted with Quick Promote
disclosure_type: null,
experiment_values: [],
card: { /* same as tweet.card_details */ },
cta: { type: null, action: null, label: null, url: "https://business.x.com/en/advertising?..." },
destination: "https://business.x.com/en/advertising?...",
}Some ads are plain posts with no card. Their card, cta and destination are null.
tweets.downvote(tweetId) / tweets.undoDownvote(tweetId)
Privately downvote a reply (the thumbs-down signal), or undo it. Downvotes only apply to replies. Use the GraphQL DownvoteTweet and UndoDownvoteTweet mutations.
await client.tweets.downvote(replyTweetId);
await client.tweets.undoDownvote(replyTweetId);tweets.addContentDisclosure(tweetId, opts?) / tweets.removeContentDisclosure(tweetId, opts?)
Add the "Made with AI" or "Paid partnership" label to a tweet you already posted, or remove both. Uses the GraphQL AddContentDisclosure and DeleteContentDisclosure mutations. addContentDisclosure throws if you pass neither label.
| Option | Type | Description |
|---|---|---|
opts.aiGenerated | boolean | Show the "Made with AI" label |
opts.aiDetectionSource | string | How the AI label was decided (default "UserDeclared") |
opts.paidPartnership | boolean | Show the "Paid partnership" label |
opts.variables | object | Additional GraphQL variables |
await client.tweets.addContentDisclosure(tweetId, { aiGenerated: true });
await client.tweets.addContentDisclosure(tweetId, { paidPartnership: true });
await client.tweets.removeContentDisclosure(tweetId);The labels show up in tweet.labels.