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

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

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

Single image

single-image.json

Multi-image (up to 4)

multi-image.json

Single video

Chunked upload runs transparently — small clips return immediately, larger ones run INIT → APPEND → FINALIZE → STATUS until X reports succeeded.
video.json

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

Quote tweet

Mutually exclusive with replyToTweetId — preflight rejects setting both, since X itself does.
quote.json

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

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