Skip to content

How do you publish an X Article with an API?

Last updated August 24, 2026

An Article starts empty and gets filled in afterwards. article/create returns a draft carrying an entity id, update_title, update_content and update_cover_media edit that draft, and article/publish flips its lifecycle to Published while posting the announcement tweet that links to it. Six of the nine routes are premium writes at $0.0016; get, list and delete are $0.0008.

Every rate here is the pricing TwitterAPIs publishes. The billed rate is $0.0008 per call; $0.04 per 1,000 tweets is derived from it at a full 20-tweet page, which is the default page size rather than a guaranteed yield (source: twitterapis.com/pricing).

The order of calls, start to finish

There is no single save call, so publishing is a sequence rather than one request with a large body. It runs article/create to open an empty draft, then article/update_title, then article/update_content, then article/update_cover_media if you want a cover, then article/publish. Five calls for a first publish, and only the last of them is visible to anybody else. Every route in that sequence needs a registered session for the acting account, because a draft lives on your account and cannot be created or edited from a shared pool. The three routes outside the sequence are article/get, article/list and article/delete, which is what you use afterwards to find and remove what you made.

What the create call hands back

article/create takes no parameters at all. It returns ok and an article object whose title is null, whose preview_text is an empty string, whose content_state holds empty blocks and entityMap arrays, and whose lifecycle reads Draft. Two identifiers come back and they are not interchangeable. id is the entity id, a base64 looking string, and it is what every later call in this family expects. rest_id is the numeric id that ends up inside the public URL after publishing. Alongside them sit author, created_at_secs and modified_at_secs as second precision epochs, plus first_published_at_secs, visibility_setting, tweet_id, cover_media and public_url, all null on a fresh draft and each filled in by a later step.

Setting the title

article/update_title takes the entity id and a title string, and either may travel in the query string or in a JSON request body, whichever suits your client. It works on a draft and on an already published article, so correcting a headline after the fact does not mean unpublishing first. The response is the whole article object again, in the same shape create returned, with title now set and modified_at_secs moved forward. That consistent envelope is why a client can treat every article write identically: send a change, read the current state of the article back, and never keep a local copy that can silently drift from what X is actually holding.

Replacing the body with Draft.js JSON

article/update_content replaces the body. content_state is Draft.js JSON, an object carrying a blocks array and an entityMap, and this is the one route where the payload must go in the JSON request body rather than the query string. A missing or non object value is a 400. Each block carries key, text, type, depth, inlineStyleRanges and entityRanges. The structure is passed through verbatim and is not validated on the way, which means your client owns it end to end: nothing here repairs a malformed block or rejects one on your behalf. preview_text on the response is derived from what you sent, so reading it back is a cheap check that the text landed where you thought it would.

Attaching a cover image

article/update_cover_media attaches, it never uploads. Send the image to media/upload first, keep the media_id it returns, then call this route with the article id and that media_id. An optional media_category defaults to DraftTweetImage, which is exactly what X's own article editor sends, so leaving it alone is usually correct. The response fills cover_media with media_id, media_key, original_img_url, original_img_width and original_img_height, so the dimensions X recorded come back to you rather than having to be measured locally before or after. cover_media stays null until this call lands, so an article published without it simply has no cover rather than falling back to something generated. Like the title route it works on a draft or a published article, which makes swapping the cover on something already live a single call.

Publishing, and the announcement tweet

article/publish is the only route here that anybody else can see. It moves lifecycle to Published, sets visibility_setting to Public, fills tweet_id with the id of the announcement tweet it has just posted, sets first_published_at_secs, and returns public_url in the form x.com/i/article/ followed by the rest_id. Three optional parameters shape it. audience controls who can see the article and reply_control controls who can reply to the announcement, both defaulting to Everyone, the most permissive setting, when left out. caption adds up to 256 characters of text to the announcement post. first_published_at_secs is stamped on this call and survives a later unpublish, so it stays the record of when the piece first went out. Because a real tweet goes out, treat a call here as a shipping action rather than another save.

Unpublish leaves the announcement standing

article/unpublish walks lifecycle back to Draft and clears visibility_setting and public_url. It fails when the article is not currently Published, because X validates that transition upstream rather than quietly accepting a no op. What it does not do is remove the announcement tweet, which stays live in the timeline with its id still sitting on the article object as tweet_id. So an article you have quietly unpublished still has a public post pointing at it, and anyone following that link learns the article is gone instead of never having seen the link at all. The response shows exactly that asymmetry: tweet_id is still populated while public_url has gone null. If you want both retracted, unpublish is the wrong route.

Delete, and its two fast path hints

article/delete handles both shapes. A draft is hard deleted directly. A published article is unpublished first and then its announcement tweet is removed, which is what makes it the complete retraction. Two optional parameters are purely about speed: pass lifecycle and tweet_id from an earlier create or list response and the route skips a lookup, whereas omitting them costs an internal scan of your Draft list and then your Published list, the first 100 of each. The failure mode is handled honestly rather than optimistically. If the announcement tweet id cannot be resolved, the article is left as a Draft instead of reporting a success that did not happen. It answers with ok, deleted and an echo of the id.

