TwitterAPIs Docs
API ReferenceArticles

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.

POST
/article/update_content

Authorization

bearerAuth
AuthorizationBearer <token>

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

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

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.