Pocket closed on July 8, 2025, and its exports could be downloaded until October 8, 2025. If you got yours, you have a CSV file, and that file is what is left of everything you saved there.
I built KeepFinding, a read-later app, and its import reads that file as it is. Here is what is actually in the Pocket export and the Raindrop.io one, and what my importer does with each column.
What is in a Pocket export
Pocket's download may be a .zip. The data is the .csv inside, and if there are several, you import them one after another. Each has five columns:
| Column | What it holds | What the import does |
|---|---|---|
title |
the title Pocket had | the link's title |
url |
the saved link | normalised, then the key for duplicates |
time_added |
unix seconds | the date you saved it |
tags |
pipe separated, like `essays\ | writing` |
status |
unread or archive
|
archive goes to Archive |
That is all of it. No images and no excerpts, so those are fetched later.
And in a Raindrop export
Raindrop's CSV has eleven columns: id,title,note,excerpt,url,folder,tags,created,cover,highlights,favorite. What matters for a parser:
-
notecan span several lines inside one quoted cell. -
tagsare comma separated inside one quoted cell, so the delimiter shows up inside a field. -
createdis ISO 8601, not unix time. - A full backup (Settings, then Backups) writes the collection path in
folder, likeDesign / Fonts. Exporting one collection gives a file with nofoldercolumn at all.
I keep the title, note, excerpt (as the description), folder, tags, date and favorite. I do not read id or cover, and highlights do not come across.
Detect by header, not by file name
People rename files, so the format is read from the first line. A Pocket header has url and time_added plus status or tags. A Raindrop header has url, one of excerpt, cover or highlights, and folder or created. Any other CSV with a URL column takes a generic path. One test feeds the Pocket fixture in as export.txt and expects Pocket back.
The CSV reader is about 35 lines with no dependency: quoted line breaks, "" as an escaped quote, a byte order mark ignored, and the delimiter sniffed from the header.
Dates: one function for every epoch
Pocket writes seconds, Raindrop writes ISO strings, and some files write milliseconds. Instead of a branch per source, the parser guesses the unit from the size of the number and refuses anything implausible:
if (ms === null) return undefined;
// Seconds vs milliseconds vs microseconds (Chrome's WebKit epoch is not used in exports).
if (ms < 1e11) ms *= 1000;
else if (ms > 1e14) ms /= 1000;
const min = Date.UTC(1995, 0, 1);
const max = Date.now() + 24 * 60 * 60 * 1000;
if (ms < min || ms > max) return undefined;
return new Date(ms).toISOString();
A date before 1995 or more than a day in the future is dropped, not guessed. The link still imports without it.
Tags become topics, collections become lists
In KeepFinding a list is a prompt you write, like "podcasts under 20 minutes", and a rule answers the simple ones. Each Pocket tag becomes a topic on the link, and a list with the prompt tagged recipes gathers everything you tagged recipes.
For Raindrop, each top-level collection in a backup can become a list the same way: the import writes a tagged <collection> prompt and names the list after the collection. Nested collection names become topics.
Real data found a bug here. I ran two Raindrop exports published on GitHub through the real import path. In one, 28 of the 96 rows sat in Unsorted, which is Raindrop's inbox and not a subject, and the import turned it into a topic and a list. The fix strips it from the path:
export function parseRaindropCsv(text: string): RawParse {
const parsed = parseGenericCsv(text, { tagSplit: /,/ });
for (const record of parsed.records) {
if (!record.folder) continue;
const path = record.folder.split(' / ');
if (path[0].toLowerCase() !== 'unsorted') continue;
path.shift();
if (path.length) record.folder = path.join(' / ');
else delete record.folder;
}
return parsed;
}
Duplicates, counted twice
Every URL is normalised first: utm_*, fbclid and other tracking parameters go, www. and trailing slashes go, http becomes https. So example.com/hello-world?utm_source=pocket and example.com/hello-world are one link, and that pair is in my Pocket test fixture.
Inside the file, the first occurrence wins. Against your library, links you already have are skipped. My first summary called both "already saved", which was wrong on a first import, so the dry run now returns repeated for the file and duplicates for the library. Importing the same file twice adds nothing.
No network while importing
Fetching every page while importing would mean thousands of page loads, so the import writes rows from the file alone, 100 links per request, and marks each row as owed a fetch. A second endpoint fills in covers and summaries ten rows at a time. Every batch is checked against the library again, so a reload halfway through leaves a consistent library. A page that is gone keeps its title and link.
Limits: 5 MB and 5,000 links per file, 3,000 links per account. Archived links count, because leaving them out let one Pocket file full of archived rows walk past the cap.
Instapaper, Omnivore and Readwise Reader
When I first wrote this, Instapaper's CSV only came in through the generic path, and its Archive and Starred folders became topics instead of archiving or starring. That was not correct enough to promise, so each of these now has its own parser:
- Instapaper:
URL,Title,Selection,Folder,Timestamp, with unix seconds.Unreadis Instapaper's inbox, so it becomes nothing;Archivearrives archived;Starredarrives starred; a folder you made can become a list. Private items such as forwarded emails have aninstapaper-private://address with no public page, so they are skipped. - Omnivore (closed in November 2024): the export is a zip of
metadata_*.jsonfiles. Each one imports on its own,state: "Archived"arrives archived, and the HTML and highlights in the zip are not read. - Readwise Reader's library CSV has a
Locationcolumn.feedrows are RSS items Reader added on its own, not things you saved, so they are skipped;archivearrives archived. Its tags look like a Python list,['a', 'b'].
Detection works the same way: an Instapaper header has url, selection, folder and timestamp, and a Reader header has url, title, document_tags, saved_date and location.
Try it
You do not need an account. Open keepfind.ing/pocket, keepfind.ing/raindrop or keepfind.ing/instapaper, save any link on Home, and KeepFinding starts a trial library in your browser. Settings then shows Your data, where you choose your export file, and it tells you what it found before anything is added. A trial library takes up to 80 links, and it comes with you if you create an account later.
A question for anyone who still has one of these files: does your header match the ones above? My fixtures are trimmed copies of the exports I could find, and if yours has a column or quirk I have not seen, I would rather fix the parser than guess.
Top comments (0)