Twitter APIX APIArticlesTutorialPythonNode.jsLong-form

Twitter Article API in 2026: Create, Publish, and Distribute Long-Form Notes

Complete 2026 tutorial for the Twitter Article API. All 7 endpoints, working Python and Node.js code, the Premium gate explained, draft vs published state machine.

GetXAPI·
Twitter Article API tutorial 2026 - 7 endpoints, working code, premium gate

You have a content pipeline. Blog posts, newsletters, research threads. You want them on X as long-form Articles without manual posting. X expanded Articles to all Premium tiers on January 7, 2026 (source: ppc.land), and developer demand for programmatic Article management jumped overnight.

The problem: the official X API v2 has no Article endpoints. Not one. The devcommunity.x.com forum has an open request thread for CRUD Article endpoints that X has not fulfilled as of 2026-05-27.

GetXAPI shipped 7 Article endpoints on 2026-05-17, covering the full Article lifecycle. This tutorial walks through every endpoint with working Python and Node.js code, the Premium gate in plain terms, and a production-ready happy-path script you can run today.

TLDR: POST to /twitter/article/create with a markdown body. Add "publish": true if you have X Premium and want to go live in one call. Done. Read on for the full lifecycle, error handling, and the state machine that governs draft vs published behavior.


The 7 Twitter Article API Endpoints at a Glance

7-endpoint matrix showing all Article API endpoints, methods, pricing, and Premium requirements All 7 Article endpoints shipped 2026-05-17. Price per call as of 2026-05-27.

Method Endpoint Purpose Price Requires X Premium Requires auth_token
GET /twitter/article/get Fetch article by ID $0.001 No Yes (for drafts; public articles may work without)
POST /twitter/article/create Create draft (or publish inline) $0.01 No (draft only). Yes for "publish": true Yes
POST /twitter/article/update Edit title, body, or cover image $0.005 No (draft edits). Verify for published edits Yes
POST /twitter/article/list List drafts and published articles (paginated) $0.005 No Yes
POST /twitter/article/publish Move draft to published (Premium-gated) $0.005 Yes Yes
POST /twitter/article/unpublish Revert published article to draft $0.005 No Yes
POST /twitter/article/delete Permanently delete article (irreversible) $0.005 No Yes

Base URL for all calls: https://api.getxapi.com

Authentication: every call requires two credentials in the request:

  • Authorization: Bearer YOUR_GETXAPI_KEY header (your GetXAPI API key from the dashboard)
  • auth_token field in the request body or params (your X session token, tied to the account managing the articles)

For how to get your API key in under two minutes, see the how to get a Twitter API key guide.


Quickstart: Twitter Article API in Python and Node.js in 60 Seconds

Get your free API key at getxapi.com/signup. No developer account. No OAuth approval queue. Paste the snippet, run it, see your first Article draft created in seconds.

curl round-trip (create + list + confirm)

# 1. Create a draft article
curl -X POST https://api.getxapi.com/twitter/article/create \
  -H "Authorization: Bearer $GETXAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "auth_token": "YOUR_X_AUTH_TOKEN",
    "title": "My First API Article",
    "body": "## Introduction\n\nThis article was created programmatically via GetXAPI.\n\nThe Twitter Article API makes long-form content automation possible."
  }'

# 2. List your articles to confirm it exists
curl -X POST https://api.getxapi.com/twitter/article/list \
  -H "Authorization: Bearer $GETXAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"auth_token": "YOUR_X_AUTH_TOKEN"}'

Python round-trip

import os
import requests

GETXAPI_KEY = os.environ["GETXAPI_KEY"]
AUTH_TOKEN = os.environ["X_AUTH_TOKEN"]
HEADERS = {"Authorization": f"Bearer {GETXAPI_KEY}"}
BASE = "https://api.getxapi.com"

# Create a draft article
resp = requests.post(
    f"{BASE}/twitter/article/create",
    json={
        "auth_token": AUTH_TOKEN,
        "title": "My First API Article",
        "body": "## Introduction\n\nCreated via GetXAPI Python client.",
    },
    headers=HEADERS,
)
resp.raise_for_status()
article = resp.json()
print("Created article ID:", article.get("id"))

# List to confirm
list_resp = requests.post(
    f"{BASE}/twitter/article/list",
    json={"auth_token": AUTH_TOKEN},
    headers=HEADERS,
)
articles = list_resp.json().get("articles", [])
print(f"Account has {len(articles)} article(s)")

Node.js round-trip

const BASE = "https://api.getxapi.com";
const HEADERS = {
  "Authorization": `Bearer ${process.env.GETXAPI_KEY}`,
  "Content-Type": "application/json",
};
const AUTH_TOKEN = process.env.X_AUTH_TOKEN;

// Create draft
const createResp = await fetch(`${BASE}/twitter/article/create`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({
    auth_token: AUTH_TOKEN,
    title: "My First API Article",
    body: "## Introduction\n\nCreated via GetXAPI Node.js client.",
  }),
});
const article = await createResp.json();
console.log("Created article ID:", article.id);

