Skip to main content

xcrap-sdk — the official Python client for XCrap: X (Twitter) posts, threads, profiles and search as dataclasses or Markdown

PyPI version No API key Python 3.9 or newer Typed MIT license GitHub stars

X (Twitter) posts, threads, profiles, search and media — as typed dataclasses, or Markdown you can paste straight into a model.
No signup · no OAuth · no bearer token · sync and async

Website · API reference · SDK guide · Examples · Node SDK · MCP server


Contents

Why this SDK

🔑 No API key No signup, no OAuth, no bearer token. Xcrap() and you are reading.
⚡ Sync and async Xcrap and AsyncXcrap, same surface, both context managers.
🧩 Typed Dataclasses mirroring the API's shapes, full hints, py.typed.
🪶 One dependency httpx.
📝 Markdown-first markdown=True on every read method, with a token estimate in client.last_meta.markdown_tokens.
🧠 Optional response cache Xcrap(cache_ttl=60) keeps answers in memory, so repeats cost no request.
🚦 Rate-limit aware client.rate_limit after every call, and a typed XcrapRateLimited with retry_after.

Install

pip install xcrap-sdk
Other package managers
uv add xcrap-sdk
poetry add xcrap-sdk
pipenv install xcrap-sdk
from xcrap import Xcrap, AsyncXcrap

xcrap = Xcrap()  # talks to https://xcrap.cc

Both clients are context managers, which is the tidiest way to close the connection pool:

with Xcrap() as xcrap:
    ...

async with AsyncXcrap() as xcrap:
    ...

How it works

sequenceDiagram
    autonumber
    participant App as Your code
    participant SDK as xcrap (Python)
    participant API as XCrap API
    participant X as X (public posts)
    App->>SDK: xcrap.thread(url, max_tweets=50)
    alt cache_ttl set and answer still fresh
        SDK-->>App: served from memory (no request)
    else
        SDK->>API: GET /v1/thread?url=…
        API->>X: read the public thread
        X-->>API: posts
        API-->>SDK: JSON + x-ratelimit-* headers
        SDK-->>App: Thread dataclass, client.rate_limit updated
    end

Pick the shape you need with format= — JSON comes back as dataclasses, everything else as a str:

flowchart LR
    link["🔗 x.com link, id or handle"] --> api(["XCrap API"])
    api --> json["JSON<br/><sub>dataclasses</sub>"]
    api --> md["Markdown<br/><sub>for LLM prompts</sub>"]
    api --> yaml["YAML"]
    api --> csv["CSV<br/><sub>pandas, sheets</sub>"]
    api --> html["HTML<br/><sub>embeds</sub>"]
    classDef accent fill:#f62d00,stroke:#141312,color:#ffffff
    class api accent

Quick start

from xcrap import Xcrap

with Xcrap() as xcrap:
    tweet = xcrap.tweet("https://x.com/jack/status/20")

    print(tweet.text)                   # just setting up my twttr
    print(tweet.author.screen_name)     # jack
    print(tweet.metrics.likes)          # 308067

A bare id works too: xcrap.tweet("20").

Recipes

📝 Feed a post to an LLM as Markdown
with Xcrap() as xcrap:
    markdown = xcrap.tweet("https://x.com/jack/status/20", markdown=True)
    print(markdown)
    print(f"~{xcrap.last_meta.markdown_tokens} tokens")  # budget your context window

Every read method takes markdown=True, or format="json" | "markdown" | "yaml" | "csv" | "html". JSON comes back as dataclasses; every other format comes back as a str.

🧵 Unroll a thread
with Xcrap() as xcrap:
    thread = xcrap.thread(
        "https://x.com/naval/status/1002103360646823936",
        max_tweets=50,
    )

    print(f"@{thread.author.screen_name} — {thread.count} posts")
    for index, post in enumerate(thread.tweets, start=1):
        print(f"{index}. {post.text}")

    if thread.truncated:
        print("…thread was longer than max_tweets")
📜 Page an account's timeline
with Xcrap() as xcrap:
    # One page at a time, cursor in hand:
    page = xcrap.user_tweets("nasa", count=20, exclude_replies=True)
    while page.next_cursor:
        for post in page.tweets:
            print(post.created_at, post.text[:60])
        page = xcrap.user_tweets("nasa", count=20, cursor=page.next_cursor)

    # Or let the SDK walk the pages for you:
    for post in xcrap.iter_user_tweets("nasa", count=20, limit=100):
        print(post.id, post.text[:60])
🗂️ Export an account's history (with reposts)
with Xcrap() as xcrap:
    history = xcrap.user_history(
        "naval",
        max_posts=500,
        since="2025-01-01",
        include_reposts=True,
    )

    print(f"{history.count} posts")
    print("stopped because:", history.stop_reason)  # max_posts | window | end_of_timeline | page_limit | upstream_error
🔎 Search with X's own operators
with Xcrap() as xcrap:
    result = xcrap.search("from:nasa mars", feed="top", since="2025-01-01")
    for post in result.tweets:
        print(post.text)

    if result.next_cursor:
        more = xcrap.search("from:nasa mars", feed="top", cursor=result.next_cursor)

