Skip to main content

x2raindrop-cli

A Python CLI tool to sync your X (Twitter) bookmarks and liked posts to Raindrop.io collections.

PyPI version Docker Image Python 3.12+ License

Features

  • Sync X bookmarks and/or liked posts to Raindrop.io collections
  • Per-source configuration: separate collections, tags, link modes, and remove-from-X settings
  • 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 Raindrops
  • Optional: unbookmark and/or unlike on X after syncing
  • Idempotent syncing with local state tracking (bookmarks and likes tracked separately)
  • Dry-run mode for safe testing
  • Interactive OAuth 2.0 PKCE authentication flow for X

Requirements

  • 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

Installation

From PyPI (Recommended)

pip install x2raindrop-cli

From Source

git clone https://github.com/dotWee/x2raindrop-cli.git
cd x2raindrop-cli

# Install dependencies
uv sync

Using Docker

Pull the image from GitHub Container Registry:

docker pull ghcr.io/dotwee/x2raindrop-cli:latest

Run 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 12345

# Sync bookmarks and likes
docker run --rm -v "$PWD":/data ghcr.io/dotwee/x2raindrop-cli \
  sync --collection 12345 --likes --likes-collection 54321

See the Docker Usage section for more details.

2. Set Up X API Credentials

You have two options for X authentication:

Option A: Direct Access Token (Simplest)

If you already have an access token (e.g., from another OAuth flow or the X Developer Portal):

  1. Set X_ACCESS_TOKEN in your config or environment
  2. 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"

Option B: OAuth 2.0 PKCE Flow (Interactive)

For browser-based login:

  1. Go to the X Developer Portal
  2. Create a new project and app (or use an existing one)
  3. 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
  4. Note your Client ID (and Client Secret if using Confidential Client)
  5. Run x2raindrop x login to authenticate

Required OAuth 2.0 Scopes:

  • bookmark.read - Read your bookmarks
  • bookmark.write - Remove bookmarks (optional, only if using --remove-from-x for bookmarks)
  • like.read - Read your liked posts (required for likes sync)
  • like.write - Unlike posts (optional, only if using --remove-from-x for likes)
  • tweet.read - Read tweet data
  • users.read - Read user profile data
  • offline.access - Refresh tokens for persistent access

If you previously authenticated without like.read / like.write, run x2raindrop x logout then x2raindrop x login again so the new scopes are granted.

3. Set Up Raindrop.io API Token

  1. Go to Raindrop.io Integrations
  2. Under "For Developers", create a new app or use "Test token"
  3. Copy the Test token for personal use

4. Configure the Application

Create a configuration file:

# Create default config file in current directory
uv run x2raindrop config init

# Edit the config file
nano config.toml

Or 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 (nested per source; note single underscore after SYNC_)
export SYNC_BOOKMARKS__COLLECTION_ID="12345"
export SYNC_BOOKMARKS__TAGS='["x-bookmark", "auto-synced"]'
export SYNC_BOOKMARKS__REMOVE_FROM_X="false"
export SYNC_BOOKMARKS__SKIP_EXISTING_LINKS="true"
export SYNC_BOOKMARKS__LINK_MODE="permalink"

# Optional: enable liked-post sync
export SYNC_LIKES__ENABLED="true"
export SYNC_LIKES__COLLECTION_ID="54321"
export SYNC_LIKES__TAGS='["x-like", "auto-synced"]'

Usage

Authenticate with X

First, authenticate with X using the interactive OAuth 2.0 PKCE flow:

uv run x2raindrop x login

This will open your browser for authorization. After approving, the tokens are saved locally.

List Raindrop.io Collections

Find the collection ID you want to sync to:

uv run x2raindrop raindrop collections

Sync Bookmarks and Likes

Basic bookmark sync:

uv run x2raindrop sync --collection 12345

Sync liked posts only:

uv run x2raindrop sync --no-bookmarks --likes --likes-collection 54321

Sync both bookmarks and likes (each to its own Raindrop collection):

uv run x2raindrop sync --collection 12345 --likes --likes-collection 54321

Passing --likes-collection also enables likes sync unless --no-likes is set.

With options:

# Sync bookmarks with custom tags (--tags applies to bookmarks only;
# configure likes tags under [sync.likes] in config.toml)
uv run x2raindrop sync --collection 12345 --tags "x,bookmarks,auto"

# Use first external URL from tweets (applies to all enabled sources)
uv run x2raindrop sync --collection 12345 --link-mode first_external_url

# Remove synced items from X after syncing (unbookmark and/or unlike)
uv run x2raindrop sync --collection 12345 --likes --likes-collection 54321 --remove-from-x

# Override config that has remove_from_x / dry_run enabled
uv run x2raindrop sync --no-remove-from-x --no-dry-run

# Dry run - see what would happen without making changes
uv run x2raindrop sync --collection 12345 --likes --likes-collection 54321 --dry-run

Enable likes permanently in config.toml with [sync.likes] enabled = true and a collection_id, then plain x2raindrop sync will sync both sources.

Check X Authentication Status

uv run x2raindrop x status

Logout from X

uv run x2raindrop x logout

Configuration Reference

Config File Location

Default: config.toml in the current working directory (project root).

Override with --config flag on any command.

Config File Format

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",
    "like.read",
    "like.write",
    "tweet.read",
    "users.read",
    "offline.access",
]

[raindrop]
token = "YOUR_RAINDROP_TOKEN"

[sync]
dry_run = false