// List to confirm
const listResp = await fetch(`${BASE}/twitter/article/list`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({ auth_token: AUTH_TOKEN }),
});
const { articles } = await listResp.json();
console.log(`Account has ${articles.length} article(s)`);

Get started free: Sign up at getxapi.com/signup for $0.10 in free credits. No credit card. First 10 article create calls on us.


Endpoint 1: GET /twitter/article/get (Fetch Article by ID)

Use this to fetch a single article by its ID. Works for your own drafts and published articles. For public published articles belonging to other accounts, an auth_token may not be required, but passing one is safe and recommended for consistency.

Request parameters:

Parameter Type Required Description
id string Yes Article ID (returned by create, list, or publish)
auth_token string Recommended X session token for your account

Response fields:

Field Type Description
id string Article ID
title string Article title
body string Article body as markdown (may be rich-text blocks for published variant; verify in your usage)
status string "draft" or "published"
cover_image_url string Cover image URL (if set)
author object Author profile (id, username, name)
created_at string ISO 8601 creation timestamp
updated_at string ISO 8601 last-updated timestamp

Python

def get_article(article_id: str) -> dict:
    resp = requests.get(
        f"{BASE}/twitter/article/get",
        params={"id": article_id, "auth_token": AUTH_TOKEN},
        headers=HEADERS,
    )
    resp.raise_for_status()
    return resp.json()

article = get_article("1234567890")
print(f"Title: {article['title']}")
print(f"Status: {article['status']}")
print(f"Body preview: {article['body'][:200]}")

Node.js

async function getArticle(articleId) {
  const params = new URLSearchParams({
    id: articleId,
    auth_token: AUTH_TOKEN,
  });
  const resp = await fetch(`${BASE}/twitter/article/get?${params}`, {
    headers: HEADERS,
  });
  return resp.json();
}

const article = await getArticle("1234567890");
console.log("Title:", article.title);
console.log("Status:", article.status);

Use cases: read own drafts before publishing, mirror published articles into a CMS, check status before calling publish or delete.


Endpoint 2: POST /twitter/article/create (Draft or One-Call Publish)

The create endpoint does two jobs. Without "publish": true it creates a draft. With "publish": true (Premium accounts only) it creates and publishes in one call. This shortcut is the most asked-about feature and is documented nowhere else.

Request body:

Field Type Required Description
auth_token string Yes X session token
title string Yes Article title
body string Yes Markdown body (up to 25,000 characters for Premium accounts)
cover_image_url string No URL of cover image (format: URL string; base64 may also be accepted; verify in docs)
publish boolean No true to publish immediately. Requires X Premium subscription on the account.

Character limits (as of 2026-05-27, per Publer docs): up to 25,000 characters for X Premium accounts. Standard title length follows normal X character constraints.

Python (draft only)

def create_article_draft(title: str, body: str, cover_url: str = None) -> dict:
    payload = {
        "auth_token": AUTH_TOKEN,
        "title": title,
        "body": body,
    }
    if cover_url:
        payload["cover_image_url"] = cover_url
    resp = requests.post(
        f"{BASE}/twitter/article/create",
        json=payload,
        headers=HEADERS,
    )
    resp.raise_for_status()
    return resp.json()

draft = create_article_draft(
    title="The Complete Guide to X Articles in 2026",
    body="## Why X Articles Matter\n\nLong-form content on X reaches a different audience than threads...",
)
print("Draft ID:", draft.get("id"))

Python (one-call publish, Premium accounts)

def create_and_publish(title: str, body: str) -> dict:
    resp = requests.post(
        f"{BASE}/twitter/article/create",
        json={
            "auth_token": AUTH_TOKEN,
            "title": title,
            "body": body,
            "publish": True,  # requires X Premium subscription
        },
        headers=HEADERS,
    )
    resp.raise_for_status()
    return resp.json()

result = create_and_publish(
    title="2026 State of Developer Tools",
    body="## Overview\n\nThis is the complete analysis...",
)
print("Published article ID:", result.get("id"))
print("Status:", result.get("status"))  # expected: "published"

Node.js (one-call publish)

async function createAndPublish(title, body) {
  const resp = await fetch(`${BASE}/twitter/article/create`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
      auth_token: AUTH_TOKEN,
      title,
      body,
      publish: true,
    }),
  });
  return resp.json();
}

const result = await createAndPublish(
  "2026 State of Developer Tools",
  "## Overview\n\nThis is the complete analysis..."
);
console.log("Status:", result.status); // "published" if Premium

Note on pricing: a create call with "publish": true charges the create rate ($0.01). If you call create then publish as two separate calls, you pay $0.01 + $0.005 = $0.015 per article. The one-call shortcut is both cheaper and simpler for Premium accounts.


Endpoint 3: POST /twitter/article/update (Partial Updates)

Update any subset of fields on an existing article. Fields not included in the request body are preserved as-is. This is safe to retry with the same fields.

Request body:

Field Type Required Description
auth_token string Yes X session token
id string Yes Article ID to update
title string No New title (omit to keep current)
body string No New markdown body (omit to keep current)
cover_image_url string No New cover image URL (omit to keep current)

Assumption (verify in your usage): update on a published article is likely allowed, but the recommended safe pattern is unpublish, update, then republish. This avoids any potential state inconsistency during the update window.

