🚀 Executive Summary

TL;DR: Manually migrating thousands of Pocket bookmarks to a self-hosted Wallabag instance is inefficient. This guide provides a Python script to automate the migration process, fetching articles from Pocket’s API and posting them to Wallabag’s API, thereby enabling users to regain full control of their digital reading data.

🎯 Key Takeaways

  • Securely manage API credentials for both Pocket and Wallabag by storing them in a `config.env` file and loading them with `python-dotenv` to avoid hard-coding.
  • Implement an idempotency check using `wallabag_entry_exists` before posting to Wallabag to prevent duplicate entries if the migration script is run multiple times.
  • Ensure comprehensive data transfer from Pocket by using `detailType: ‘complete’` to fetch tags and correctly map Pocket’s `status` (e.g., ‘1’ for archived) to Wallabag’s `archive` status.

Migrate Pocket Bookmarks to Wallabag (Self-hosted)

Migrate Pocket Bookmarks to Wallabag (Self-hosted)

Hey there, Darian Vance here. For years, my digital reading list was a mess, scattered across a half-dozen services. My “read it later” workflow was broken. The big win for me was consolidating everything into my own self-hosted Wallabag instance. It wasn’t just about tidying up; it was about taking back full control of my data. The problem was the initial migration of over 2,000 articles from Pocket. Doing that manually was a non-starter. This script saved me a weekend of tedious work. Let me show you how to build it so you can get your data migrated and get back to your day.

Prerequisites

Before we dive in, make sure you have the following ready:

  • A running, accessible self-hosted Wallabag instance (version 2.x).
  • Python 3.8 or newer installed on a machine that can reach your Wallabag server.
  • A Pocket account with API access enabled.
  • Wallabag API client credentials.
  • A basic comfort level with running Python scripts and handling API keys.

The Guide: Step-by-Step Migration

Step 1: Gather Your Credentials

This is the most important step. We need four sets of credentials, which we’ll store securely. Never hard-code these into your script.

  1. Pocket Consumer Key: Go to the Pocket Developer Apps page, create a new application, and get your Consumer Key.
  2. Pocket Access Token: You’ll need to authorize your app. The simplest way is to follow Pocket’s documentation for a 3-legged OAuth flow. A quicker method for a personal script is to use a pre-existing tool like the one found at this simple authenticator to generate your token.
  3. Wallabag Client ID & Secret: In your Wallabag instance, go to Developer > Create a new client. Give it a name and Wallabag will generate a Client ID and Client Secret for you.
  4. Wallabag Username & Password: Your standard login credentials for your Wallabag account.

Create a file named config.env in your project directory and add your credentials like this:

POCKET_CONSUMER_KEY="your_pocket_consumer_key"
POCKET_ACCESS_TOKEN="your_pocket_access_token"
WALLABAG_URL="https://your-wallabag-instance.com"
WALLABAG_CLIENT_ID="your_wallabag_client_id"
WALLABAG_CLIENT_SECRET="your_wallabag_client_secret"
WALLABAG_USERNAME="your_wallabag_username"
WALLABAG_PASSWORD="your_wallabag_password"

Step 2: Set Up Your Python Environment

Okay, let’s get our workspace ready. I’ll skip the standard commands for creating a directory and a virtual environment since you likely have your own workflow for that. The key part is to install the necessary Python libraries. From your activated virtual environment, you’ll need to install two packages: requests for making HTTP calls and python-dotenv for loading our credentials securely from the config.env file. Just use your standard package installer for Python to get those set up.

Step 3: The Python Script – Fetching from Pocket

Let’s start building our script. Create a file, say migrate.py. First, we’ll load our credentials and write the function to fetch all our articles from Pocket.

import os
import requests
from dotenv import load_dotenv

# Load environment variables from config.env
load_dotenv('config.env')

