Skip to main content
Before you start. Every URL costs 23 characters regardless of its real length — even after t.co wrapping. The grapheme counter subtracts the raw URL and adds the t.co weight so the 280-character preflight matches X’s server-side count exactly. Long links don’t help you fit; they just hurt the visible text.

Quick reference

Limit / capabilityValue
Character limit280 graphemes (t.co-weighted)
Images per tweet4
Image formatsjpeg, png, webp
Image size5,000,000 bytes (5 MB)
GIF size15,000,000 bytes (15 MB)
Video formatsmp4 (H.264 + AAC recommended)
Video size512,000,000 bytes (512 MB)
Video duration140 s on the standard tier (platform-enforced)
Mixed image + videoRejected — pick one per tweet
Alt text per item1,000 graphemes (best-effort write)
Post types supportedtext, single image, multi-image, single video, single GIF, reply, quote
SchedulingYes (via scheduledAt)
Thread (reply chain)Yes (twitter.replyToTweetId)
Quote tweetYes (twitter.quoteTweetId)
First commentNot supported (use a reply chain)
Inbox / DMNot supported in v1
AnalyticsNot supported in v1

Connect an account

POST /v1/accounts/connect/twitter. OAuth 2.0 PKCE. letmepost signs a state token carrying the PKCE code_verifier through the redirect so the dashboard never stashes it client-side. After the X callback, the code+verifier exchange runs server-side.

Scopes

tweet.write     — create tweets
tweet.read      — required to mint the token
users.read      — required to mint the token
offline.access  — required for refresh tokens
Extended: like.read, follows.read — off by default; not needed for publishing.

Token lifecycle

X access tokens are short-lived (~2 hours); offline.access mints a refresh token that lets the publisher refresh on schedule. If a user revokes app access from X’s settings the next call surfaces platform_auth_failed.

Post types

Text post

text.json
{
  "targets": [{ "accountId": "..." }],
  "text": "shipped"
}

Single image

single-image.json
{
  "targets": [{ "accountId": "..." }],
  "text": "shipped",
  "media": [{ "kind": "image", "mediaId": "med_…", "altText": "..." }]
}

Multi-image (up to 4)

multi-image.json
{
  "targets": [{ "accountId": "..." }],
  "text": "four shots",
  "media": [
    { "kind": "image", "mediaId": "med_a…", "altText": "front" },
    { "kind": "image", "mediaId": "med_b…", "altText": "back" },
    { "kind": "image", "mediaId": "med_c…", "altText": "left" },
    { "kind": "image", "mediaId": "med_d…", "altText": "right" }
  ]
}

Single video

Chunked upload runs transparently — small clips return immediately, larger ones run INIT → APPEND → FINALIZE → STATUS until X reports succeeded.
video.json
{
  "targets": [{ "accountId": "..." }],
  "text": "demo clip",
  "media": [{ "kind": "video", "mediaId": "med_…" }]
}

Reply (and threads)

Threads on X are reply chains — pass the previous tweet id on each follow-up. The first tweet in a thread is just a normal post; the second carries replyToTweetId set to the first’s id, the third to the second’s, and so on.
reply.json
{
  "targets": [
    {
      "accountId": "...",
      "options": { "platform": "twitter", "replyToTweetId": "1700000000000000001" }
    }
  ],
  "text": "(2/3) more details"
}

Quote tweet

Mutually exclusive with replyToTweetId — preflight rejects setting both, since X itself does.
quote.json
{
  "targets": [
    {
      "accountId": "...",
      "options": { "platform": "twitter", "quoteTweetId": "1700000000000000001" }
    }
  ],
  "text": "this is huge"
}

Wisdom (platform-specific things that bite)

  • Every URL costs 23 characters regardless of length. The counter subtracts the real URL and adds the t.co weight, so a 3-char shortlink and a 300-char tracking URL both weigh 23.
  • Alt text is best-effort. The /1.1/media/metadata/create write happens on a deprecation track separate from /2/tweets; if the metadata write fails, the tweet still posts. You don’t lose the post — you lose the accessibility text.
  • The free posting tier was retired in 2025; new developers go through Pay Per Use or a paid tier. letmepost works on any tier with tweet.write. The launch-cap preflight (twitter.launch_cap.per_account) surfaces per-account ceilings before X 429s.
  • replyToTweetId and quoteTweetId are mutually exclusive — X rejects tweets with both. Preflight catches this locally before the upstream round-trip.
  • Chunked video uploads use 4 MiB segments. The publisher polls the FINALIZE → STATUS loop and surfaces failures as twitter.media.video_processing_failed with the upstream processing_info.error attached.
  • GIFs are not “images” on X — they have their own 15 MB ceiling (twitter.media.gif_size_max) distinct from static images’ 5 MB.
  • Mixed image + video isn’t allowed on the same tweet; carousel of mixed media doesn’t exist on X. Pick one kind per tweet, or chain a thread.

Common errors

Error ruleWhat it meansHow to fix
twitter.text.max_graphemesWeighted character count > 280Trim text or shorten URLs (won’t help past 23 chars/URL).
twitter.text.non_emptyTweet body was emptyProvide non-whitespace text — X requires it even on media tweets.
twitter.media.count_max> 4 images, or > 1 video/GIFUp to 4 images, 1 video, or 1 GIF per tweet.
twitter.media.image_video_exclusiveMixed image + videoPick one media kind per tweet.
twitter.media.image_size_maxImage > 5 MBRe-encode under 5,000,000 bytes.
twitter.media.gif_size_maxGIF > 15 MBRe-encode under 15,000,000 bytes.
twitter.media.video_size_maxVideo > 512 MBCompress under 512,000,000 bytes.
twitter.media.video_processing_failedFINALIZE / STATUS returned an errorInspect platformResponse.processing_info.error for the codec / container reason.
twitter.media.processing_timeoutSTATUS poll exceeded the deadlineRetry; X’s transcoder sometimes lags.
twitter.launch_cap.per_accountPer-account launch-cap exhaustedWait for the cap window to roll over, or upgrade the X tier.

What you can’t do (yet)

  • Polls.
  • Long-form (premium) tweets above 280 graphemes — the preflight cap is wired for the standard limit; long-form support lands in a follow-up slice.
  • Reading replies, likes, or retweet metadata.
  • DMs.
  • Lists (creating tweets that target a list audience).
  • Spaces.

API reference