Python

def update_article(article_id: str, **fields) -> dict:
    payload = {"auth_token": AUTH_TOKEN, "id": article_id, **fields}
    resp = requests.post(
        f"{BASE}/twitter/article/update",
        json=payload,
        headers=HEADERS,
    )
    resp.raise_for_status()
    return resp.json()

# Update title only
updated = update_article("1234567890", title="Updated Title: 2026 Edition")
print("Updated:", updated.get("id"))

# Update body and cover
updated = update_article(
    "1234567890",
    body="## Revised Content\n\nThis replaces the original body.",
    cover_image_url="https://example.com/new-cover.jpg",
)

Node.js

async function updateArticle(articleId, fields) {
  const resp = await fetch(`${BASE}/twitter/article/update`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
      auth_token: AUTH_TOKEN,
      id: articleId,
      ...fields,
    }),
  });
  return resp.json();
}

await updateArticle("1234567890", {
  title: "Updated Title: 2026 Edition",
  body: "## Revised Content\n\nThis replaces the original body.",
});

Start building with GetXAPI

$0.05 per 1,000 tweets. $0.10 free credits. No credit card required.

Endpoint 4: POST /twitter/article/list (Paginated Draft and Published Listing)

Returns all articles for the authenticated account, paginated by cursor. As of 2026-05-27, server-side status filtering is not documented. The pattern below fetches all pages and filters by status client-side.

Request body:

Field Type Required Description
auth_token string Yes X session token
cursor string No Pagination cursor from previous response
limit integer No Results per page (default: 20)

Python (full pagination loop)

def list_all_articles(status_filter: str = None) -> list[dict]:
    """Fetch all articles, optionally filtered by status ('draft' or 'published')."""
    articles = []
    cursor = None

    while True:
        payload = {"auth_token": AUTH_TOKEN}
        if cursor:
            payload["cursor"] = cursor

        resp = requests.post(
            f"{BASE}/twitter/article/list",
            json=payload,
            headers=HEADERS,
        )
        resp.raise_for_status()
        data = resp.json()

        page = data.get("articles", [])
        articles.extend(page)

        if not data.get("has_more"):
            break
        cursor = data.get("next_cursor")

    # Client-side status filter (server-side filter not documented as of 2026-05-27)
    if status_filter:
        articles = [a for a in articles if a.get("status") == status_filter]

    return articles

drafts = list_all_articles(status_filter="draft")
published = list_all_articles(status_filter="published")
print(f"Drafts: {len(drafts)} | Published: {len(published)}")

Node.js (pagination)

async function listAllArticles(statusFilter = null) {
  const articles = [];
  let cursor = null;

  while (true) {
    const body = { auth_token: AUTH_TOKEN };
    if (cursor) body.cursor = cursor;

    const resp = await fetch(`${BASE}/twitter/article/list`, {
      method: "POST",
      headers: HEADERS,
      body: JSON.stringify(body),
    });
    const data = await resp.json();

    articles.push(...(data.articles || []));
    if (!data.has_more) break;
    cursor = data.next_cursor;
  }

  return statusFilter
    ? articles.filter((a) => a.status === statusFilter)
    : articles;
}

const drafts = await listAllArticles("draft");
console.log(`Drafts: ${drafts.length}`);

Endpoint 5: POST /twitter/article/publish (Premium-Gated)

Moves a draft to published and makes it live on X. Requires X Premium on the account tied to auth_token. X enforces this gate (not GetXAPI): publishing touches the public feed, create/update/list/get do not.

Request body:

Field Type Required Description
auth_token string Yes X session token for a Premium account
id string Yes Article ID to publish

403 response when account is not Premium:

{
  "error": "forbidden",
  "message": "Publishing articles requires an X Premium subscription on this account.",
  "code": 403
}

Python

def publish_article(article_id: str) -> dict:
    resp = requests.post(
        f"{BASE}/twitter/article/publish",
        json={"auth_token": AUTH_TOKEN, "id": article_id},
        headers=HEADERS,
    )
    if resp.status_code == 403:
        raise PermissionError(
            "Publishing requires X Premium. "
            "Account is not Premium or auth_token is for a non-Premium account."
        )
    resp.raise_for_status()
    return resp.json()

try:
    result = publish_article("1234567890")
    print("Published. Status:", result.get("status"))
except PermissionError as e:
    print("Premium required:", e)

Node.js

async function publishArticle(articleId) {
  const resp = await fetch(`${BASE}/twitter/article/publish`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({ auth_token: AUTH_TOKEN, id: articleId }),
  });
  if (resp.status === 403) {
    throw new Error("Publishing requires X Premium on this account.");
  }
  return resp.json();
}

try {
  const result = await publishArticle("1234567890");
  console.log("Published. Status:", result.status);
} catch (err) {
  console.error(err.message);
}

Endpoint 6: POST /twitter/article/unpublish (Revert to Draft)

Moves a published article back to draft. Removes it from the public X feed, preserves all content, keeps the ID. Not a delete. Calling unpublish on an already-draft article returns a no-op. Safe to retry.

Request body:

