xdk-rs
Async Rust client for the X (Twitter) API v2: credentials held in code, one typed call per endpoint, OAuth1, OAuth2
PKCE, bearer tokens, media upload, and streaming. xdk is the name X uses for its own SDKs; this is an independent
project, not affiliated with, endorsed by, or maintained by X. The xr command-line tool
(xurl-rs) is built on this crate.
Quick start
The package is xdk-rs; the library it installs is xdk:
Those two commands leave this in Cargo.toml. #[tokio::main] needs both tokio features:
[]
= "0.1"
= { = "1", = ["macros", "rt-multi-thread"] }
A complete program: the app-only bearer token from the
X developer portal in XURL_BEARER_TOKEN, one search, the text printed.
use Client;
async
A bearer token is app-only: it reads public data (search, post and user lookups) and cannot act as a user, so get_me,
create_post, likes, follows, and direct messages answer 401 or 403 under it. Anything user-scoped needs a client built
from an OAuth2 credential, described under Authentication.
Every request draws credits at the rates on X's pricing page, and
the app must be enrolled as described in the repository's
Before you start section; without that step, reads
fail with client-not-enrolled even when the credential is valid.
Authentication
The client holds credentials directly and picks the scheme per call from the endpoint's accepted set, in OAuth2, OAuth1, bearer order. Four paths:
- Bearer (app-only):
Client::builder().bearer(token). Public reads and search. - OAuth2 user context: an
OAuth2Credentialcarrying the app's client ID and secret, the access token, and the optional refresh token and expiry. The client refreshes an expired token on the next call and hands the rotated pair to theOnTokenRefreshedhook registered withon_token_refreshed. X rotates the refresh token on every refresh, so the hook is where the new pair gets persisted; a hook that returns an error fails the call that triggered the refresh. - OAuth1 HMAC-SHA1: an
OAuth1Credentialwith the consumer pair and the user's access pair, for legacy v1.1 endpoints and some v2 write paths. - The token store:
Client::new(&Config, Auth)orClient::from_env()builds a client over the~/.xurlstore thexrCLI writes, so a program can reuse a sign-in done withxr auth oauth2with nothing exported;from_envalso readsCLIENT_ID,CLIENT_SECRET,REDIRECT_URI,AUTH_URL,TOKEN_URL,API_BASE_URL,INFO_URL, andXURL_BEARER_TOKENwhen they are set.TokenStore::refresh_hook_foris the reference implementation of the refresh hook.
use ;
use Client;
use OAuth2Credential;
async
Errors and rate limits
Every call returns xdk::Result<T>, whose error is the #[non_exhaustive] xdk::Error. Beyond Display, an error
answers four questions: kind() names its category as a stable string, exit_code() maps it to the process exit code
the xr CLI uses, next_action() names the one thing a caller can do about it as a closed NextAction (sign in,
register an app, enroll the app, and so on), and docs_url() points at the page that explains it when one exists. A
429 carries no next action; Client::last_rate_limit() returns the x-rate-limit-* window from the most recent
response that reported one, which is what a retry loop waits on.
use Client;
async
Testing
Two ways to exercise code built on this crate without spending credits.
The testing feature ships xdk::testing::MockX, an in-process mock of the API. MockX::start().await binds a
local server seeded with the fixture responses the crate's own response types are validated against, so a read
against it deserializes into the same types a live call would, and every response carries a rate-limit window.
app_client() and user_client() return clients pointed at it, stub overrides a route or rehearses a failure,
and requests() lists what arrived. Enable it in a test profile only, so a release build pulls none of its
dependencies:
[]
= { = "0.1", = ["testing"] }
The module's docs.rs page carries a complete program, and the offline_search example runs one end to end with no
X app, no credential, and no network, from a clone of the repository:
X's playground is X's own local server that simulates the API v2 with seeded users and posts, for proving the wire protocol end to end rather than for unit tests. Two limits: it needs a Go toolchain, and its parameter vocabulary predates X's post-vocabulary rename (spec 2.168), so a call that sends the current expansion names comes back as an invalid-request error.
Point a client at it with Client::builder().bearer("test_token").base_url("http://localhost:8089"); a bearer
reaches its app-only endpoints, and user-context calls stop at the auth matrix before a request leaves the machine,
the same refusal they get without credentials against the real API.
What the crate publishes
Every published module is one an embedder has a reason to call:
api: the client and its builder, oneCallper shortcut, theMediaUploadbuilder behindClient::upload_media, the typed responses, the raw-request path for endpoints without a shortcut, and the last rate-limit window a response reported.auth: credentials held in code, the refresh hook that receives a rotated token pair, the store-backedAuth, and the OAuth2 sign-in flows.config: base URL, timeouts, and the environment overrides a client built from the environment reads.error:Error,Result, the machine-readableNextAction, and the exit codes a CLI built on this crate maps them to.store:TokenStore, the on-disk credential store the CLI shares, and the reference implementation of the refresh hook.
Items marked #[doc(hidden)] are seams the xr binary reaches across the crate boundary; they stay callable but are
not part of this surface.
Cargo features
rustls(default): TLS through rustls with the platform's certificate verifier; no system TLS library is linked.native-tls: TLS through the operating system's library (OpenSSL on Linux, Secure Transport on macOS, SChannel on Windows) via native-tls. To use it alone, turn the default off:xdk-rs = { version = "0.1", default-features = false, features = ["native-tls"] }. With both backends enabled, reqwest picksnative-tls.testing: an in-process mock of the API seeded from the crate's fixtures, for tests that must not spend credits; see Testing.
A build with neither TLS feature fails at compile time with a message naming both, rather than at the first https
request.
Versioning
The crate is 0.x, so a breaking change ships in a minor. Two rules make that livable:
- Every breaking entry in the changelog carries a before/after snippet, not only a description, so the developer who adopted at one minor and upgrades two later types the new form straight from the changelog. A release that breaks something and ships no snippet does not go out.
- An MSRV bump is a minor, never a patch.
rust-versionis declared once, in the workspace's[workspace.package], and both crates inherit it, so a bump moves the floor ofxdk-rsandxurl-rstogether and is a minor for both, independent version lines notwithstanding.
The two crates version and tag independently: the library on xdk-rs-vX.Y.Z, the CLI on vX.Y.Z. Breaking
changes the CLI takes across its majors are written up under
docs/migrating.
Relationship to xr and xurl
xr (xurl-rs) is the command-line tool built on this crate and an independent
Rust port of xdevplatform/xurl, X's own Go CLI. The two crates share the
~/.xurl token store, so a sign-in done with xr auth oauth2 is usable from a program through Client::from_env().
The repository's README routes between the two.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.