Reading articles back, and the price split

Two routes read. article/list pages your own articles filtered by lifecycle, draft or published, defaulting to draft, with count clamped to 1 through 100 and a cursor for paging. X exposes no combined view, so seeing everything means calling it twice, and an empty array is a valid answer rather than a failure. article/get has two mutually exclusive forms: id or url, meaning the announcement tweet, is a public pooled read that needs no session and works on published articles only, while article_id is an owner read that reaches your drafts too. On price, the six writes from create through unpublish sit in the premium tier at $0.0016, while get, list and delete are charged at the ordinary read rate. That rate is $0.0016 a call, per our own rate card.

The nine article routes and what they bill

RouteDoes whatPrice per call
article/createOpens an empty draft and returns its entity id$0.0016
article/update_titleSets or corrects the headline$0.0016
article/update_contentReplaces the body with Draft.js JSON$0.0016
article/update_cover_mediaAttaches an uploaded media_id as the cover$0.0016
article/publishPublishes and posts the announcement tweet$0.0016
article/unpublishReturns it to Draft, announcement stays live$0.0016
article/get and article/listRead one article, or page your own by lifecycle$0.0008
article/deleteRemoves the article and its announcement tweet$0.0008
Each media object may have multiple display or playback variants, with different resolutions or formats.
X Developer Platform, media upload documentation. Source

Questions and answers

What is an X Article?
It is X's long form post format, stored as its own entity rather than as a tweet. It carries a title, a cover image, a Draft.js content body, an author and a lifecycle that is either Draft or Published. When published it gets a public_url of the form x.com/i/article/<rest_id> plus a real announcement tweet, whose id comes back on the object as tweet_id.
What is the difference between id and rest_id?
id is the entity id, a base64 looking string, and it is the value every write in this family expects: title, content, cover, publish, unpublish and delete all take it. rest_id is the numeric id that appears inside the public article URL after publishing. They arrive together on the create response and are not interchangeable, so store both rather than picking one.
What format does the article body take?
Draft.js JSON, an object with a blocks array and an entityMap. Each block carries key, text, type, depth, inlineStyleRanges and entityRanges. It must be sent in the request body rather than the query string, and it is forwarded to X untouched with no validation on the way through, so your client builds and edits that structure itself. A missing or non object value is a 400.
How do I set a cover image?
Upload the image through media/upload first, keep the media_id it hands back, then call update_cover_media with the article id and that media_id. The cover route attaches, it never uploads. media_category defaults to DraftTweetImage, matching what X's own editor sends. The reply returns cover_media with the media_key and the original image width and height filled in. Until that call lands, cover_media is null on the article object, so publishing early means publishing without a cover.
Can I edit an article after it is published?
The title and cover routes are both documented as working on a draft or an already published article, so fixing a headline or swapping a cover image is a single call with no unpublish step in front of it. Every write returns the full article object back, so you can read the new state immediately rather than assuming the change landed and caching your own copy.
Can I take a published article back down?
Partly. article/unpublish returns the lifecycle to Draft and clears public_url, but it leaves the announcement tweet standing, so the post that told everyone about it is still in the timeline. Use article/delete if you want both gone, since it unpublishes first and then removes the announcement. Passing lifecycle and tweet_id as hints skips an internal list scan. After an unpublish the object keeps tweet_id populated while public_url goes null, which is the shape to check for.
How do I find an article I created earlier?
Call article/list, which pages your own articles by lifecycle and defaults to draft. X exposes no combined view, so run it once for draft and once for published to see everything. Take the entity id from a row and pass it to article/get as article_id for the owner read, which is the only form that reaches drafts, since a draft has no announcement tweet.
Do I need a logged in account for this?
Every article write does, plus the list route and the owner form of get, because drafts live on your account and cannot come from a shared pool. Register auth_token and ct0 through the session route, or send them per call as headers. Only the public form of article/get, keyed on the announcement tweet id or its status URL, works with no session at all.
Why does article/get return a 404?
On the public form, a 404 covers three separate cases: the tweet does not exist, it is protected, or it exists but is not an article announcement. On the owner form it additionally covers an article that is not owned by the calling account. The status does not distinguish between them, so treat it as not readable by you rather than as proof that nothing is there.
What does one full publish cycle cost?
Create, set the title, set the content, attach a cover, publish: five premium writes at $0.0016 each is eight tenths of a cent. Reading the result back with get, or listing your drafts, is charged at the standard read rate instead, as is delete. Nothing recurs, so an article that is never edited again is billed exactly once. The $0.50 of credit a new account starts with covers roughly 62 complete publish cycles before you pay anything.

Start with $0.50 in free credits

No credit card. Roughly 12,500 tweets to test every endpoint.