Sign In

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
  }
}
FieldTypeDescription
tierstringThe user's paid plan: "basic", "pro", or "free" (no active subscription).
features.ttsbooleanWhether 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.statusstringStripe subscription status: "active", "trialing", "past_due", "canceled", or "none".
subscription.currentPeriodEndstringISO 8601 date string for when the current billing period ends. null when there is no subscription.
subscription.cancelAtPeriodEndbooleanWhether 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"
  }
}
FieldTypeDescription
urlstringRequired. The URL to save.
publishedAtstringISO 8601 date string for when the article was originally published. Overrides any date extracted from the page or from extractedContent.
sourcestringOptional. Pass "api" to mark the save as coming from an API integration. Used for reporting only.
extractedContentobjectPre-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:

FieldTypeDescription
contentstringArticle HTML content (will be sanitized server-side). Required for extractedContent to be used.
textContentstringPlain text version of the article. Used to compute word count.
titlestringArticle title. Defaults to "Untitled".
excerptstringShort summary or description.
bylinestringAuthor name.
siteNamestringName of the source site. Defaults to the URL's domain.
publishedAtstringISO 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

ParameterTypeDescription
archivedbooleanSet to true to list archived articles. Default: false
limitintegerMaximum number of articles to return. Default: 50
offsetintegerNumber 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/: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/archive #

Archive an article.

Response

{ "success": true }

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"
}
FieldTypeDescription
titlestringNew title. Must be a non-empty string.
contentstringNew HTML content. Will be sanitized. Word count is recalculated. Cached TTS audio is invalidated.
authorstringNew 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.

DELETE /api/articles/:id #

Permanently delete an article.

Response

{ "success": true }

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

ParameterTypeDescription
limitintegerMaximum number of highlights to return (1–200). Default: 50
offsetintegerNumber 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"
  }
]

DELETE /api/highlights/:id #

Delete a highlight.

Response

{ "success": true }

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"
}
StatusDescription
400Bad Request - Missing or invalid parameters
401Unauthorized - Invalid or missing API key
404Not Found - Resource doesn't exist
409Conflict - Resource already exists (e.g. saving a URL you've already saved, or creating a duplicate tag)
500Internal 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"
    }
  }'