Field Type Required Description
auth_token string Yes X session token
id string Yes Article ID to unpublish

Python

def unpublish_article(article_id: str) -> dict:
    resp = requests.post(
        f"{BASE}/twitter/article/unpublish",
        json={"auth_token": AUTH_TOKEN, "id": article_id},
        headers=HEADERS,
    )
    resp.raise_for_status()
    result = resp.json()
    print(f"Article {article_id} status: {result.get('status')}")  # "draft"
    return result

Node.js

async function unpublishArticle(articleId) {
  const resp = await fetch(`${BASE}/twitter/article/unpublish`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({ auth_token: AUTH_TOKEN, id: articleId }),
  });
  const result = await resp.json();
  console.log("Status after unpublish:", result.status); // "draft"
  return result;
}

Endpoint 7: POST /twitter/article/delete (Permanent Delete)

Delete is destructive and irreversible. The article is gone from both your draft list and the public feed. There is no trash or recovery. Build a confirmation step into any production workflow that calls this endpoint.

Retry safety: calling delete on an already-deleted article returns a 404. Treat 404 as semantically successful (idempotent intent satisfied) in retry logic.

Request body:

Field Type Required Description
auth_token string Yes X session token
id string Yes Article ID to delete permanently

Python

def delete_article(article_id: str, confirm: bool = False) -> dict:
    if not confirm:
        raise ValueError(
            f"Pass confirm=True to permanently delete article {article_id}. "
            "This action cannot be undone."
        )
    resp = requests.post(
        f"{BASE}/twitter/article/delete",
        json={"auth_token": AUTH_TOKEN, "id": article_id},
        headers=HEADERS,
    )
    if resp.status_code == 404:
        print(f"Article {article_id} already deleted (404 treated as success).")
        return {"status": "already_deleted"}
    resp.raise_for_status()
    return resp.json()

# Explicit confirm required in code to prevent accidents
delete_article("1234567890", confirm=True)

Node.js

async function deleteArticle(articleId, { confirm = false } = {}) {
  if (!confirm) {
    throw new Error(
      `Pass confirm: true to permanently delete article ${articleId}. Cannot be undone.`
    );
  }
  const resp = await fetch(`${BASE}/twitter/article/delete`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({ auth_token: AUTH_TOKEN, id: articleId }),
  });
  if (resp.status === 404) {
    return { status: "already_deleted" };
  }
  return resp.json();
}

await deleteArticle("1234567890", { confirm: true });

The Premium Gate Explained

Premium gate visualization showing which Article API endpoints require X Premium The gate is narrow: only the publish action requires Premium. Every other endpoint is open.

The Premium gate is the single most misunderstood part of the Article API. Here is the full per-endpoint picture:

Endpoint Requires X Premium
GET /twitter/article/get No
POST /twitter/article/create (draft) No
POST /twitter/article/create with publish: true Yes
POST /twitter/article/update No (draft edits; verify for published)
POST /twitter/article/list No
POST /twitter/article/publish Yes
POST /twitter/article/unpublish No
POST /twitter/article/delete No

The design implication: you can build a complete draft management system, a CMS sync pipeline, a content review workflow, and an Article archive without any X Premium requirement. Only the final "go live" step needs Premium. This matters for multi-account tools: you can stage and manage articles on non-Premium accounts, then route only the publish call through a Premium account token.

Start without Premium: Get your free API key, build your draft workflow, and upgrade only when you are ready to publish. The first 10 create calls are covered by your $0.10 signup credit.


The cheapest Twitter API. Try it free.

$0.05 per 1,000 tweets. $0.10 free credits. No credit card required.

Draft vs Published: The Article State Machine

Article state machine diagram showing transitions between draft, published, and deleted states State machine for the full Article lifecycle. Solid arrows are free. Dashed arrows require Premium.

The Article lifecycle has three states: draft, published, and deleted (terminal). Here is every valid transition:

[create]      --> Draft
Draft         --[update]--> Draft         (anytime, no Premium)
Draft         --[publish]--> Published    (requires X Premium)
Published     --[update]--> Published     (verify allowed; safe pattern: unpublish first)
Published     --[unpublish]--> Draft      (no Premium required)
Draft         --[delete]--> (gone)        (irreversible)
Published     --[delete]--> (gone)        (irreversible)

Retry safety per transition:

Transition Retry behavior
create Each call creates a NEW draft. Not safe to blind-retry on network error. Check list first.
update Same fields safe to retry. Fields omitted are preserved, not cleared.
publish Safe to retry. Already-published article is a no-op.
unpublish Safe to retry. Already-draft article is a no-op.
delete Returns 404 on second call. Treat 404 as semantically successful.

The status field in every get and list response returns "draft" or "published". Check it before publish or unpublish to avoid redundant calls.


Full Python Workflow for the Twitter Article API

Happy path workflow diagram: create draft, update cover, publish, list to confirm Complete happy-path flow: four calls, one article live on X.

This is the production-ready script. It reads a markdown file from disk, creates a draft, adds a cover image, publishes (Premium required), and lists to confirm. Handles errors at each step.

import os
import sys
import time
import requests