# --- Pocket Configuration ---
POCKET_CONSUMER_KEY = os.getenv('POCKET_CONSUMER_KEY')
POCKET_ACCESS_TOKEN = os.getenv('POCKET_ACCESS_TOKEN')
POCKET_API_URL = 'https://getpocket.com/v3/get'

# --- Wallabag Configuration ---
WALLABAG_URL = os.getenv('WALLABAG_URL')
WALLABAG_CLIENT_ID = os.getenv('WALLABAG_CLIENT_ID')
WALLABAG_CLIENT_SECRET = os.getenv('WALLABAG_CLIENT_SECRET')
WALLABAG_USERNAME = os.getenv('WALLABAG_USERNAME')
WALLABAG_PASSWORD = os.getenv('WALLABAG_PASSWORD')


def fetch_pocket_articles():
    """Fetches all articles from the Pocket API."""
    print("Fetching articles from Pocket...")
    payload = {
        'consumer_key': POCKET_CONSUMER_KEY,
        'access_token': POCKET_ACCESS_TOKEN,
        'state': 'all',  # Fetch both archived and unread
        'detailType': 'complete' # Get all data including tags
    }
    try:
        response = requests.post(POCKET_API_URL, json=payload)
        response.raise_for_status()  # This will raise an HTTPError for bad responses (4xx or 5xx)
        articles = response.json().get('list', {})
        print(f"Found {len(articles)} articles in Pocket.")
        return articles
    except requests.exceptions.RequestException as e:
        print(f"Error fetching from Pocket: {e}")
        return None

Here, we’re making a POST request to Pocket’s `get` endpoint. We ask for `state: ‘all’` to ensure we migrate both your unread and archived items. The `detailType: ‘complete’` gives us everything, including the all-important tags.

Step 4: Authenticating and Posting to Wallabag

Next, we need to handle Wallabag. First, we’ll get an authentication token, and then we’ll write the function to post a single article. This modular approach makes the code cleaner.

Pro Tip: Notice the function `wallabag_entry_exists`. Before adding a new article, we check if it’s already there. This makes the script idempotent, meaning you can run it multiple times without creating duplicates. In my production scripts, this is a non-negotiable feature to prevent messes.

def get_wallabag_token():
    """Authenticates with Wallabag to get a JWT token."""
    token_url = f"{WALLABAG_URL}/oauth/v2/token"
    payload = {
        'grant_type': 'password',
        'client_id': WALLABAG_CLIENT_ID,
        'client_secret': WALLABAG_CLIENT_SECRET,
        'username': WALLABAG_USERNAME,
        'password': WALLABAG_PASSWORD
    }
    try:
        response = requests.post(token_url, data=payload)
        response.raise_for_status()
        return response.json()['access_token']
    except requests.exceptions.RequestException as e:
        print(f"Error getting Wallabag token: {e}")
        return None

def wallabag_entry_exists(token, url):
    """Checks if a URL already exists in Wallabag."""
    api_url = f"{WALLABAG_URL}/api/entries/exists.json"
    headers = {'Authorization': f'Bearer {token}'}
    params = {'url': url}
    try:
        response = requests.get(api_url, headers=headers, params=params)
        response.raise_for_status()
        return response.json().get('exists', False)
    except requests.exceptions.RequestException as e:
        print(f"Error checking if entry exists for {url}: {e}")
        return True # Assume it exists to be safe and avoid duplicates

def post_to_wallabag(token, article_data):
    """Posts a single article to the Wallabag API."""
    api_url = f"{WALLABAG_URL}/api/entries.json"
    headers = {'Authorization': f'Bearer {token}'}
    
    # Check if the entry already exists
    if wallabag_entry_exists(token, article_data['url']):
        print(f"Skipping existing article: {article_data['title']}")
        return True

    try:
        response = requests.post(api_url, headers=headers, json=article_data)
        response.raise_for_status()
        print(f"Successfully added: {article_data['title']}")
        return True
    except requests.exceptions.RequestException as e:
        print(f"Error adding article '{article_data['title']}': {e}")
        return False

