Skip to content

Tweets ​

Create, delete, like, retweet, pin, schedule, and manage tweets.

tweets.create(text, opts?) ​

Create a new tweet. Uses the GraphQL CreateTweet mutation.

OptionTypeDescription
textstringThe tweet text
opts.mediaIdsstring[]Array of media IDs to attach
opts.gifobjectAttach a GIF from search.gifs
opts.replyTostringTweet ID to reply to
opts.quoteTweetIdstringTweet ID to quote
opts.sensitivebooleanMark media as possibly sensitive
opts.aiGeneratedbooleanShow the "Made with AI" label
opts.paidPartnershipbooleanShow the "Paid partnership" label
opts.conversationControlstringWho can reply: "ByInvitation" or "Community"
opts.pollobjectAttach a poll
opts.cardUristringA Twitter Card URI to attach (cannot be combined with poll)
opts.variablesobjectAdditional GraphQL variables to merge
js
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 ​

FieldTypeRequiredDescription
choicesstring[] | object[]Yes2–4 choices - strings for text polls, objects for media polls
duration_minutesnumberNoHow 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, where image is a media_id from media.create()

Text poll ​

js
await client.tweets.create("Tabs or spaces?", {
  poll: {
    choices: ["Tabs", "Spaces"],
  },
});

Media poll ​

Upload images first, then reference their media_id in each choice:

js
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 ​

js
// 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 ​

MinMaxDefault
5 minutes10 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.

OptionTypeDescription
textstringThe note tweet text
opts.mediaIdsstring[]Array of media IDs to attach
opts.replyTostringTweet ID to reply to
opts.sensitivebooleanMark media as possibly sensitive
opts.aiGeneratedbooleanShow the "Made with AI" label
opts.paidPartnershipbooleanShow the "Paid partnership" label
opts.richtext_tagsarrayRich text formatting tags
opts.variablesobjectAdditional GraphQL variables
js
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.

ParamTypeDescription
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.

js
// 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 tweet

Returns: Array of parsed tweet objects, one per tweet in the thread.

tweets.delete(tweetId) ​

Delete a tweet. Uses the GraphQL DeleteTweet mutation.

js
await client.tweets.delete("1234567890");

tweets.like(tweetId) ​

Like a tweet. Uses the GraphQL FavoriteTweet mutation.

js
await client.tweets.like("1234567890");

tweets.unlike(tweetId) ​

Unlike a tweet. Uses the GraphQL UnfavoriteTweet mutation.

js
await client.tweets.unlike("1234567890");

tweets.retweet(tweetId) ​

Retweet a tweet. Uses the GraphQL CreateRetweet mutation.

js
await client.tweets.retweet("1234567890");

tweets.unretweet(tweetId) ​

Undo a retweet. Uses the GraphQL DeleteRetweet mutation.

js
await client.tweets.unretweet("1234567890");

tweets.pin(tweetId) ​

Pin a tweet to your profile. Uses the GraphQL PinTweet mutation.

js
await client.tweets.pin("1234567890");

tweets.unpin(tweetId) ​

Unpin a tweet from your profile. Uses the GraphQL UnpinTweet mutation.

js
await client.tweets.unpin("1234567890");

tweets.get(tweetId) ​

Get a single tweet by ID. Uses the GraphQL TweetResultByRestId query.

js
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.

js
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.

OptionTypeDescription
opts.variablesobjectAdditional GraphQL variables
js
const detail = await client.tweets.detail("1234567890");

tweets.editHistory(tweetId) ​

Get the edit history of a tweet. Uses the GraphQL TweetEditHistory query.

js
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.

OptionTypeDescription
opts.countnumberNumber of results (default 20)
opts.cursorstringPagination cursor
js
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.

ParamTypeDescription
tweetIdstringThe tweet that carries the poll
choicenumberThe 1-based option number, from 1 to 4
card.uristringThe poll card's card:// URI, from tweet.poll.card_uri
card.namestringThe card name, like poll2choice_text_only (default poll2choice_text_only)
card.apistringThe card's api binding value (default capi://passthrough/1)
js
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.

OptionTypeDescription
opts.countnumberNumber of results (default 20)
opts.cursorstringPagination cursor
js
const retweeters = await client.tweets.retweeters("1234567890");

tweets.highlight(tweetId) ​

Highlight a tweet on your profile. Uses the GraphQL CreateHighlight mutation.

js
await client.tweets.highlight("1234567890");

tweets.unhighlight(tweetId) ​

Remove a tweet highlight. Uses the GraphQL DeleteHighlight mutation.

js
await client.tweets.unhighlight("1234567890");