# --- Configuration ---
GETXAPI_KEY = os.environ["GETXAPI_KEY"]
AUTH_TOKEN = os.environ["X_AUTH_TOKEN"]
BASE = "https://api.getxapi.com"
HEADERS = {
    "Authorization": f"Bearer {GETXAPI_KEY}",
    "Content-Type": "application/json",
}


def api_post(path: str, payload: dict) -> dict:
    """POST with basic retry on 5xx."""
    url = f"{BASE}{path}"
    for attempt in range(3):
        resp = requests.post(url, json=payload, headers=HEADERS, timeout=20)
        if resp.status_code >= 500:
            time.sleep(2 ** attempt)
            continue
        if resp.status_code == 403:
            raise PermissionError(f"403 on {path}: {resp.json().get('message', 'Forbidden')}")
        resp.raise_for_status()
        return resp.json()
    raise RuntimeError(f"Failed after 3 attempts: {path}")


def api_get(path: str, params: dict) -> dict:
    url = f"{BASE}{path}"
    resp = requests.get(url, params=params, headers=HEADERS, timeout=20)
    resp.raise_for_status()
    return resp.json()


def run_article_workflow(
    md_file: str,
    title: str,
    cover_image_url: str = None,
    publish: bool = False,
) -> str:
    """
    Full Article workflow:
    1. Read markdown from file
    2. Create draft
    3. Update cover image (if provided)
    4. Publish (if publish=True and account is Premium)
    5. List to confirm
    Returns the article ID.
    """
    # 1. Read markdown body
    with open(md_file, "r", encoding="utf-8") as f:
        body = f.read()
    print(f"[1/4] Read {len(body)} chars from {md_file}")

    # 2. Create draft
    create_payload = {"auth_token": AUTH_TOKEN, "title": title, "body": body}
    draft = api_post("/twitter/article/create", create_payload)
    article_id = draft.get("id")
    print(f"[2/4] Draft created: {article_id}")

    # 3. Update cover image (separate call to keep create simple)
    if cover_image_url:
        api_post(
            "/twitter/article/update",
            {"auth_token": AUTH_TOKEN, "id": article_id, "cover_image_url": cover_image_url},
        )
        print(f"[3/4] Cover image set: {cover_image_url}")
    else:
        print("[3/4] No cover image provided, skipping.")

    # 4. Publish (requires X Premium on AUTH_TOKEN account)
    if publish:
        try:
            result = api_post(
                "/twitter/article/publish",
                {"auth_token": AUTH_TOKEN, "id": article_id},
            )
            print(f"[4/4] Published. Status: {result.get('status')}")
        except PermissionError as exc:
            print(f"[4/4] Publish skipped: {exc}")
            print("       Article saved as draft. Upgrade to X Premium to publish.")
    else:
        print("[4/4] Publish=False. Article saved as draft.")

    # 5. List to confirm
    list_data = api_post("/twitter/article/list", {"auth_token": AUTH_TOKEN})
    all_ids = [a.get("id") for a in list_data.get("articles", [])]
    if article_id in all_ids:
        print(f"Confirmed: article {article_id} appears in account list.")
    else:
        print(f"Warning: article {article_id} not found in list response.")

    return article_id


if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("Usage: python article_workflow.py <markdown_file> <title> [cover_url]")
        sys.exit(1)

    article_id = run_article_workflow(
        md_file=sys.argv[1],
        title=sys.argv[2],
        cover_image_url=sys.argv[3] if len(sys.argv) > 3 else None,
        publish=os.environ.get("PUBLISH_LIVE", "false").lower() == "true",
    )
    print(f"\nDone. Article ID: {article_id}")

Run it:

# Draft only
python article_workflow.py my-article.md "The 2026 Developer Tools Guide"

# Draft with cover
python article_workflow.py my-article.md "The 2026 Developer Tools Guide" https://cdn.example.com/cover.jpg

# Publish live (Premium required)
PUBLISH_LIVE=true python article_workflow.py my-article.md "The 2026 Developer Tools Guide"

Scheduled Publishing Workflow

Scheduled publish pattern: create draft now, store ID, trigger publish at future time APScheduler pattern for time-zone-targeted article releases.

X does not offer a native scheduled-publish parameter in the Article API. The standard workaround: create the draft now, store the article ID, and run a scheduler to call publish at the right time. This covers newsletter syndication, content campaign batching, and time-zone-targeted releases.

This uses APScheduler (pip install apscheduler):

import os
import requests
from datetime import datetime, timezone
from apscheduler.schedulers.blocking import BlockingScheduler

GETXAPI_KEY = os.environ["GETXAPI_KEY"]
AUTH_TOKEN = os.environ["X_AUTH_TOKEN"]
HEADERS = {"Authorization": f"Bearer {GETXAPI_KEY}"}
BASE = "https://api.getxapi.com"


def create_draft(title: str, body: str) -> str:
    resp = requests.post(
        f"{BASE}/twitter/article/create",
        json={"auth_token": AUTH_TOKEN, "title": title, "body": body},
        headers=HEADERS,
    )
    resp.raise_for_status()
    return resp.json()["id"]