Step 5: Tying It All Together

Now, let’s create the main execution block. This part will orchestrate the entire process: fetch from Pocket, get a Wallabag token, and then loop through the articles to post them.

def main():
    """Main function to run the migration."""
    pocket_articles = fetch_pocket_articles()
    if not pocket_articles:
        print("No articles to process. Exiting.")
        return

    wallabag_token = get_wallabag_token()
    if not wallabag_token:
        print("Could not authenticate with Wallabag. Exiting.")
        return

    print("\nStarting migration to Wallabag...")
    success_count = 0
    fail_count = 0

    for item_id, article in pocket_articles.items():
        # Pocket sometimes has items without a URL, we skip those
        if 'resolved_url' not in article or not article['resolved_url']:
            continue
            
        tags = ','.join(tag for tag in article.get('tags', {}).keys())
        
        wallabag_payload = {
            'url': article['resolved_url'],
            'title': article.get('resolved_title') or article.get('given_title', ''),
            'tags': tags,
            'archive': 1 if article['status'] == '1' else 0, # Map Pocket status to Wallabag archive status
        }
        
        if post_to_wallabag(wallabag_token, wallabag_payload):
            success_count += 1
        else:
            fail_count += 1
            
    print("\n--- Migration Complete ---")
    print(f"Successfully migrated: {success_count}")
    print(f"Failed to migrate: {fail_count}")

if __name__ == "__main__":
    main()

To run it, you just execute the Python script from your terminal. It will print its progress as it goes. For thousands of articles, give it some time to complete.

Step 6 (Optional): Automate with Cron

For an initial bulk import, you only need to run this once. However, if you want to keep using Pocket and sync new articles periodically, you can schedule it with cron. A job to run it every Monday at 2 AM would look like this:

0 2 * * 1 python3 migrate.py

Remember to adjust the script to only fetch *new* articles using Pocket’s `since` parameter to avoid re-processing your entire library every time.

Common Pitfalls

Here are a couple of things that tripped me up the first time:

  • Credential Typos: 90% of the time, the script fails on the first run because of a typo in the config.env file. Double-check your Wallabag URL (make sure it includes `https://`) and all your keys and secrets.
  • API Rate Limiting: Both services have rate limits. While they are generally high enough for personal use, if you have over 10,000 articles, you might consider adding a small delay (e.g., `time.sleep(0.5)`) inside the loop to be a good API citizen.
  • Pocket Items Without URLs: I discovered that some items saved to Pocket, like raw text or images, don’t have a `resolved_url`. The script I’ve provided now checks for this and skips them to prevent errors.

Conclusion

And there you have it. A robust, reusable script to take your data from Pocket and move it into your own self-hosted Wallabag instance. This isn’t just a one-off migration; it’s a template for taking control of your data from other services, too. By owning your “read later” platform, you’re building a more resilient and private digital workspace. Happy hosting.

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ How can I automate the migration of my Pocket bookmarks to a self-hosted Wallabag instance?

You can automate this using a Python script that leverages the Pocket and Wallabag APIs. The script fetches articles from Pocket, authenticates with Wallabag using client credentials, and then posts each article’s URL, title, and tags to your Wallabag instance.

âť“ How does self-hosting Wallabag for ‘read-it-later’ compare to using a service like Pocket?

Self-hosting Wallabag provides full control over your data, offering enhanced privacy and resilience by owning your ‘read later’ platform. Unlike Pocket, which is a proprietary cloud service, Wallabag allows for customization and avoids vendor lock-in, aligning with principles of data ownership.

âť“ What is a common implementation pitfall when running the Pocket to Wallabag migration script, and how can it be resolved?

A common pitfall is credential typos in the `config.env` file, particularly for the Wallabag URL (ensure `https://`) or API keys/secrets. This can be resolved by meticulously double-checking all entered credentials against their source to ensure accuracy before execution.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading