A command-line tool for interacting with the X (formerly Twitter) API, supporting both OAuth 1.0a and OAuth 2.0 authentication.
- Multi-app support — register multiple X API apps with separate credentials and tokens
- OAuth 2.0 PKCE flow authentication
- OAuth 1.0a authentication
- Multiple OAuth 2.0 account support per app
- Default app and default user selection (interactive Bubble Tea picker or single command)
- Persistent token storage in YAML (
~/.xurl), auto-migrates from legacy JSON - HTTP request customization (headers, methods, body)
- Per-request app override with
--app
brew install --cask xdevplatform/tap/xurlnpm install -g @xdevplatform/xurlcurl -fsSL https://raw.githubusercontent.com/xdevplatform/xurl/main/install.sh | bashInstalls to ~/.local/bin. If it's not in your PATH, the script will tell you what to add.
go install github.com/xdevplatform/xurl@latestYou must have a developer account and app to use this tool.
Register your X API app credentials so they're stored in ~/.xurl (no env vars needed after this):
xurl auth apps add my-app --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRETIf you want the app to keep its own callback configuration in ~/.xurl, you can store the redirect URI there too:
xurl auth apps add my-app --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET --redirect-uri http://localhost:8080/callbackYou can register multiple apps:
xurl auth apps add prod-app --client-id PROD_ID --client-secret PROD_SECRET
xurl auth apps add dev-app --client-id DEV_ID --client-secret DEV_SECRETLegacy / env-var flow: You can also set
CLIENT_IDandCLIENT_SECRETas environment variables. They'll be auto-saved into the active app on first use.
REDIRECT_URInow resolves in this order:REDIRECT_URIenvironment variable, then the app's storedredirect_uriin~/.xurl, then the built-in defaulthttp://localhost:8080/callback.
Note: For OAuth 2.0 authentication, you must specify the redirect URI in the X API developer portal.
- Create an app at the X API developer portal.
- Go to authentication settings and set the redirect URI to the same value that
xurlwill use throughREDIRECT_URI. The default ishttp://localhost:8080/callback, andxurlderives the callback host, port, and path from the effective redirect URI. The effective value is resolved fromREDIRECT_URI, then the app's storedredirect_uri, then the built-in default. When you uselocalhost,xurllistens on both127.0.0.1and::1so browser loopback resolution does not break the callback.