feed is latest (default), top, photos or videos.

🖼️ Download media
with Xcrap() as xcrap:
    url = "https://x.com/NASA/status/1793412932641689801"

    listing = xcrap.media(url)
    print(f"{listing.count} attachment(s)")

    for item in listing.media:
        file = xcrap.download_media(url, index=item.index, quality="best")
        saved = file.save()          # uses the suggested filename
        print(f"saved {saved} ({file.content_type}, {file.content_length} bytes)")
📦 Resolve fifty posts in one call
with Xcrap() as xcrap:
    result = xcrap.bulk([
        "https://x.com/jack/status/20",
        "https://x.com/nasa/status/1",
        "1849565662058590299",
    ])

    print(f"{result.succeeded} ok, {result.failed} failed")
    for item in result.results:
        if item.ok:
            print("✓", item.tweet.text)
        else:
            print("✗", item.input, item.error["message"])

Bulk takes at most 50 URLs and never fails the whole batch for one bad link.

⚡ Async, with concurrency
import asyncio
from xcrap import AsyncXcrap

async def main() -> None:
    async with AsyncXcrap() as xcrap:
        handles = ["nasa", "jack", "esa"]
        users = await asyncio.gather(*(xcrap.user(h) for h in handles))
        for user in users:
            print(f"@{user.screen_name}: {user.metrics.followers:,} followers")

        async for post in xcrap.iter_user_tweets("nasa", limit=50):
            print(post.text[:60])

asyncio.run(main())
🚨 Handle errors by type
from xcrap import (
    Xcrap,
    XcrapBadRequest,
    XcrapNotFound,
    XcrapOptedOut,
    XcrapRateLimited,
    XcrapUpstreamError,
)
import time

with Xcrap() as xcrap:
    try:
        tweet = xcrap.tweet("https://x.com/jack/status/20")
    except XcrapNotFound as error:
        print("Gone for good:", error)
    except XcrapRateLimited as error:
        print(f"Budget spent, sleeping {error.retry_after}s")
        time.sleep(error.retry_after)
    except XcrapOptedOut:
        print("Account opted out — do not retry")
    except XcrapUpstreamError:
        print("Transient, retry in a minute")
    except XcrapBadRequest as error:
        print("Fix the input:", error.hint)

Every message ends with the action that resolves it, so it is safe to surface directly to a user or an agent.

🚦 Watch your rate-limit budget
with Xcrap() as xcrap:
    xcrap.trends(count=10)

    limit = xcrap.rate_limit
    print(f"{limit.remaining}/{limit.limit} left, window resets at {limit.reset_at:%H:%M:%S}")

What's new in 1.2.0

🧠 In-memory response cache Xcrap(cache_ttl=60) keeps successful GET responses for that many seconds. A repeat costs no request and no rate limit. fresh=True always skips it; clear_cache() empties it.
🔁 include_reposts user_history(handle, include_reposts=True) also returns the posts the account reposted.
🛑 stop_reason History responses say why the walk stopped: max_posts, window, end_of_timeline, page_limit or upstream_error (the posts collected until then are still returned).
with Xcrap(cache_ttl=60) as xcrap:  # one minute
    xcrap.user("nasa")  # one request
    xcrap.user("nasa")  # answered from memory
    xcrap.clear_cache()

API

Xcrap(base_url=..., **options) / AsyncXcrap(base_url=..., **options)

option default meaning
base_url https://xcrap.cc API origin. Leave it; change it only to route through a proxy you control
timeout 30.0 Per-request timeout, seconds
retries 1 Retries on 502/503/504 and transport errors
retry_delay 0.5 Backoff before a retry, seconds
cache_ttl 0.0 Keep successful GET responses in memory this many seconds; 0 is off
user_agent xcrap-python-sdk/… Replaces the default descriptive agent
headers {} Extra headers on every request
client None Bring your own httpx.Client / AsyncClient

Methods

method endpoint returns
tweet(url, ...) /v1/tweet Tweet
thread(url, max_tweets=25, ...) /v1/thread Thread
user(handle, ...) /v1/user User
user_tweets(handle, count=20, cursor=None, exclude_replies=True, media_only=False, ...) /v1/user/tweets Timeline
iter_user_tweets(handle, limit=None, ...) /v1/user/tweets iterator of Tweet
user_history(handle, max_posts=200, since=None, until=None, include_replies=False, include_reposts=False, ...) /v1/user/history AccountHistory
search(query, feed="latest", since=None, until=None, cursor=None, ...) /v1/search SearchResult
replies(url, sort="top", ...) /v1/replies Replies
followers(handle, cursor=None, ...) /v1/user/followers UserList
following(handle, cursor=None, ...) /v1/user/following UserList
trends(count=20, ...) /v1/trends TrendsResult
media(url, ...) /v1/media MediaList
download_media(url, index=0, quality="best") /v1/media/download DownloadedMedia
bulk(urls, ...) /v1/bulk BulkResult (≤ 50 urls)
clear_cache() — empties the in-memory response cache

