A Python CLI tool to sync your X (Twitter) bookmarks to Raindrop.io collections.
- Sync X bookmarks to a specified Raindrop.io collection
- Configurable link handling:
- Use X post permalink
- Use first external URL from the post (with fallback to permalink)
- Both: create entries for external URLs with X permalink stored in notes
- Apply custom tags to synced bookmarks
- Optional: Remove bookmarks from X after syncing
- Idempotent syncing with local state tracking
- Dry-run mode for safe testing
- Interactive OAuth 2.0 PKCE authentication flow for X
- Python 3.12 or higher
- uv for dependency and environment management
- X Developer account with OAuth 2.0 app
- Raindrop.io account with API token
pip install x2raindrop-cligit clone https://github.com/dotWee/x2raindrop-cli.git
cd x2raindrop-cli
# Install dependencies
uv syncPull the image from GitHub Container Registry:
docker pull ghcr.io/dotwee/x2raindrop-cli:latestRun commands by mounting your local directory (for config and state persistence):
# Show help
docker run --rm ghcr.io/dotwee/x2raindrop-cli --help
# Initialize config in current directory
docker run --rm -v "$PWD":/data ghcr.io/dotwee/x2raindrop-cli config init
# Sync bookmarks
docker run --rm -v "$PWD":/data ghcr.io/dotwee/x2raindrop-cli sync --collection 12345See the Docker Usage section for more details.
You have two options for X authentication:
If you already have an access token (e.g., from another OAuth flow or the X Developer Portal):
- Set
X_ACCESS_TOKENin your config or environment - No browser login required - just run sync directly
[x]
access_token = "your_access_token_here"
# Optional: provide refresh_token to enable automatic token refresh
refresh_token = "your_refresh_token_here"For browser-based login:
- Go to the X Developer Portal
- Create a new project and app (or use an existing one)
- Under "User authentication settings", configure:
- App permissions: Read and write
- Type of App: Native App (for PKCE without client secret) or Confidential Client
- Callback URL:
http://127.0.0.1:8765/callback
- Note your Client ID (and Client Secret if using Confidential Client)
- Run
x2raindrop x loginto authenticate
Required OAuth 2.0 Scopes:
bookmark.read- Read your bookmarksbookmark.write- Remove bookmarks (optional, only if using--remove-from-x)tweet.read- Read tweet datausers.read- Read user profile dataoffline.access- Refresh tokens for persistent access
- Go to Raindrop.io Integrations
- Under "For Developers", create a new app or use "Test token"
- Copy the Test token for personal use
Create a configuration file:
# Create default config file in current directory
uv run x2raindrop config init
# Edit the config file
nano config.tomlOr use environment variables:
# X API credentials (choose one method)
# Option A: Direct access token
export X_ACCESS_TOKEN="your_access_token"
# Option B: OAuth PKCE flow (then run `x2raindrop x login`)
export X_CLIENT_ID="your_client_id"
export X_CLIENT_SECRET="your_client_secret" # Optional for public clients
# Raindrop.io credentials
export RAINDROP_TOKEN="your_raindrop_token"
# Sync settings
export SYNC_COLLECTION_ID="12345" # Target collection ID
export SYNC_TAGS='["x-bookmark", "auto-synced"]' # JSON array format
export SYNC_REMOVE_FROM_X="false"
export SYNC_SKIP_EXISTING_LINKS="true" # Skip links already saved in Raindrop
export SYNC_LINK_MODE="permalink" # permalink, first_external_url, or bothFirst, authenticate with X using the interactive OAuth 2.0 PKCE flow:
uv run x2raindrop x loginThis will open your browser for authorization. After approving, the tokens are saved locally.
Find the collection ID you want to sync to:
uv run x2raindrop raindrop collectionsBasic sync:
uv run x2raindrop sync --collection 12345With options:
# Sync with custom tags
uv run x2raindrop sync --collection 12345 --tags "x,bookmarks,auto"
# Use first external URL from tweets
uv run x2raindrop sync --collection 12345 --link-mode first_external_url
# Remove from X after syncing (use with caution!)
uv run x2raindrop sync --collection 12345 --remove-from-x
# Dry run - see what would happen without making changes
uv run x2raindrop sync --collection 12345 --dry-runuv run x2raindrop x statusuv run x2raindrop x logoutDefault: config.toml in the current working directory (project root).
Override with --config flag on any command.
log_level = "INFO"
[x]
# Option A: Direct access token (simplest - no browser login needed)
access_token = ""
# Option B: OAuth PKCE flow (use `x2raindrop x login`)
client_id = ""
client_secret = "" # Leave empty for public clients
redirect_uri = "http://127.0.0.1:8765/callback"
scopes = [
"bookmark.read",
"bookmark.write",
"tweet.read",
"users.read",
"offline.access",
]
[raindrop]
token = "YOUR_RAINDROP_TOKEN"
[sync]
collection_id = 12345
collection_title = "" # Optional: look up collection by title
tags = ["x-bookmark", "auto-synced"]
remove_from_x = false
skip_existing_links = true
link_mode = "permalink" # permalink, first_external_url, or both
both_behavior = "one_external_plus_note" # one_external_plus_note or two_raindrops
dry_run = false| Mode | Description |
|---|---|
permalink |
Create a Raindrop with the X post URL |
first_external_url |
Use the first external URL in the tweet (falls back to permalink if none) |
both |
Create entries for both external URL and permalink (behavior configurable) |
When link_mode = "both" and the tweet contains an external URL:
| Option | Description |
|---|---|
one_external_plus_note |
Create one Raindrop for the external URL, store X permalink in the note |
two_raindrops |
Create two separate Raindrops (one for external URL, one for X permalink) |
The tool stores data in the current working directory:
config.toml- Configuration file.x2raindrop/x_token.json- X OAuth tokens (keep secure!).x2raindrop/state.json- Sync state for idempotency
- Dry Run First: Always use
--dry-runbefore syncing to preview changes - Remove from X: The
--remove-from-xflag permanently removes bookmarks from X. Use with caution and consider backing up first - Token Security: The
x_token.jsonfile contains sensitive tokens. Ensure proper file permissions
IMPORTANT: X API has strict rate limits, especially on the Free Tier.
| Tier | Rate Limit | Notes |
|---|---|---|
| Free | 1 request / 15 min | Very limited - sync may take a long time |
| Basic | Higher limits | Check X Developer Portal for current limits |
API Request Breakdown:
- Fetching bookmarks: 1 request per 100 bookmarks (paginated)
- Deleting a bookmark: 1 request per bookmark
Rate Limit Behavior: The CLI now uses the official Python XDK for X API calls. If X returns a 429 rate-limit response, the command exits with the API error from the SDK.
Recommendations for Free Tier:
- Don't use
--remove-from-x- each deletion is a separate request - Wait for the current rate-limit window to reset, then rerun the command
- The tool tracks synced bookmarks locally, so interrupted syncs can resume
- Consider upgrading to Basic tier if you have many bookmarks
The Docker image provides a convenient way to run x2raindrop-cli without installing Python dependencies locally.
# Latest version
docker pull ghcr.io/dotwee/x2raindrop-cli:latest
# Specific version
docker pull ghcr.io/dotwee/x2raindrop-cli:1.0.3The container's working directory is /data. Mount your local directory there to persist configuration and state:
# Create an alias for convenience
alias x2raindrop='docker run --rm -v "$PWD":/data ghcr.io/dotwee/x2raindrop-cli'
# Now use it like the native CLI
x2raindrop --version
x2raindrop config init
x2raindrop raindrop collections
x2raindrop sync --collection 12345 --dry-runPass credentials via environment variables instead of a config file:
docker run --rm \
-e X_ACCESS_TOKEN="your_token" \
-e RAINDROP_TOKEN="your_raindrop_token" \
-e SYNC_COLLECTION_ID="12345" \
-v "$PWD":/data \
ghcr.io/dotwee/x2raindrop-cli syncThe interactive OAuth 2.0 PKCE flow (x2raindrop x login) requires a browser, which doesn't work well inside a container. You have two options:
Option 1: Use a Direct Access Token (Recommended for Docker)
Set X_ACCESS_TOKEN in your config or as an environment variable. No browser login required.
Option 2: Authenticate on Host, Then Use in Docker
- Install the CLI locally and run
x2raindrop x loginon your host machine - This creates
.x2raindrop/x_token.jsonin your current directory - Mount that directory when running Docker:
docker run --rm -v "$PWD":/data ghcr.io/dotwee/x2raindrop-cli sync --collection 12345The container will use the token file from your mounted directory.
The container stores data in /data (the working directory):
| File | Purpose |
|---|---|
config.toml |
Configuration file |
.x2raindrop/x_token.json |
X OAuth tokens |
.x2raindrop/state.json |
Sync state for idempotency |
Always mount a volume to /data to persist this data between runs.
uv sync --group devuv run pytestuv run pytest --cov=x2raindrop_cli --cov-report=htmluv run ruff check src tests
uv run ruff format src testsuv run ty check srcRun x2raindrop x login to authenticate.
The tool automatically refreshes tokens. If issues persist, run x2raindrop x logout then x2raindrop x login.
Run x2raindrop raindrop collections to list available collections and their IDs.
Wait 15 minutes and try again. X API allows 180 bookmark requests per 15-minute window.
Copyright (c) 2026 Lukas 'dotWee' Wolfsteiner lukas@wolfsteiner.media
Licensed under the Do What The Fuck You Want To Public License. See the LICENSE file for details.
- python-raindropio - Raindrop.io API wrapper
- X Python XDK - X API documentation