- Register the app (if you haven't already):
xurl auth apps add my-app --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET- Get your access keys for the registered app:
xurl auth oauth2 --app my-appIf you omit --app, the token is saved to the current default app. You can also run xurl auth default my-app first and then use xurl auth oauth2.
Headless / remote machines. The default flow opens a browser and waits for a callback on localhost, which isn't reachable from a remote server. On those hosts use --headless:
xurl auth oauth2 --app my-app --headlessxurl prints the authorization URL; open it on any device with a browser, approve, then paste the resulting redirect URL (or just the code value from the address bar) back into the prompt. No callback listener is needed — the page failing to load is expected; the code is in the URL.
If X returns a client-forbidden / client-not-enrolled error even though auth completed successfully, check the app’s package and environment in the X developer console. On current X platform setup, the working fix was:
- Go to
Apps->Manage apps - Open the app
- Use
Move to package - Choose
Pay-per-use - Move the app to the
Productionenvironment
Without that enrollment step, xurl whoami and other /2/* reads can fail even when the OAuth callback and tokens are valid.
If X does not return your username reliably through /2/users/me, authenticate with an explicit handle instead:
xurl auth oauth2 --app my-app YOUR_USERNAMEThat keeps the OAuth2 token associated with the expected username and also gives shortcut commands a fallback when /2/users/me is unavailable.
xurl auth app-only BEARER_TOKEN
cat token.txt | xurl auth app-only - # read from stdin (keeps it out of shell history)This stores X's app-only Bearer Token (from the developer portal), used at request time with --auth app. It's named for the auth mode (app-only) rather than the token scheme (bearer), since OAuth2 user tokens are also sent as Authorization: Bearer.
Back-compat:
xurl auth appandxurl auth bearerstill work as aliases, and--bearer-token TOKENis still accepted.
xurl auth oauth1 --consumer-key KEY --consumer-secret SECRET --access-token TOKEN --token-secret SECRETList registered apps:
xurl auth apps listUpdate credentials on an existing app:
xurl auth apps update my-app --client-id NEW_ID --client-secret NEW_SECRET
xurl auth apps update my-app --redirect-uri http://localhost:8080/callbackREDIRECT_URI from the environment still overrides the stored app value at runtime, so auth apps update --redirect-uri is best for your default per-app callback while env vars remain the temporary override path.
View the effective and stored redirect URI for an app:
xurl auth apps redirect-uri get my-appSet the stored redirect URI for an app:
xurl auth apps redirect-uri set my-app http://localhost:8080/callbackRemove an app:
xurl auth apps remove old-appSet the default app and user — interactive picker (uses Bubble Tea):
xurl auth defaultSet the default app and user — single command:
xurl auth default my-app # set default app
xurl auth default my-app alice # set default app + default userUse a specific app for a single request:
xurl --app dev-app /2/users/meView authentication status across all apps:
xurl auth statusThis output shows the effective redirect URI for each app and, when REDIRECT_URI is set in the environment, also shows the stored app value separately so precedence is visible.
Example output:
▸ my-app [client_id: VUttdG9P…]
redirect_uri: http://localhost:8080/callback [app config]
▸ oauth2: alice
oauth2: bob
oauth1: ✓
bearer: ✓
dev-app [client_id: OTHER789…]
redirect_uri: http://localhost:8080/callback [built-in default]
oauth2: (none)
oauth1: –
bearer: –
If OAuth succeeds but reads like xurl whoami fail with an error body containing client-forbidden or client-not-enrolled, the current X platform fix is to move the app into the Pay-per-use package and use the Production environment in the developer console. This is an X platform enrollment issue, not a local callback-listener issue in xurl.
▸ on the left = default app. ▸ next to a user = default user.
xurl auth clear --all # Clear all tokens
xurl auth clear --oauth1 # Clear OAuth 1.0a tokens
xurl auth clear --oauth2-username USERNAME # Clear specific OAuth 2.0 token
xurl auth clear --bearer # Clear bearer tokenBasic GET request:
xurl /2/users/meCustom HTTP method:
xurl -X POST /2/tweets -d '{"text":"Hello world!"}'Add headers:
xurl -H "Content-Type: application/json" /2/tweetsSpecify authentication type:
xurl --auth oauth2 /2/users/me
xurl --auth oauth1 /2/tweets
xurl --auth app /2/users/meUse specific OAuth 2.0 account:
xurl --username johndoe /2/users/meStreaming endpoints (like /2/tweets/search/stream) are automatically detected and handled appropriately. The tool will automatically stream the response for these endpoints:
/2/tweets/search/stream/2/tweets/sample/stream/2/tweets/sample10/stream/2/tweets/firehose/stream/lang/en/2/tweets/firehose/stream/lang/ja/2/tweets/firehose/stream/lang/ko/2/tweets/firehose/stream/lang/pt
For example:
xurl /2/tweets/search/streamYou can also force streaming mode for any endpoint using the --stream or -s flag:
xurl -s /2/users/mexurl token prints a valid OAuth2 access token for the active app to stdout (a single line, no decoration). If the stored token has expired it is refreshed and persisted first. This command never opens a browser, so it is safe to use in scripts:
xurl token # token for the default app/user
xurl token --app my-app # token for a specific app
xurl token -u alice # token for a specific OAuth2 user
TOKEN=$(xurl token) && curl -H "Authorization: Bearer $TOKEN" https://api.x.com/2/users/meIf no token is available (and none can be refreshed), it exits non-zero with a hint to run xurl auth oauth2.
xurl mcp turns xurl into a Model Context Protocol bridge for the hosted X API MCP server. It reads newline-delimited JSON-RPC from stdin, relays each message to a remote Streamable HTTP MCP endpoint with an Authorization: Bearer <token> header, and writes the server's responses (plain JSON or text/event-stream) back to stdout as newline-delimited JSON. The MCP session id is maintained automatically and the token is refreshed in-process as it expires.
Because X's OAuth requires your own app (there is no dynamic client registration), xurl holds the app identity and mints/refreshes the token. On first run with no cached token, the bridge opens the browser for a one-time OAuth2 login using the CLIENT_ID/CLIENT_SECRET from its environment, then caches and auto-refreshes the token for subsequent runs. The MCP handshake is held until that login completes, so give the server a generous startup_timeout_sec.
Use it directly from any MCP client (Claude Desktop, Cursor, etc.) with a standard MCP server config — no separate install step is needed thanks to the npm launcher:
{
"mcpServers": {
"xapi": {
"command": "npx",
"args": ["-y", "@xdevplatform/xurl", "mcp", "https://api.x.com/mcp"],
"env": { "CLIENT_ID": "...", "CLIENT_SECRET": "..." },
"startup_timeout_sec": 300
}
}
}Requirements for the first-run browser login: a browser on the machine running the client, and your X app must have the OAuth2 redirect URI http://localhost:8080/callback registered (or set REDIRECT_URI to one that is). On a headless host with no reachable browser, authenticate out-of-band first with xurl auth oauth2 --headless (the bridge then just reuses the cached token).
The <url> positional is optional and defaults to https://api.x.com/mcp. --app is honored, so you can point a client at a specific registered app:
xurl --app my-app mcp # bridge the default endpoint using my-app
xurl mcp https://api.x.com/mcp # explicit endpointAll diagnostics are written to stderr so stdout stays a clean JSON-RPC channel.
xurl can help you quickly set up a temporary webhook URL to receive events from the X API. This is useful for development and testing.
-
Start the local webhook server with ngrok:
Run the
webhook startcommand. This will start a local server and use ngrok to create a public URL that forwards to your local server. You will be prompted for your ngrok authtoken if it's not already configured via theNGROK_AUTHTOKENenvironment variable.xurl webhook start # Or with a specific port and output file for POST bodies xurl webhook start -p 8081 -o webhook_events.logThe command will output an ngrok URL (e.g.,
https://your-unique-id.ngrok-free.app/webhook). Note this URL. -
Register the webhook with the X API:
Use the ngrok URL obtained in the previous step to register your webhook. You'll typically use app authentication for this.
# Replace https://your-ngrok-url.ngrok-free.app/webhook with the actual URL from the previous step xurl --auth app /2/webhooks -d '{"url": "<your ngrok url>"}' -X POST
Your local
xurl webhook startserver will then handle the CRC handshake from Twitter and log incoming POST events (and write them to a file if-owas used).
The tool supports uploading media files to the X API using the chunked upload process.
Upload a media file (the media type and category are auto-detected from the file extension):
xurl media upload path/to/file.mp4
xurl media upload path/to/photo.jpgOverride the auto-detected media type and category when needed:
xurl media upload --media-type image/jpeg --category tweet_image path/to/image.jpgCheck media upload status:
xurl media status MEDIA_IDWait for media processing to complete:
xurl media status --wait MEDIA_IDMost users should just use xurl media upload above. If you need to drive the
chunked upload manually, use the -F flag with the path-style endpoints that
xurl media upload itself uses:
- First, initialize the upload:
xurl -X POST /2/media/upload/initialize -d '{"total_bytes": FILE_SIZE, "media_type": "video/mp4", "media_category": "tweet_video"}'- Then, append the media chunks (repeat with an increasing
segment_index):
xurl -X POST -F path/to/file.mp4 /2/media/upload/MEDIA_ID/append- Finally, finalize the upload:
xurl -X POST /2/media/upload/MEDIA_ID/finalize- Check the status:
xurl '/2/media/upload?command=STATUS&media_id=MEDIA_ID'Tokens and app credentials are stored in ~/.xurl in YAML format. Each registered app has its own isolated set of tokens. Example:
apps:
my-app:
client_id: abc123
client_secret: secret456
redirect_uri: http://localhost:8080/callback
default_user: alice
oauth2_tokens:
alice:
type: oauth2
oauth2:
access_token: "..."
refresh_token: "..."
expiration_time: 1234567890
bearer_token:
type: bearer
bearer: "AAAA..."
default_app: my-appMigration: If you have an existing JSON-format
~/.xurlfile from a previous version, it will be automatically migrated to the new YAML multi-app format on first use. Your tokens are preserved in adefaultapp.
Contributions are welcome!
This project is open-sourced under the MIT License - see the LICENSE file for details.