Reading List API
February 26, 2026 ยท View on GitHub
Reading List endpoints support capture, extraction, organization, import/export, and actions (summarize/TTS).
Endpoints (MVP)
POST /api/v1/reading/save- save a URL (or inline content)GET /api/v1/reading/items- list items with filters/searchGET /api/v1/reading/items/{id}- item detail with text/clean_htmlPATCH /api/v1/reading/items/{id}- update metadata (status/tags/notes/title)DELETE /api/v1/reading/items/{id}- soft delete (archived) or hard deletePOST /api/v1/reading/items/{id}/summarize- summarize an itemPOST /api/v1/reading/items/{id}/tts- generate TTS audio for an itemPOST /api/v1/reading/items/{id}/archive- create an archive snapshot (HTML/MD)POST /api/v1/reading/import- Pocket/Instapaper import (multipart, async job)GET /api/v1/reading/import/jobs- list reading import jobsGET /api/v1/reading/import/jobs/{job_id}- get reading import job statusGET /api/v1/reading/export- JSONL or ZIP exportPOST /api/v1/reading/digests/schedules- create a digest scheduleGET /api/v1/reading/digests/schedules- list digest schedulesGET /api/v1/reading/digests/schedules/{schedule_id}- get schedulePATCH /api/v1/reading/digests/schedules/{schedule_id}- update scheduleDELETE /api/v1/reading/digests/schedules/{schedule_id}- delete scheduleGET /api/v1/reading/digests/outputs- list digest output artifacts
Core object: ReadingItem
Key fields:
id,url,canonical_url,titlestatus:saved|reading|read|archivedprocessing_status:processing|readytags,favoritemedia_id(link to Media DB when ingested)created_at,updated_at,read_at
Save
POST /api/v1/reading/save
Request:
{
"url": "https://example.com/article",
"title": "Example Article",
"tags": ["ai", "reading"],
"notes": "Why it matters"
}
Response (ReadingItem):
{
"id": 123,
"title": "Example Article",
"url": "https://example.com/article",
"canonical_url": "https://example.com/article",
"status": "saved",
"processing_status": "processing",
"favorite": false,
"tags": ["ai", "reading"],
"created_at": "2025-10-19T09:15:00Z",
"updated_at": "2025-10-19T09:15:00Z"
}
Notes:
- Use
contentin the request body to supply inline text (offline/testing). - Non-HTML URLs (PDF, EPUB, etc.) are routed into the document ingestion pipeline.
List
GET /api/v1/reading/items
Filters: q, tags, status, favorite, domain, pagination (page/size), offset/limit, and sort.
Sort options: updated_desc (default), updated_asc, created_desc, created_asc, title_asc, title_desc, relevance.
Example:
GET /api/v1/reading/items?status=saved&tags=ai&favorite=true&q=vector&sort=updated_desc&limit=20&offset=0
Response:
{
"items": [...],
"total": 42,
"page": 1,
"size": 20,
"offset": 0,
"limit": 20
}
Detail
GET /api/v1/reading/items/{id}
Response (ReadingItemDetail) includes:
text(plain text)clean_html(sanitized HTML)metadata(extra fields likereading_time_seconds,media_uuid)
Update
PATCH /api/v1/reading/items/{id}
Request:
{
"status": "read",
"favorite": true,
"tags": ["ai", "priority"],
"notes": "Follow up later"
}
Response: updated ReadingItem.
Delete
DELETE /api/v1/reading/items/{id}
- Soft delete (default) marks the item as
archived. - Hard delete:
?hard=true.
Response:
{ "status": "archived", "item_id": 123, "hard": false }
Summarize
POST /api/v1/reading/items/{id}/summarize
Request:
{
"provider": "openai",
"model": "gpt-4o",
"prompt": "Summarize for a product brief."
}
Response:
{
"item_id": 123,
"summary": "Short summary text...",
"provider": "openai",
"model": "gpt-4o",
"citations": [
{
"item_id": 123,
"url": "https://example.com/article",
"canonical_url": "https://example.com/article",
"title": "Example Article",
"source": "reading"
}
],
"generated_at": "2025-10-19T09:20:00Z"
}
TTS
POST /api/v1/reading/items/{id}/tts
Request:
{
"model": "kokoro",
"voice": "af_heart",
"response_format": "mp3",
"stream": true,
"text_source": "text",
"max_chars": 12000
}
Response:
- Streaming audio bytes (Content-Type based on
response_format) - Non-streaming mode returns raw bytes in the response body
Import (Async)
POST /api/v1/reading/import (multipart)
Form fields:
file: Pocket JSON or Instapaper CSVsource:auto|pocket|instapapermerge_tags:true|false
Example:
multipart form
file = (pocket.json)
source = pocket
merge_tags = true
Response (202 Accepted):
{
"job_id": 42,
"job_uuid": "b7fbd5a0-3b37-4a2d-b6c2-0d0b9d3a6c1c",
"status": "queued"
}
Import normalization behavior:
- URLs are canonicalized (tracking params removed where possible) before dedupe/upsert.
- Equivalent URLs in one import batch are merged with tag union and strongest status precedence (
read > archived > reading > saved). - Missing titles are backfilled from URL path/domain.
- If merged status is
readand no timestamp is provided,read_atis set automatically.
Reading Digests
Create schedule
POST /api/v1/reading/digests/schedules
Request:
{
"name": "Morning Digest",
"cron": "0 8 * * *",
"timezone": "UTC",
"format": "md",
"filters": {
"status": ["saved", "reading"],
"tags": ["ai"],
"limit": 50
}
}
Optional suggestions block:
{
"filters": {
"status": ["saved"],
"tags": ["ai"],
"limit": 50,
"suggestions": {
"enabled": true,
"limit": 5,
"status": ["saved", "reading"],
"exclude_tags": ["ignore"],
"max_age_days": 90,
"include_read": false,
"include_archived": false
}
}
}
Response:
{ "id": "5a6f0e9d3d1b4b2f8c3d5a9c7b1a2e3f" }
List schedules
GET /api/v1/reading/digests/schedules
Response:
[
{
"id": "5a6f0e9d3d1b4b2f8c3d5a9c7b1a2e3f",
"name": "Morning Digest",
"cron": "0 8 * * *",
"timezone": "UTC",
"enabled": true,
"require_online": false,
"format": "md",
"filters": {
"status": ["saved", "reading"],
"tags": ["ai"],
"limit": 50
},
"last_run_at": null,
"next_run_at": "2025-10-20T08:00:00+00:00"
}
]
Digest suggestions (optional)
If filters.suggestions.enabled is true, the digest job adds a suggestions list to the template context.
Fields:
enabled: turn suggestions on/off.limit: max suggestions (default 5).status: candidate statuses (default["saved", "reading"]).exclude_tags: tags to exclude from suggestions.max_age_days: drop items older than N days.include_read: allowreaditems even if status list excludes them.include_archived: allowarchiveditems even if status list excludes them.
Digest template context
The output template receives:
items: digest items.item_count: number of digest items.filters: the schedule filters.suggestions: suggestion items (optional).suggestions_meta:{count, scores, reasons}keyed by item id (optional).
Sample template snippet:
{% if suggestions %}
## Suggested reads
{% for item in suggestions %}
- {{ item.title }} ({{ item.url }})
{% endfor %}
{% endif %}
List digest outputs
GET /api/v1/reading/digests/outputs
Response:
{
"items": [
{
"output_id": 11,
"title": "Morning Digest",
"format": "md",
"created_at": "2025-10-20T08:00:01Z",
"download_url": "/api/v1/outputs/11/download",
"schedule_id": "5a6f0e9d3d1b4b2f8c3d5a9c7b1a2e3f",
"item_count": 42
}
],
"total": 1,
"limit": 50,
"offset": 0
}
Notes:
- Output metadata includes
suggestions_count,suggestions_item_ids, andsuggestions_configwhen enabled.
Import Jobs
GET /api/v1/reading/import/jobs
Response:
{
"jobs": [
{
"job_id": 42,
"job_uuid": "b7fbd5a0-3b37-4a2d-b6c2-0d0b9d3a6c1c",
"status": "completed",
"created_at": "2025-10-19T09:15:00Z",
"completed_at": "2025-10-19T09:15:05Z",
"result": {
"source": "pocket",
"imported": 12,
"updated": 3,
"skipped": 1,
"errors": []
}
}
],
"total": 1,
"limit": 50,
"offset": 0
}
GET /api/v1/reading/import/jobs/{job_id}
Response:
{
"job_id": 42,
"job_uuid": "b7fbd5a0-3b37-4a2d-b6c2-0d0b9d3a6c1c",
"status": "completed",
"created_at": "2025-10-19T09:15:00Z",
"completed_at": "2025-10-19T09:15:05Z",
"result": {
"source": "pocket",
"imported": 12,
"updated": 3,
"skipped": 1,
"errors": []
}
}
Export
GET /api/v1/reading/export
Query params:
format:jsonl(default) orzip- filters:
status,tags,favorite,q,domain - optional flags:
include_metadata,include_clean_html,include_text,include_highlights
JSONL line example:
{"id":123,"url":"https://example.com/article","canonical_url":"https://example.com/article","domain":"example.com","title":"Example Article","summary":"...","notes":null,"status":"saved","favorite":0,"tags":["ai"],"created_at":"2025-10-19T09:15:00Z","updated_at":"2025-10-19T09:15:00Z","read_at":null,"published_at":null,"origin_type":"manual","metadata":{"import_source":"pocket"}}
Archive
POST /api/v1/reading/items/{id}/archive
Request:
{
"format": "html",
"source": "auto",
"retention_days": 30
}
Response:
{
"output_id": 101,
"title": "Example Article (archive 20251021_120000)",
"format": "html",
"storage_path": "reading_archive_123_example_article_20251021_120000.html",
"created_at": "2025-10-21T12:00:00+00:00",
"retention_until": "2025-11-20T12:00:00+00:00",
"download_url": "/api/v1/outputs/101/download"
}