def publish_at_time(article_id: str) -> None:
    resp = requests.post(
        f"{BASE}/twitter/article/publish",
        json={"auth_token": AUTH_TOKEN, "id": article_id},
        headers=HEADERS,
    )
    if resp.status_code == 403:
        print("Publish failed: account is not X Premium.")
        return
    resp.raise_for_status()
    print(f"Published article {article_id} at {datetime.now(timezone.utc).isoformat()}")


# 1. Create the draft immediately
article_id = create_draft(
    title="Weekly Industry Roundup: May 2026",
    body="## This Week in Tech\n\nHere are the five stories that mattered most...",
)
print(f"Draft saved. ID: {article_id}")

# 2. Schedule publish for a specific UTC time
scheduler = BlockingScheduler()
scheduler.add_job(
    publish_at_time,
    "date",
    run_date=datetime(2026, 5, 28, 9, 0, 0, tzinfo=timezone.utc),
    args=[article_id],
)
print("Scheduler running. Article will publish at 09:00 UTC on 2026-05-28.")
scheduler.start()

For production, swap BlockingScheduler for BackgroundScheduler and run it inside your server process. Or store the article_id in a database and use a cron job to call publish at the right time. Key use cases: newsletter syndication, content campaign batching, and time-zone targeting. For more on distributing Twitter content programmatically, see the Twitter followers export API guide.


Multi-Account Management and Pricing

Pricing comparison chart: getxapi Article API vs official X API Enterprise vs twitterapi.io As of 2026-05-27. Official X API has no Article endpoints at any tier.

Multi-account routing

Each API call carries an auth_token bound to one X account. To manage articles across multiple accounts, store one auth_token per account and route per-call:

import os

# Store auth tokens per account
ACCOUNTS = {
    "brand_main": os.environ["X_AUTH_TOKEN_BRAND"],
    "brand_eu": os.environ["X_AUTH_TOKEN_BRAND_EU"],
    "founder": os.environ["X_AUTH_TOKEN_FOUNDER"],
}


def publish_to_account(account_key: str, article_id: str) -> dict:
    auth = ACCOUNTS[account_key]
    resp = requests.post(
        f"{BASE}/twitter/article/publish",
        json={"auth_token": auth, "id": article_id},
        headers=HEADERS,
    )
    resp.raise_for_status()
    return resp.json()


# Publish the same article to multiple accounts
for account in ["brand_main", "brand_eu"]:
    result = publish_to_account(account, "1234567890")
    print(f"{account}: {result.get('status')}")

Note: auth_token is account-specific. An article created under brand_main can only be published, updated, or deleted by a call that includes brand_main's auth token.

Pricing comparison (as of 2026-05-27)

Endpoint Official X API v2 twitterapi.io GetXAPI
GET article Not available Not available $0.001
POST create Not available Not available $0.01
POST publish / update / list / unpublish / delete Not available Not available $0.005 each

The official X API has no Article endpoints at any tier, including Enterprise ($42,000/month per the official X pricing page). twitterapi.io has no Article endpoints as of 2026-05-27.

Real workload math:

  • 1,000 articles via create-with-publish (one call): 1,000 x $0.01 = $10/month
  • 1,000 articles via create then separate publish: $15/month
  • 10,000 article reads (GET): $10/month

For the full Twitter API pricing breakdown, see the pricing guide. For an independent comparison of third-party providers, see the best Twitter API for scraping guide.


Error Handling, Pagination, and Rate Limits

Pagination flow diagram showing cursor-based traversal through article list responses Cursor-based pagination. Each page returns next_cursor and has_more.

Common errors

HTTP Status Error Cause Fix
401 Unauthorized Invalid GETXAPI_KEY Check your API key in the dashboard
401 Invalid auth_token X session token expired or malformed Re-authenticate via /twitter/user_login or refresh from browser cookies
403 Premium required Account tied to auth_token is not X Premium Upgrade the X account or use a Premium account's token
404 Article not found ID does not exist, or belongs to a different account Verify ID via /twitter/article/list
429 Rate limited Upstream X rate limit surfaced through GetXAPI Retry with exponential backoff (see snippet below)
500+ Server error Transient upstream error Retry up to 3 times with exponential backoff

Retry with exponential backoff

import time
import requests

def post_with_backoff(path: str, payload: dict, max_retries: int = 3) -> dict:
    url = f"{BASE}{path}"
    delay = 1.0
    for attempt in range(max_retries):
        resp = requests.post(url, json=payload, headers=HEADERS, timeout=20)
        if resp.status_code == 429:
            wait = float(resp.headers.get("Retry-After", delay * 2))
            print(f"Rate limited. Waiting {wait}s before retry.")
            time.sleep(wait)
            continue
        if resp.status_code >= 500:
            time.sleep(delay)
            delay *= 2
            continue
        if resp.status_code == 403:
            raise PermissionError(resp.json().get("message", "Forbidden"))
        resp.raise_for_status()
        return resp.json()
    raise RuntimeError(f"Exhausted {max_retries} retries on {path}")

Rate limit context

GetXAPI has no platform-level rate limit caps. Upstream X may surface 429 responses during heavy write bursts. Read operations (list, get) are unlikely to hit limits at normal volumes. Bulk write operations (100+ creates/hour) should include 1 to 2 second delays between calls.

