Giving Your AI Agent X API Access from a Headless Server
How to authenticate xurl on a headless server over SSH so your AI agent can read bookmarks, post tweets, and search X — including every OAuth gotcha that wasted my afternoon.
I wanted my AI agent to read my saved tweets (bookmarks) and eventually draft posts on my behalf. The X API requires OAuth 2.0 user authentication for bookmark access, which means a browser-based login flow. My agent runs on a headless server over SSH — no browser, no GUI. This is the exact path that finally worked, including every dead end along the way.
The Problem
xurl is the official CLI for the X API. It’s the cleanest way to give an agent programmatic access to X — it returns JSON for every command, handles token refresh automatically, and covers the full v2 API surface.
But the default OAuth flow starts a local HTTP server and opens a browser. On a headless server over SSH, it just hangs silently. No error, no output, no URL to open.
Step 1: Install xurl and Check Your Version
curl -fsSL https://raw.githubusercontent.com/xdevplatform/xurl/main/install.sh | bash
xurl version
You need v1.2.0 or later. The --headless flag — which is the entire solution to this problem — was added in v1.2.0, released June 2026. If you’re on v1.1.0, --headless doesn’t exist and the install script will silently give you the old version if the GitHub release hasn’t propagated. Re-run the install script to get the latest.
Step 2: Create the X API App
In the X Developer Portal:
- Create a new app
- Under User authentication settings → OAuth 2.0 Settings:
- App type: “Web app, automated app or bot” — not “Native App”. Native App causes
unauthorized_clientduring OAuth. - Callback URI:
http://localhost:8080/callback(exactly this — more on why below) - Scopes: Enable what your agent needs. For bookmarks:
tweet.read,bookmark.read,users.read. For posting: addtweet.write,media.write.
- App type: “Web app, automated app or bot” — not “Native App”. Native App causes
- Copy the OAuth 2.0 Client ID and Client Secret
Step 3: Register the App Locally
xurl auth apps add my-app --client-id YOUR_ID --client-secret YOUR_SECRET
xurl auth default my-app
Gotcha: ~/.xurl exists as a directory
If you see Error: IO Error: open ~/.xurl: is a directory, a previous failed run left an empty directory. Fix:
rmdir ~/.xurl
xurl expects a file, not a directory. It’ll create the config file on the next run.
Step 4: Authenticate with —headless
xurl auth oauth2 --app my-app --headless
This prints an authorization URL and waits for you to paste back a code. The flow:
- Copy the URL xurl prints
- Open it in any browser: your laptop, your phone, anything
- Approve the X authorization
- After approving, X redirects to
http://localhost:8080/callback?state=…&code=…. The page won’t load — there’s no server listening, and that’s fine. The authorization code is in the address bar. - Copy the full URL from the address bar (or just the
codeparameter value) - Paste it back into the xurl prompt
Gotcha: Paste the redirect URL, not the auth URL
The most natural thing to do is copy the URL xurl gave you and paste it right back. That’s the authorization URL — it doesn’t contain a code. You need the URL after X redirects you, the one in the address bar of the page that failed to load.
If you paste the wrong URL, you get:
oauth2: "invalid_request" "The method was either called with invalid arguments..."
Gotcha: “Something went wrong” on the X auth page
This means the redirect URI in your X Developer Portal doesn’t match what xurl is sending. Both must be exactly http://localhost:8080/callback — same casing, same trailing slash (or lack thereof), same port. Check the portal and fix it, then re-run the command.
Step 5: Handle Port 8080 Conflicts
The default redirect URI uses port 8080. If another service is already on that port (Tailscale’s serve proxy, Jenkins, etc.), the standard OAuth flow fails:
Auth Error: ServerError (cause: listen tcp 127.0.0.1:8080: bind: address already in use)
With --headless, this doesn’t matter. No listener is started — the redirect URI is just a string that needs to match the X Developer Portal. Keep it at 8080 and use --headless.
If you’re stuck on v1.1.0 (no --headless) and need to use a different port:
xurl auth apps update my-app --redirect-uri http://localhost:9090/callback
Then update the Callback URI in the X Developer Portal to match. Both must match exactly.
Step 6: Verify
xurl whoami
Returns your profile as JSON — user ID, follower count, subscription type. If this works, your agent has authenticated access to the X API.
Step 7: Give Your Agent Access
Your agent can now call xurl directly. Some examples of what this enables:
# Read bookmarks (the original goal)
xurl bookmarks -n 50
# Search for tweets to engage with
xurl search "buildinpublic" -n 10
# Check mentions
xurl mentions -n 20
All commands return JSON to stdout — pipe-friendly and scriptable. Tokens auto-refresh, so no ongoing maintenance.
Key Takeaways
--headlessis the entire solution: but it requires v1.2.0+. Check your version first.- The redirect URI must match exactly between xurl and the X Developer Portal. Mismatch = “Something went wrong” with no useful error message.
- App type must be “Web app”, not “Native App” — otherwise OAuth fails with
unauthorized_client. - The code is in the address bar of the page that failed to load. Copy the redirect URL, not the authorization URL.
- Port conflicts don’t matter with
--headless: no listener is started. Without--headless, port 8080 conflicts are the most common failure.