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:

  1. Create a new app
  2. Under User authentication settingsOAuth 2.0 Settings:
    • App type: “Web app, automated app or bot” — not “Native App”. Native App causes unauthorized_client during 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: add tweet.write, media.write.
  3. 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:

  1. Copy the URL xurl prints
  2. Open it in any browser: your laptop, your phone, anything
  3. Approve the X authorization
  4. 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.
  5. Copy the full URL from the address bar (or just the code parameter value)
  6. 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