See the GetXAPI best practices guide for production patterns. For data pipelines combining Article reads with tweet-level signals, see the Twitter sentiment analysis Python guide.


Migrating from the Legacy /twitter/tweet/article Endpoint

Migration map showing legacy paths mapped to new Article API paths Legacy endpoint retired 2026-05-17. One-line path and param change required.

The legacy GET /twitter/tweet/article endpoint (from the twitterapi.io compatibility layer) was retired on 2026-05-17 when the new Article endpoints launched. If your code calls the legacy path, you will receive a 404 or deprecation error.

Migration table:

Legacy New Change required
GET /twitter/tweet/article GET /twitter/article/get Path change + param rename
Param: tweet_id Param: id Rename tweet_id to id

Before (legacy):

# Legacy pattern (retired 2026-05-17)
resp = requests.get(
    "https://api.getxapi.com/twitter/tweet/article",
    params={"tweet_id": "1234567890", "auth_token": AUTH_TOKEN},
    headers=HEADERS,
)

After (new endpoint):

# New pattern (live as of 2026-05-17)
resp = requests.get(
    "https://api.getxapi.com/twitter/article/get",
    params={"id": "1234567890", "auth_token": AUTH_TOKEN},
    headers=HEADERS,
)

Node.js migration:

// Before
const params = new URLSearchParams({ tweet_id: "1234567890", auth_token: AUTH_TOKEN });
const resp = await fetch(`${BASE}/twitter/tweet/article?${params}`, { headers: HEADERS });

// After
const params = new URLSearchParams({ id: "1234567890", auth_token: AUTH_TOKEN });
const resp = await fetch(`${BASE}/twitter/article/get?${params}`, { headers: HEADERS });

Two-line diff: update the path string, rename tweet_id to id. Response shape is compatible. For migrating all endpoints from twitterapi.io, see the full migration guide.


Reliability and support: GetXAPI has maintained 99.9% uptime across all endpoints since launch. The status page at status.getxapi.com shows real-time endpoint health and incident history. Article endpoints launched 2026-05-17 and are under active maintenance with a public changelog you can subscribe to for release notifications. Support is available via Discord and email for integration questions.

What to build next: CMS webhook on post publish triggers Article create. Newsletter send triggers Article publish. Draft review gates hold articles until approved. Full Article history mirrors into your database for search. For more patterns, see the complete Python Twitter API tutorial, the Twitter DM API guide, and the Twitter API v2 vs GetXAPI comparison.

Start using the Twitter Article API in 60 seconds. Sign up free at getxapi.com/signup.


Frequently Asked Questions

The X Article API manages long-form Articles on X (up to 25,000 characters, markdown-formatted, with cover images). Tweet endpoints handle short-form posts (280 characters) and have partial official X API v2 coverage. Article endpoints have no official X API v2 equivalent at any tier. GetXAPI is the only provider shipping all 7 Article lifecycle endpoints as of 2026-05-27. Create, edit, and list work without X Premium. Publishing to the public feed requires it.

A draft article is stored privately in your account. It is not visible to anyone else on X. A published article is live on the public X feed, linked from your profile, and accessible via the article URL. You move between states using the publish and unpublish endpoints. The `status` field in every get and list response tells you the current state: `"draft"` or `"published"`. Deleting either state is permanent and cannot be reversed.

Unpublish moves a published article back to draft status. The article disappears from the public X feed, but all content (title, body, cover image) is preserved under the same article ID. You can republish later without losing any work. Delete is permanent: the article is removed from both the public feed and your draft list, and the ID is no longer valid. There is no recovery from delete. Use unpublish when you want to retract temporarily or edit before republishing. Use delete only when the article is no longer needed at all.

Yes. GetXAPI replaces the official OAuth 2.0 flow with two credentials: a GetXAPI API key (from your GetXAPI dashboard, no developer account required) and an `auth_token` (an X session token from your browser cookies or the `/twitter/user_login` endpoint). No OAuth callback URL. No token-refresh logic. No developer account application. Sign up at [getxapi.com/signup](/signup) and you have an API key in under a minute.

X Premium accounts support up to 25,000 characters in the article body. The body accepts markdown formatting (headers, bold, italic, links, code blocks, lists). Cover images are set via a URL string in the `cover_image_url` field. Article title length follows standard X constraints. These limits apply regardless of whether you create through the web UI or the API. For non-Premium accounts, the publish step is blocked entirely, so the practical character limit question is only relevant for Premium users.

The official X API v2 has no Article endpoints at any tier. GetXAPI has no subscription tiers: you pay per call. The only X-side requirement is that the account tied to `auth_token` must have X Premium for the publish step. Every new GetXAPI account gets $0.10 in free credits to test all 7 endpoints before committing.

For most operations, no. Creating a draft, updating a draft, listing your articles, getting an article by ID, unpublishing, and deleting all work without X Premium. The single operation that requires X Premium is publishing: both `POST /twitter/article/publish` and `POST /twitter/article/create` with `"publish": true` will return a 403 if the account tied to the `auth_token` is not an X Premium subscriber. You can build a full draft management pipeline and only upgrade when you are ready to go live.