Every read method accepts markdown=, format= and fresh=. fresh=True bypasses both the server's five-day cache and the client cache. AsyncXcrap has the same methods as coroutines, and iter_user_tweets becomes an async iterator.

Client state

  • client.rate_limit — RateLimit(limit, remaining, reset, reset_at) from the last response.
  • client.last_meta — ResponseMeta (format, cache status, markdown_tokens) from the last response.

Errors

class status when
XcrapBadRequest 400 missing or unparseable parameter
XcrapNotFound 404 deleted, suspended, private or nonexistent
XcrapRateLimited 429 budget spent — carries retry_after, reset_at, budget
XcrapOptedOut 451 the account opted out of XCrap
XcrapUpstreamError 5xx X could not be reached or timed out; usually transient
XcrapConnectionError — DNS, TLS, socket or timeout failure
XcrapError — base class for all of the above
flowchart TD
    E{{"XcrapError"}} --> B["XcrapBadRequest · 400<br/><sub>fix the input</sub>"]
    E --> N["XcrapNotFound · 404<br/><sub>do not retry</sub>"]
    E --> R["XcrapRateLimited · 429<br/><sub>sleep retry_after</sub>"]
    E --> O["XcrapOptedOut · 451<br/><sub>final</sub>"]
    E --> U["XcrapUpstreamError · 5xx<br/><sub>retry shortly</sub>"]
    E --> C["XcrapConnectionError<br/><sub>check the network</sub>"]
    classDef accent fill:#f62d00,stroke:#141312,color:#ffffff
    class E accent

Rate limits

Budgets are per endpoint, per IP, and every response carries x-ratelimit-limit / -remaining / -reset.

endpoint method(s) budget
/v1/tweet tweet 45 / minute
/v1/user user 45 / minute
/v1/thread thread 15 / minute
/v1/user/tweets user_tweets, iter_user_tweets 15 / minute
/v1/user/followers, /v1/user/following followers, following 15 / minute
/v1/replies replies 15 / minute
/v1/user/history user_history 4 / 5 minutes
/v1/search search 10 / 15 minutes
/v1/bulk bulk 6 / 5 minutes (up to 50 posts each)
/v1/media, /v1/media/download media, download_media 20 / minute
/v1/trends trends 90 / minute

Examples

Small, runnable projects live in examples/. Each works as soon as you install it — there is no key to set up.

Example What it does Extra dependency
💻 cli/ xcrap-cli tweet, thread, user, search and history from your terminal, saving to CSV, Markdown or JSON none (argparse)
🎮 discord-bot/ Turns X links into clean embeds and sends !thread as a Markdown file discord.py 2.x
✈️ telegram-bot/ /tweet, /thread and automatic link expansion python-telegram-bot v21
🗄️ thread-archiver/ Saves a list of threads to archive/<handle>-<id>.md, waiting out rate limits none
🤖 ai-summary/ Builds a ready-to-send summary prompt from a thread, for any LLM none
cd examples/cli
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python xcrap_cli.py tweet https://x.com/jack/status/20

Stop a running bot with Ctrl + C.

Tests

pip install -e ".[test]"
pytest -v

Most of the suite is an integration smoke test: it talks to the live API, because that is the only thing that proves the client and the API still agree.

Repository What it is
🟩 xcrap-node The Node.js SDK — npm install @xcrapcc/sdk
🤖 Xcrap-mcp The MCP server for Claude, Cursor and other agents — npx -y @xcrapcc/mcp
📖 xcrap-docs Guides, API reference, OpenAPI 3.1, llms.txt and skills.md
🏠 XcrapCC Everything XCrap on GitHub

xcrap.cc · Docs · OpenAPI · llms.txt · skills.md
MIT licensed · Public data only · Not affiliated with X Corp.

Metadata

Release files for xcrap-sdk 1.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for xcrap-sdk 1.2.0
File Size Uploaded
xcrap_sdk-1.2.0.tar.gz 26.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for xcrap-sdk 1.2.0
File Interpreter ABI Platform
xcrap_sdk-1.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 52.7 kB

Release files / xcrap_sdk-1.2.0.tar.gz

Download URL xcrap_sdk-1.2.0.tar.gz
Size 26.4 kB
Tags Source
SHA-256 checksum
How to use checksums
9ede4dbdd1256035832290038b7f679afc53172ffe0771b7b27c07a2f20387ea
BLAKE2b-256 checksum
How to use checksums
d0bbac827c69e841ece4f904b844c951e5f016d242f7aac6e706abb268b00483
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / xcrap_sdk-1.2.0-py3-none-any.whl

Download URL xcrap_sdk-1.2.0-py3-none-any.whl
Size 26.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
976420e0722dd5506dd5fa32a337a4ac2e98a550aea487588d48b2f5840bcb47
BLAKE2b-256 checksum
How to use checksums
34c480c80af78a865dd779465832e04eccc335013314a1d70617cd841b3c497b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

1.3.0

2 release files

This release

1.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page