API Documentation
The Quick Reads API allows you to save articles, manage your reading queue, and create highlights programmatically. Use it to build browser extensions, integrations, or automate your reading workflow.
Terms of Use #
You're welcome to build almost anything on top of the Quick Reads API. Browser extensions, automations, integrations, personal scripts, and public tools are all fair game, and you don't need to ask permission first.
There is one rule. Do not use the API to give other people access to Quick Reads without their own account. Specifically, you may not:
- Resell, proxy, or share your API access so that people who haven't signed up can use Quick Reads through your software.
- Build a service that lets users access Quick Reads features without creating their own account.
- Work around subscription tiers or feature gating, such as exposing paid features like text-to-speech to users who aren't entitled to them.
In short: build freely, but every end user should be signed in with their own account.
Not sure whether something is allowed?
Quick Start #
The most common things people do with the API. Click any card to jump to its endpoint.
Authentication #
All API requests require authentication using an API key. Include your API key in the Authorization header as a Bearer token:
Authorization: Bearer rl_your_api_key_here
You can create API keys in your Settings page.
Base URL #
https://quickreads.app/api
Account #
GET /api/me #
Get the authenticated user's account info, including their subscription tier and feature entitlements. Use this to determine whether the user can access tier-gated features such as text-to-speech.
This endpoint is reachable even when the user has no active subscription, so integrators can detect the "free" tier and prompt the user to upgrade.
Response
{
"id": "abc123",
"email": "[email protected]",
"tier": "pro",
"features": {
"tts": true
},
"subscription": {
"status": "active",
"currentPeriodEnd": "2026-12-31T00:00:00.000Z",
"cancelAtPeriodEnd": false
}
}
| Field | Type | Description |
|---|---|---|
tier | string | The user's paid plan: "basic", "pro", or "free" (no active subscription). |
features.tts | boolean | Whether the user is currently entitled to use text-to-speech. true for Pro subscribers and a small set of premium accounts; false for Basic and free users. |
subscription.status | string | Stripe subscription status: "active", "trialing", "past_due", "canceled", or "none". |
subscription.currentPeriodEnd | string | ISO 8601 date string for when the current billing period ends. null when there is no subscription. |
subscription.cancelAtPeriodEnd | boolean | Whether the subscription will end at the current period end. |
Prefer reading features.* over tier when gating UI: feature mappings may change as new tiers are introduced, but feature flags will stay stable.
Articles #
POST /api/articles #
Save a new article to your reading queue. By default the URL is fetched and parsed for readable content. You can also provide pre-parsed content via extractedContent (e.g. from a browser extension using Readability) to skip server-side fetching.
Request Body
{
"url": "https://example.com/article",
"publishedAt": "2024-09-15T08:00:00.000Z",
"source": "api",
"extractedContent": {
"title": "Article Title",
"content": "<p>Article HTML content...</p>",
"textContent": "Article plain text content...",
"excerpt": "A short summary of the article",
"byline": "Author Name",
"siteName": "Example",
"publishedAt": "2024-09-15T08:00:00.000Z"
}
}
| Field | Type | Description |
|---|---|---|
url | string | Required. The URL to save. |
publishedAt | string | ISO 8601 date string for when the article was originally published. Overrides any date extracted from the page or from extractedContent. |
source | string | Optional. Pass "api" to mark the save as coming from an API integration. Used for reporting only. |
extractedContent | object | Pre-parsed article content from the client. When provided, the server uses this instead of fetching the URL. Falls back to server-side parsing if invalid. |
extractedContent fields:
| Field | Type | Description |
|---|---|---|
content | string | Article HTML content (will be sanitized server-side). Required for extractedContent to be used. |
textContent | string | Plain text version of the article. Used to compute word count. |
title | string | Article title. Defaults to "Untitled". |
excerpt | string | Short summary or description. |
byline | string | Author name. |
siteName | string | Name of the source site. Defaults to the URL's domain. |
publishedAt | string | ISO 8601 date string for when the article was originally published. |
Response
{
"id": "abc123",
"userId": "user-uuid",
"url": "https://example.com/article",
"title": "Article Title",
"author": "Author Name",
"siteName": "Example",
"content": "<p>Article HTML content...</p>",
"excerpt": "A short summary of the article",
"wordCount": 1250,
"type": "article",
"faviconUrl": "https://example.com/favicon.png",
"imageUrl": "https://example.com/article-image.jpg",
"publishedAt": "2024-09-15T08:00:00.000Z",
"savedAt": "2025-01-21T10:30:00.000Z",
"archivedAt": null
}
The type field is "article" for parsed content or "link" when parsing fails. Links have null content and a word count of 0. The publishedAt field is null when no publication date could be determined.
GET /api/articles #
List articles in your queue.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
archived | boolean | Set to true to list archived articles. Default: false |
limit | integer | Maximum number of articles to return. Default: 50 |
offset | integer | Number of articles to skip. Default: 0 |
Response
[
{
"id": "abc123",
"userId": "user-uuid",
"url": "https://example.com/article",
"title": "Article Title",
"author": "Author Name",
"siteName": "Example",
"content": "<p>Article HTML content...</p>",
"excerpt": "A short summary of the article",
"wordCount": 1250,
"highlightCount": 3,
"type": "article",
"faviconUrl": "https://example.com/favicon.png",
"imageUrl": "https://example.com/article-image.jpg",
"audioChunks": null,
"publishedAt": "2024-09-15T08:00:00.000Z",
"savedAt": "2025-01-21T10:30:00.000Z",
"archivedAt": null,
"readAnchorIndex": null,
"readAnchorTotal": null,
"readProgress": 0,
"listenProgressSeconds": null,
"progressUpdatedAt": null,
"tags": [
{ "id": "tag123", "name": "Tech", "color": "blue" }
]
}
]
List responses return the full article shape (including content) — same fields as GET /api/articles/:id, plus highlightCount (number of highlights on the article) and the tags array. Articles with no tags have an empty array. When TTS audio has been generated for an article, an audio block is also included (see GET /api/articles/:id below).
GET /api/articles/search #
Search across all your saved articles (both queue and archived).
Query Parameters
| Parameter | Type | Description |
|---|---|---|
q | string | Search query. Matches against article title and content. |
tag | string | Filter by tag ID. Can be combined with q. |
dateField | string | Which date to filter on: published, saved, or archived. Required to use dateFrom/dateTo. |
dateFrom | string | ISO 8601 date (inclusive lower bound) for dateField. |
dateTo | string | ISO 8601 date (inclusive upper bound, expanded to end-of-day) for dateField. |
limit | integer | Maximum number of articles to return (1–200). Default: 50 |
At least one of q, tag, or a date filter (dateField + dateFrom/dateTo) must be provided.
Response
Same shape as GET /api/articles — full article objects with highlightCount and a tags array.
[
{
"id": "abc123",
"userId": "user-uuid",
"url": "https://example.com/article",
"title": "Article Title",
"author": "Author Name",
"siteName": "Example",
"content": "<p>Article HTML content...</p>",
"excerpt": "A short summary of the article",
"wordCount": 1250,
"highlightCount": 3,
"type": "article",
"faviconUrl": "https://example.com/favicon.png",
"imageUrl": "https://example.com/article-image.jpg",
"audioChunks": null,
"publishedAt": "2024-09-15T08:00:00.000Z",
"savedAt": "2025-01-21T10:30:00.000Z",
"archivedAt": null,
"readAnchorIndex": null,
"readAnchorTotal": null,
"readProgress": 0,
"listenProgressSeconds": null,
"progressUpdatedAt": null,
"tags": [
{ "id": "tag123", "name": "Tech", "color": "blue" }
]
}
]
GET /api/articles/:id #
Get a single article by ID, including its full content.
Response
{
"id": "abc123",
"userId": "user-uuid",
"url": "https://example.com/article",
"title": "Article Title",
"author": "Author Name",
"siteName": "Example",
"content": "<p>Article HTML content...</p>",
"excerpt": "A short summary of the article",
"wordCount": 1250,
"type": "article",
"faviconUrl": "https://example.com/favicon.png",
"imageUrl": "https://example.com/article-image.jpg",
"publishedAt": "2024-09-15T08:00:00.000Z",
"savedAt": "2025-01-21T10:30:00.000Z",
"archivedAt": null,
"audioChunks": 9,
"audio": {
"totalChunks": 9,
"chunks": [
{
"index": 0,
"audioUrl": "https://cdn.example.com/tts/abc123/chunk-0.mp3",
"alignmentUrl": "https://cdn.example.com/tts/abc123/chunk-0.json"
}
]
},
"readAnchorIndex": 11,
"readAnchorTotal": 24,
"readProgress": 0.5,
"listenProgressSeconds": 750,
"progressUpdatedAt": "2025-01-21T10:35:00.000Z"
}
The audio field is present when text-to-speech audio has been generated for the article. It contains direct URLs to MP3 audio files and word-level alignment data for each chunk. audioChunks is null when no audio has been generated. The audio field also appears on articles returned by the list and search endpoints.
readAnchorIndex is the 0-based index of the paragraph-level block the reader last viewed; readAnchorTotal is the total number of such blocks at save time. Both are null for unread articles. readProgress is a derived, read-only convenience field equal to (readAnchorIndex + 1) / readAnchorTotal, or 0 when the article is unread. listenProgressSeconds is the seconds-elapsed in TTS audio where the user last paused (null if never listened, 0 if listened to completion). progressUpdatedAt is the ISO timestamp of the most recent progress update (null if none).
POST /api/articles/:id/unarchive #
Restore an archived article to your reading queue.
Response
{ "success": true }
PATCH /api/articles/:id #
Update an article's title, content, or author. At least one field must be provided. When content is updated, word count is recalculated automatically. Pass an empty string for author to clear it.
Request Body
{
"title": "Updated Title",
"content": "<p>Updated HTML content...</p>",
"author": "Author Name"
}
| Field | Type | Description |
|---|---|---|
title | string | New title. Must be a non-empty string. |
content | string | New HTML content. Will be sanitized. Word count is recalculated. Cached TTS audio is invalidated. |
author | string | New author name. Max 200 characters. Empty string clears the author. |
Response
Returns the updated article object (same shape as GET /api/articles/:id).
PATCH /api/articles/:id/progress #
Update reading or listening progress for an article. Send anchorIndex and anchorTotal together to update the reading anchor, and/or listenProgressSeconds to update the listening position. At least one of those updates must be present. The legacy readProgress field is no longer accepted and will return 400.
Request Body
{
"anchorIndex": 11,
"anchorTotal": 24,
"listenProgressSeconds": 750
}
Response
Returns the updated article object (same shape as GET /api/articles/:id), including the updated readAnchorIndex, readAnchorTotal, derived readProgress, listenProgressSeconds, and progressUpdatedAt.
Highlights #
POST /api/highlights #
Create a new highlight.
Request Body
{
"articleId": "abc123",
"text": "The highlighted text passage"
}
Response
{
"id": "xyz789",
"userId": "user-uuid",
"articleId": "abc123",
"text": "The highlighted text passage",
"createdAt": "2025-01-21T10:35:00.000Z"
}
GET /api/highlights #
List your highlights across all articles, paginated and ordered by most recent first.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
limit | integer | Maximum number of highlights to return (1–200). Default: 50 |
offset | integer | Number of highlights to skip. Default: 0 |
Response
{
"highlights": [
{
"id": "xyz789",
"userId": "user-uuid",
"articleId": "abc123",
"articleTitle": "Article Title",
"url": "https://example.com/article",
"siteName": "Example",
"author": "Author Name",
"faviconUrl": "https://example.com/favicon.png",
"publishedAt": "2024-09-15T08:00:00.000Z",
"text": "The highlighted text passage",
"createdAt": "2025-01-21T10:35:00.000Z"
}
],
"total": 42
}
total is the total number of highlights for the user (not just the current page), useful for paginating.
GET /api/articles/:id/highlights #
List all highlights for a specific article.
Response
[
{
"id": "xyz789",
"userId": "user-uuid",
"articleId": "abc123",
"text": "The highlighted text passage",
"createdAt": "2025-01-21T10:35:00.000Z"
}
]
Tags #
Every tag has a color chosen from a fixed palette. A random color is assigned when a tag is created, and can be changed via PATCH /api/tags/:id. Allowed values: blue, purple, orange, green, yellow, red, gray.
GET /api/tags #
List all your tags with article counts.
Response
[
{
"id": "tag123",
"name": "Tech",
"color": "blue",
"createdAt": "2025-01-21T10:35:00.000Z",
"articleCount": 5
}
]
POST /api/tags #
Create a new tag. If color is omitted, one is picked at random from the palette.
Request Body
{
"name": "Tech",
"color": "blue"
}
Response
{
"id": "tag123",
"name": "Tech",
"color": "blue",
"createdAt": "2025-01-21T10:35:00.000Z"
}
PATCH /api/tags/:id #
Update a tag. Provide name, color, or both. At least one must be present.
Request Body
{
"name": "Technology",
"color": "purple"
}
Response
{ "success": true }
DELETE /api/tags/:id #
Delete a tag. This removes the tag from all articles.
Response
{ "success": true }
GET /api/articles/:id/tags #
List all tags for a specific article.
Response
[
{
"id": "tag123",
"name": "Tech",
"color": "blue"
}
]
PUT /api/articles/:id/tags #
Set tags on an article. Replaces all existing tags on the article.
Request Body
{
"tagIds": ["tag123", "tag456"]
}
Response
[
{
"id": "tag123",
"name": "Tech",
"color": "blue"
},
{
"id": "tag456",
"name": "News",
"color": "orange"
}
]
Errors #
The API returns standard HTTP status codes. Error responses include a JSON body with an error field:
{
"error": "Article not found"
}
| Status | Description |
|---|---|
400 | Bad Request - Missing or invalid parameters |
401 | Unauthorized - Invalid or missing API key |
404 | Not Found - Resource doesn't exist |
409 | Conflict - Resource already exists (e.g. saving a URL you've already saved, or creating a duplicate tag) |
500 | Internal Server Error |
Examples #
Save an article (parsed)
curl -X POST https://quickreads.app/api/articles \
-H "Authorization: Bearer rl_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/article"}'
Save with pre-extracted content (e.g. from a browser extension)
curl -X POST https://quickreads.app/api/articles \
-H "Authorization: Bearer rl_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/article",
"extractedContent": {
"title": "Article Title",
"content": "<p>Article HTML...</p>",
"textContent": "Article plain text...",
"excerpt": "Summary of the article",
"byline": "Author Name",
"siteName": "Example"
}
}'