Yes. Pass `"publish": true` in the body of `POST /twitter/article/create`. This creates the article and publishes it immediately in one call at the create rate ($0.01). The account tied to your `auth_token` must have an active X Premium subscription, or the call returns a 403 error. Without `"publish": true`, the call creates a draft only (no Premium required). The one-call shortcut is also slightly cheaper than calling create and then publish separately ($0.01 vs $0.015 total for two-call flow).

The official X API v2 has no Article endpoints at any pricing tier, including Enterprise ($42,000/month). GetXAPI charges $0.001 per GET, $0.01 per CREATE, and $0.005 per PUBLISH/UPDATE/LIST/UNPUBLISH/DELETE call. For a workload of 1,000 published articles per month via the one-call create-and-publish shortcut, total cost is $10. For the full pricing comparison across all endpoint types, see the [Twitter API pricing guide](/blogs/twitter-api-cost). For an independent comparison of third-party providers on the scraping side, see the [best Twitter API for scraping guide](/blogs/best-twitter-api-for-scraping).

Call `POST /twitter/article/list` with your `auth_token`. The endpoint returns paginated results with `has_more` and `next_cursor` fields. Loop over pages until `has_more` is false. Each article object includes an `id`, `title`, `status` (`"draft"` or `"published"`), and timestamps. Server-side status filtering is not documented as of 2026-05-27, so filter by `status` on the client side after fetching all pages. The pagination loop in the Endpoint 4 section above shows the full pattern in Python and Node.js.

Yes. Set up a webhook in your CMS that fires on post publish, calls your Article workflow script with the post body as markdown, and calls `POST /twitter/article/create`. For WordPress, the `publish_post` hook triggers the workflow. Webflow, Ghost, and Contentful all support outgoing webhooks that connect to the same script. The [scheduled publishing workflow](#scheduled-publishing-workflow) section shows how to decouple CMS publish time from X Article publish time.

Delete is permanent. The article ID is gone from your account and the public feed. A second delete returns 404 (treat as idempotent success). Unpublish is reversible: it moves the article to draft, preserves all content under the same ID, and removes it from the public feed. A second unpublish on an already-draft article is a no-op. Use unpublish as the default retraction. Reserve delete only when the article should not exist in any form.

Check out similar blogs

More guides on the Twitter/X API, scraping, and pricing.

Python Twitter API tutorial — full working code samples for 2026
PythonTwitter API

How to Use the Twitter API with Python — 2026 Tutorial

Step-by-step Python tutorial for the Twitter API in 2026. Working code for search, users, DMs, pagination, retries — plus a tweepy migration guide.

GetXAPI·
Export Twitter followers via API 2026 cover: futuristic glowing circuitry representing data extraction at scale
Twitter APIX API

How to Export Twitter Followers via API in 2026 ($0.001/Call, No OAuth)

Export any public Twitter (X) account's followers at $0.001 per call. Bearer token, 200 followers per page, full profile data. Python + Node code, real pricing math.

GetXAPI·
Twitter sentiment analysis Python tutorial — TextBlob, VADER, and RoBERTa compared on real tweets
Twitter APISentiment Analysis

Twitter Sentiment Analysis in Python — Full Tutorial

Step-by-step Python tutorial for Twitter sentiment analysis. Compare TextBlob, VADER, and transformer models on real tweets. Code, costs, and pitfalls covered.

GetXAPI·
Twitter Search API guide and advanced search operators reference for 2026
Twitter Search APITwitter API

Twitter Search API & Advanced Operators (2026 Guide)

Twitter Search API guide for 2026 — every advanced search operator (from:, to:, min_faves:, since:, until:) with working code in curl, Python, JavaScript.

GetXAPI·
twitterapi.io alternative — migrate to GetXAPI guide for 2026
twitterapi.io alternativeGetXAPI

twitterapi.io Alternative — Migrate to GetXAPI 3x Cheaper

twitterapi.io alternative migration guide — cut your Twitter API bill 3x without rewriting. Step-by-step base URL, auth header, and response-shape mapping.

GetXAPI·
Twitter scraping best practices for production workflows in 2026
Twitter ScrapingTwitter API

Twitter Scraping — Best Practices for Production in 2026

Production-grade Twitter scraping patterns — retry logic, pagination, proxy strategy, rate-limit handling, and cost optimization for any third-party API.

GetXAPI·
Twitter API cost in 2026 — complete pricing guide and ROI scenarios
Twitter APIX API

Twitter API Cost 2026: $0.05/1K Tweets to $42K Tiers

X moved to pay-per-use in 2026 — tiers from $200/mo to $42K. See real monthly bills + $0.05/1K tweet alternatives that cut costs 90%+. Includes ROI.

GetXAPI·
Apify Twitter Scraper vs GetXAPI comparison 2026
Twitter APIApify

Apify Twitter Scraper vs GetXAPI: Cost, Speed and Reliability in 2026

Head-to-head comparison of Apify Twitter Scraper and GetXAPI REST API. Cost per 1,000 tweets, response time benchmarks, rate limits, and when to pick each one.

GetXAPI·