[sync.bookmarks]
enabled = true
collection_id = 12345
collection_title = ""  # Optional: look up by title when collection_id is unset
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

[sync.likes]
enabled = false
collection_id = 54321
collection_title = ""  # Optional: look up by title when collection_id is unset
tags = ["x-like", "auto-synced"]
remove_from_x = false  # Unlike posts on X after syncing
skip_existing_links = true
link_mode = "permalink"
both_behavior = "one_external_plus_note"

Copy config.example.toml as a starting point, or run x2raindrop config init.

Legacy flat [sync] settings (without bookmarks / likes sections) are still supported and are treated as bookmark settings for backward compatibility.

Each source can be configured independently: different Raindrop collections, tags, link modes, and remove-from-X behavior.

CLI flags override config when passed. Boolean flags are tri-state, so --no-remove-from-x and --no-dry-run clear values that are enabled in config. log_level controls stdlib/structlog verbosity for CLI commands.

Link Modes

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)

Both Behavior Options

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)

Data Storage

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 (bookmarks and likes tracked separately)

Safety Notes

  1. Dry Run First: Always use --dry-run before syncing to preview changes
  2. Remove from X: The remove_from_x setting permanently removes bookmarks or unlikes posts on X. Use with caution and consider backing up first
  3. Token Security: The x_token.json file contains sensitive tokens. Ensure proper file permissions

X API Rate Limits

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)
  • Fetching liked posts: 1 request per 100 likes (paginated)
  • Deleting a bookmark / unliking a post: 1 request per item

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:

  1. Don't use --remove-from-x - each unbookmark/unlike is a separate request
  2. Wait for the current rate-limit window to reset, then rerun the command
  3. The tool tracks synced bookmarks and likes locally, so interrupted syncs can resume
  4. Sync one source at a time on Free Tier if rate limits are tight
  5. Consider upgrading to Basic tier if you have many bookmarks or likes

Docker Usage

The Docker image provides a convenient way to run x2raindrop-cli without installing Python dependencies locally.

Pulling the Image

# Latest version
docker pull ghcr.io/dotwee/x2raindrop-cli:latest

Running Commands

The 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-run
x2raindrop sync --collection 12345 --likes --likes-collection 54321 --dry-run

Using Environment Variables

Pass 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_BOOKMARKS__COLLECTION_ID="12345" \
  -e SYNC_LIKES__ENABLED="true" \
  -e SYNC_LIKES__COLLECTION_ID="54321" \
  -v "$PWD":/data \
  ghcr.io/dotwee/x2raindrop-cli sync

OAuth Authentication in Docker

The 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

  1. Install the CLI locally and run x2raindrop x login on your host machine
  2. This creates .x2raindrop/x_token.json in your current directory
  3. Mount that directory when running Docker:
docker run --rm -v "$PWD":/data ghcr.io/dotwee/x2raindrop-cli sync --collection 12345

The container will use the token file from your mounted directory.

Data Persistence

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 bookmarks and likes

Always mount a volume to /data to persist this data between runs.

Development

Setup Development Environment

uv sync --group dev

Run Tests

uv run pytest

Run Tests with Coverage

uv run pytest --cov=x2raindrop_cli --cov-report=html

Linting

uv run ruff check src tests
uv run ruff format src tests

Type Checking

uv run ty check src

Troubleshooting

"Not authenticated with X"

Run x2raindrop x login to authenticate.

"Token expired"

The tool automatically refreshes tokens. If issues persist, run x2raindrop x logout then x2raindrop x login.

"Collection ID not found"

Run x2raindrop raindrop collections to list available collections and their IDs.

Rate Limit Errors

Wait for the current rate-limit window to reset (often 15 minutes on Free Tier), then rerun. Fetching bookmarks/likes and each unbookmark/unlike consume separate request quota.

Likes sync fails with authorization / scope errors

Ensure your X app token includes like.read (and like.write if removing likes). Re-authenticate with x2raindrop x logout then x2raindrop x login after updating scopes in config.toml.

License

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.

Credits

Release files for x2raindrop-cli 1.0.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for x2raindrop-cli 1.0.5
File Size Uploaded
x2raindrop_cli-1.0.5.tar.gz 96.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for x2raindrop-cli 1.0.5
File Interpreter ABI Platform
x2raindrop_cli-1.0.5-py3-none-any.whl Python 3 none any Details

Total release size: 137.0 kB

Release files / x2raindrop_cli-1.0.5.tar.gz

Download URL x2raindrop_cli-1.0.5.tar.gz
Size 96.9 kB
Tags Source
SHA-256 checksum
How to use checksums
1bb4c0b186834fb182eb552a25af8d6e5bc63d2f426780f4c12075c829f62b10
BLAKE2b-256 checksum
How to use checksums
b870af19ad5d299023b0ec012647fd107d8865f7e0878a7e3fc895f3a55dcccc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 12, 2026.

Transparency log

Release files / x2raindrop_cli-1.0.5-py3-none-any.whl

Download URL x2raindrop_cli-1.0.5-py3-none-any.whl
Size 40.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a07266d24c6ddf8d36b01dbcf49fbd3bd2238e40375ed66eb2efe8377cc46d59
BLAKE2b-256 checksum
How to use checksums
be76dfede1a1b7ed0aa02f75ac2876e5cb88ade0692c652b4ea9b472887af01f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 12, 2026.

Transparency log

Release history Release notifications | RSS feed

1.1.0

2 release files

This release

1.0.5 This release

2 release files

1.0.4

2 release files

1.0.3

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page