Update Article Content API
Replace the body of an article you own with a new content_state, identified by its entity id. content_state is Draft.js editor-state JSON ({ blocks: [...], entityMap: [...] }) that you build client-side; in the shape the X Article editor produces. Blocks are forwarded with only key, text, type, data, entityRanges and inlineStyleRanges; X's schema rejects any other block field (depth included). Block types: unstyled, header-two, unordered-list-item, ordered-list-item, blockquote and atomic (a one-space block that carries an entity through entityRanges). entityMap is an ARRAY of { key, value: { type, mutability, data } }; entity data is snake_case on input and comes back camelCase: TWEET (an embedded post) { tweet_id }, MEDIA (an inline image) { caption, entity_key, media_items: [{ local_media_id, media_category: "DraftTweetImage", media_id }] } with media_id from media/upload, DIVIDER {}, LINK { url } over a text range, MARKDOWN { markdown } for tables. A field X does not know is refused with 422, reason validation_failed and the offending path in detail. Measured live 2026-09-18. Requires a registered session. Billed at $0.0016 per call. Cost: $0.0016 per call.
POST article/update_content replaces the full body of an article you own with a new Draft.js content_state, identified by the article's base64 entity id. It requires a registered session and costs $0.0016 per call. On success it returns ok: true and the updated article object reflecting the new content.
Authorization
bearerAuth Pass your API key as a bearer token on every request: Authorization: Bearer <API_KEY>.
In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/article/update_content" \ -H "Content-Type: application/json" \ -d '{ "id": "QXJ0aWNsZUVudGl0eToyMDg2NjUwMjI4OTUwNjgzNjQ4", "content_state": "{\\"blocks\\":[{\\"key\\":\\"a1b2c\\",\\"text\\":\\"Opening paragraph.\\",\\"type\\":\\"unstyled\\",\\"inlineStyleRanges\\":[],\\"entityRanges\\":[]},{\\"key\\":\\"em1\\",\\"text\\":\\" \\",\\"type\\":\\"atomic\\",\\"inlineStyleRanges\\":[],\\"entityRanges\\":[{\\"key\\":0,\\"offset\\":0,\\"length\\":1}]},{\\"key\\":\\"im1\\",\\"text\\":\\" \\",\\"type\\":\\"atomic\\",\\"inlineStyleRanges\\":[],\\"entityRanges\\":[{\\"key\\":1,\\"offset\\":0,\\"length\\":1}]}],\\"entityMap\\":[{\\"key\\":\\"0\\",\\"value\\":{\\"type\\":\\"TWEET\\",\\"mutability\\":\\"Immutable\\",\\"data\\":{\\"tweet_id\\":\\"2077572855194157402\\"}}},{\\"key\\":\\"1\\",\\"value\\":{\\"type\\":\\"MEDIA\\",\\"mutability\\":\\"Immutable\\",\\"data\\":{\\"caption\\":\\"\\",\\"entity_key\\":\\"img-1\\",\\"media_items\\":[{\\"local_media_id\\":\\"1\\",\\"media_category\\":\\"DraftTweetImage\\",\\"media_id\\":\\"2101008406014312448\\"}]}}}]}" }'{ "ok": true, "article": { "id": "QXJ0aWNsZUVudGl0eToyMDg2NjUwMjI4OTUwNjgzNjQ4", "rest_id": "2086650228950683648", "title": "Why we rebuilt our onboarding from scratch", "preview_text": "Opening paragraph.", "lifecycle": "Draft", "content_state": { "blocks": [ { "key": "a1b2c", "text": "Opening paragraph.", "type": "unstyled", "inlineStyleRanges": [], "entityRanges": [] } ], "entityMap": [] }, "cover_media": null, "media_entities": [], "author": { "id": "44196397", "username": "elonmusk", "name": "Elon Musk" }, "created_at_secs": 1786400000, "modified_at_secs": 1786400240, "first_published_at_secs": null, "visibility_setting": null, "tweet_id": null, "public_url": null }}{ "error": "bad_request", "message": "Missing or malformed parameter. Fix the request before retrying."}{ "error": "unauthorized", "message": "The API key is missing, malformed, or revoked. Check the Authorization header."}{ "error": "insufficient_credits", "message": "Your balance is exhausted. Top up your credits to continue."}{ "error": "forbidden", "message": "X refused this resource for the acting account: on a read, the account is not a participant in the DM thread, the target is protected or has blocked it, or the session was attestation-blocked for calling from a datacenter or rotating IP; on a write, the account is not authorized for the action or has no logged-in session. The credentials are still valid, so do not re-register."}{ "error": "not_found", "message": "The resource does not exist, for example a deleted tweet or a private account."}{ "error": "rate_limited", "message": "Too many requests. Back off and retry with exponential backoff."}{ "error": "server_error", "message": "Something failed on our side. Retry with backoff; if it persists, contact support."}Pricing
| Unit | Price |
|---|---|
| Per call | $0.0016 |
| Per 1,000 calls | $1.60 |
When to use
Use this endpoint when you already have an article's entity id and need to overwrite its body with a full new Draft.js content_state, for example after the user edits the article in your own editor and hits save. It replaces the entire body in one call; it does not patch a title (use article/update_title for that) or touch publish state (use article/publish or article/unpublish).
Notes
- Measured live 2026-09-18 on a throwaway draft: the request variables and the entity contract below were executed end-to-end and read back through article/list.
- X validates the shape strictly. A block field or entity data key X does not know (depth on a block, tweetId instead of tweet_id on a TWEET entity) returns 422 with ok:false, reason validation_failed and the offending variable path in detail; nothing is written or billed.
- Entity contract: TWEET { tweet_id } embeds a post; MEDIA { caption, entity_key, media_items: [{ local_media_id, media_category: "DraftTweetImage", media_id }] } places an image uploaded with media/upload and populates media_entities on the response; DIVIDER {}; LINK { url } as a Mutable entity over a text range; MARKDOWN { markdown } for tables. X echoes entity data back camelCase (tweetId, mediaItems).
- Needs a session for the account you are writing as: either register one once (POST /customer/session), or pass per-call inline credentials as x-auth-token and x-ct0 request headers, which let a single API key act as many accounts. Returns 409 session_required if neither is supplied, 401 session_dead when the session has expired.
- Works on both a draft and an already-published article.
Related endpoints
- Create Article: Start a new draft article. No request body required.
- Update Article Title: Set the title on a draft or published article.
- Publish Article: Publish a draft article. This posts a real, public announcement tweet.
FAQ
What format does content_state need to be in?
content_state is a JSON object in the request body, the Draft.js editor-state structure Twitter/X's own article editor produces: blocks (unstyled, header-two, list items, blockquote, atomic) plus an entityMap array of TWEET, MEDIA, DIVIDER, LINK and MARKDOWN entities with snake_case data keys. The endpoint does not accept plain text or Markdown as the body; a MARKDOWN entity carries a table inside an atomic block.
How do I get the id parameter for an article I want to update?
id is the article's base64 entity id, not its URL or slug. Retrieve it from a prior article/create or article/list call's response, then pass that same id string to article/update_content to target that specific article.
Update Article Cover Image POST
Set the cover image on an article you own, identified by its entity id. This attaches media you have already uploaded; it does not upload anything itself, so call media/upload first and pass the media_id it returns. Requires a registered session. Billed at $0.0016 per call. Cost: $0.0016 per call.
Publish Article POST
Publish a draft article as the account behind your registered session, transitioning it from Draft to Published. WARNING: publishing posts a real, public announcement tweet on the account's timeline. This is not a preview or a private action, and it cannot be undone by re-editing the article: once published, the only way back is Delete Article, which unpublishes the article AND removes the announcement tweet. Requires a registered session. Billed at $0.0016 per call. Cost: $0.0016 per call.