tweets.schedule(text, scheduledAt, opts?) ​

Schedule a tweet for future posting. Uses the GraphQL CreateScheduledTweet mutation.

OptionTypeDescription
textstringThe tweet text
scheduledAtstring|DateWhen to post (ISO string or Date)
opts.mediaIdsstring[]Media IDs to attach
opts.replyTostringTweet ID to reply to
js
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.

ParamTypeDescription
scheduledTweetIdstringID from tweets.getScheduled()
textstringNew tweet text
scheduledAtDate | string | numberNew posting time
opts.mediaIdsstring[]Media IDs from media.create
opts.replyTostringTweet ID to reply to
js
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.

js
await client.tweets.deleteScheduled("9876543210");

tweets.getScheduled() ​

Fetch all your scheduled tweets. Uses the GraphQL FetchScheduledTweets query.

js
const scheduled = await client.tweets.getScheduled();

tweets.moderate(tweetId) ​

Moderate a tweet (hide it from your replies). Uses the GraphQL ModerateTweet mutation.

js
await client.tweets.moderate("1234567890");

tweets.unmoderate(tweetId) ​

Unmoderate a tweet. Uses the GraphQL UnmoderateTweet mutation.

js
await client.tweets.unmoderate("1234567890");

tweets.report(tweetId, reason, opts?) ​

Report a post for a policy violation. Alias for report.tweet.

js
await client.tweets.report("1234567890", "spam");

tweets.pinReply(tweetId) ​

Pin a reply under your tweet. Uses the GraphQL PinReply mutation.

js
await client.tweets.pinReply("1234567890");

tweets.unpinReply(tweetId) ​

Unpin a reply. Uses the GraphQL UnpinReply mutation.

js
await client.tweets.unpinReply("1234567890");

tweets.setConversationControl(tweetId, mode) ​

Change who can reply to your tweet. Uses the GraphQL ConversationControlChange mutation.

ModeDescription
"ByInvitation"Only mentioned users
"Community"Only followers
js
await client.tweets.setConversationControl("1234567890", "ByInvitation");

tweets.removeConversationControl(tweetId) ​

Remove reply restrictions from a tweet. Uses the GraphQL ConversationControlDelete mutation.

js
await client.tweets.removeConversationControl("1234567890");

tweets.unmention(tweetId) ​

Remove yourself from a conversation. Uses the GraphQL UnmentionUserFromConversation mutation.

js
await client.tweets.unmention("1234567890");

tweets.similar(tweetId) ​

Find similar posts. Uses the GraphQL SimilarPosts query.

js
const similar = await client.tweets.similar("1234567890");

Tweet schema ​

All methods that return tweets parse them into a consistent format:

js
{
  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.

js
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,
// }
FieldTypex.com showsComes from
ai_generatedboolean"Made with AI"content_disclosure.ai_generated_disclosure
ai_generated_sourcestring | null"UserDeclared" when the author ticked the box
paid_partnershipboolean"Paid partnership"content_disclosure.advertising_disclosure
promotedboolean"Ad"The timeline entry. tweets.get can't tell, so it is always false there
grok_imagineboolean"Made with Grok Imagine"Every attached image or video has a grok_post_id
made_with_grokbooleanThe Grok badge on an imageAn attached image has a grok_image_annotation
nsfwbooleanThe sensitive content blurpossibly_sensitive, a media warning, or an adult content interstitial
sensitive_reasonsstring[]Media warnings: "adult_content", "graphic_violence" or "other"
visibility_limitedboolean"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:

js
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:

js
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,
// }
FieldDescription
limitedtrue when x.com limited the reach of the post. x.com calls this FOSNR, "freedom of speech, not reach"
noticeThe notice under the post. type is one of the values below and label is the heading x.com shows
interstitialA warning over the whole post that the viewer has to click through, with type, text and reveal_text
media_interstitialThe blur over the media, with title such as "Content warning: Adult Content", text, action and opacity
nudgesPrompts shown before you reply or repost, such as "read the article first"
appealableAppeal details shown to the author, unparsed
notice.typenotice.label
FosnrVisibility limited
MisleadingMisleading
GovernmentRequestedLegally required notice
GetTheLatestGet the latest
StayInformedStay 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.

js
{
  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:

js
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);
}
js
// 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.

js
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.

OptionTypeDescription
opts.aiGeneratedbooleanShow the "Made with AI" label
opts.aiDetectionSourcestringHow the AI label was decided (default "UserDeclared")
opts.paidPartnershipbooleanShow the "Paid partnership" label
opts.variablesobjectAdditional GraphQL variables
js
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.

not affiliated with X Corp.