Skip to main content

API Reference

Lektr exposes a REST API at /api/v1/. All endpoints return JSON responses.

Authentication

All API endpoints (except auth routes) require authentication via HTTP-only cookie.

Register

POST /api/v1/auth/register
Content-Type: application/json

{
"email": "user@example.com",
"password": "password123"
}

Response (201):

{
"success": true,
"user": {
"id": "uuid",
"email": "user@example.com",
"role": "user"
}
}

[!NOTE] The first user to register automatically becomes an admin.

Login

POST /api/v1/auth/login
Content-Type: application/json

{
"email": "user@example.com",
"password": "password123"
}

Response: Sets auth_token cookie and returns user object.

Logout

POST /api/v1/auth/logout

Current User

GET /api/v1/auth/me

Change Password

PUT /api/v1/auth/password
Content-Type: application/json

{
"currentPassword": "oldPassword123",
"newPassword": "newSecurePassword456"
}

Change Email

PUT /api/v1/auth/email
Content-Type: application/json

{
"newEmail": "newemail@example.com",
"password": "currentPassword123"
}

Response:

{
"success": true,
"email": "newemail@example.com"
}

Books

List Books

GET /api/v1/books

Response:

{
"books": [
{
"id": "uuid",
"title": "Atomic Habits",
"author": "James Clear",
"coverImageUrl": "/api/v1/covers/...",
"highlightCount": 42,
"tags": []
}
]
}

Get Book

GET /api/v1/books/:id

Update Book

PATCH /api/v1/books/:id
Content-Type: application/json

{
"title": "New Title",
"author": "New Author"
}

Update Book Metadata

Manually update or refresh book metadata from external sources.

PATCH /api/v1/books/:id/metadata
Content-Type: application/json

{
"coverImageUrl": "https://...",
"description": "Book description",
"pageCount": 320
}

Delete Book

Soft-deletes a book and all its highlights.

DELETE /api/v1/books/:id

Highlights

Get Highlights for Book

GET /api/v1/books/:bookId/highlights

Create Highlight

POST /api/v1/books/:bookId/highlights
Content-Type: application/json

{
"content": "Highlight text",
"note": "Optional note",
"chapter": "Chapter 1",
"page": 42
}

Update Highlight

PATCH /api/v1/books/:bookId/highlights/:highlightId
Content-Type: application/json

{
"content": "Updated text",
"note": "Updated note"
}

Delete Highlight

Soft-deletes a highlight (can be restored from trash).

DELETE /api/v1/books/:bookId/highlights/:highlightId

Toggle Pin

Pin or unpin a highlight for quick access.

POST /api/v1/books/:bookId/highlights/:highlightId/pin

Restore Highlight

Restore a soft-deleted highlight from trash.

POST /api/v1/books/:bookId/highlights/:highlightId/restore

Hard Delete Highlight

Permanently delete a highlight (cannot be undone).

DELETE /api/v1/books/:bookId/highlights/:highlightId/hard-delete

Trash

List Deleted Items

Get all soft-deleted books and highlights.

GET /api/v1/trash

Response:

{
"books": [],
"highlights": [
{
"id": "uuid",
"content": "Deleted highlight...",
"deletedAt": "2024-01-15T10:30:00Z"
}
]
}

Performs semantic + full-text hybrid search across all highlights.

GET /api/v1/search?q=query&limit=20

Response:

{
"highlights": [
{
"id": "highlight-uuid",
"content": "Matching highlight...",
"bookId": "book-uuid",
"bookTitle": "Book Name",
"bookAuthor": "Author",
"similarity": 0.85
}
]
}

Generate Embeddings

Trigger embedding generation for highlights without embeddings.

POST /api/v1/search/generate-embeddings

Embedding Status

Check the status of embedding generation.

GET /api/v1/search/embedding-status

Response:

{
"total": 500,
"complete": 450,
"pending": 50,
"percentComplete": 90
}

Tags

List Tags

GET /api/v1/tags

Response:

{
"tags": [
{
"id": "uuid",
"name": "philosophy",
"color": "#8B5CF6",
"highlightCount": 15,
"bookCount": 3
}
]
}

Get Tag

GET /api/v1/tags/:id

Create Tag

POST /api/v1/tags
Content-Type: application/json

{
"name": "philosophy",
"color": "#8B5CF6"
}

Update Tag

PATCH /api/v1/tags/:id
Content-Type: application/json

{
"name": "new-name",
"color": "#10B981"
}

Delete Tag

DELETE /api/v1/tags/:id

Add Tag to Book

POST /api/v1/tags/:tagId/books/:bookId

Remove Tag from Book

DELETE /api/v1/tags/:tagId/books/:bookId

Get Books by Tag

GET /api/v1/tags/:tagId/books

Add Tag to Highlight

POST /api/v1/tags/:tagId/highlights/:highlightId

Remove Tag from Highlight

DELETE /api/v1/tags/:tagId/highlights/:highlightId

Get Highlights by Tag

GET /api/v1/tags/:tagId/highlights

Decks & Flashcards

Flashcard decks for active recall studying with FSRS scheduling.

List Decks

GET /api/v1/decks

Response:

{
"decks": [
{
"id": "deck-uuid",
"title": "Physics 101",
"description": "Exam review deck",
"type": "manual",
"tagLogic": null,
"settings": null,
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-15T10:00:00Z",
"cardCount": 42,
"dueCount": 5,
"tags": []
}
]
}

Create Deck

POST /api/v1/decks
Content-Type: application/json

{
"title": "History Review",
"description": "Ancient civilizations",
"type": "manual"
}

For smart decks (tag-based):

{
"title": "Physics Smart Deck",
"type": "smart",
"tagLogic": "AND",
"tagIds": ["tag-uuid-1", "tag-uuid-2"],
"settings": {
"includeRawHighlights": true
}
}

Get Deck

GET /api/v1/decks/:id

Update Deck

PATCH /api/v1/decks/:id
Content-Type: application/json

{
"title": "Updated Title",
"settings": { "includeRawHighlights": true }
}

Delete Deck

DELETE /api/v1/decks/:id

[!WARNING] Deleting a manual deck also deletes all flashcards in it.

List Cards in Deck

GET /api/v1/decks/:id/cards?limit=50&offset=0

Create Flashcard

POST /api/v1/decks/:id/cards
Content-Type: application/json

{
"front": "What is Newton's First Law?",
"back": "An object in motion stays in motion...",
"highlightId": "highlight-uuid",
"cardType": "basic"
}

[!NOTE] Cards can only be added to manual decks. Smart decks auto-collect cards from tagged highlights.

Update Flashcard

PATCH /api/v1/decks/cards/:cardId
Content-Type: application/json

{
"front": "Updated question",
"back": "Updated answer"
}

Delete Flashcard

DELETE /api/v1/decks/cards/:cardId

Study Sessions

Get Study Cards

Get a batch of due cards for a deck (includes virtual cards for smart decks).

GET /api/v1/decks/:id/study?limit=20

Response:

{
"cards": [
{
"id": "card-uuid",
"front": "What is...?",
"back": "The answer is...",
"cardType": "basic",
"isVirtual": false,
"highlightId": "highlight-uuid",
"deckId": "deck-uuid"
},
{
"id": "virtual:highlight-uuid",
"front": "Raw highlight preview...",
"back": "Full highlight content",
"cardType": "basic",
"isVirtual": true,
"highlightId": "highlight-uuid",
"deckId": null
}
],
"totalDue": 15
}

Submit Review

POST /api/v1/decks/cards/:cardId/review
Content-Type: application/json

{
"rating": 3
}

Rating values: 1 (Again), 2 (Hard), 3 (Good), 4 (Easy)

Response:

{
"nextDue": "2024-01-17T08:00:00Z",
"fsrsData": {
"stability": 2.4,
"difficulty": 5.0,
"due": "2024-01-17T08:00:00Z",
"state": 2,
"lastReview": "2024-01-15T10:00:00Z"
}
}

Review Virtual Card

When reviewing a raw highlight for the first time, a real flashcard is created.

POST /api/v1/decks/virtual-cards/:highlightId/review
Content-Type: application/json

{
"rating": 3,
"deckId": "deck-uuid",
"front": "Custom question (optional)",
"back": "Custom answer (optional)"
}

Response (201):

{
"card": { ... },
"nextDue": "2024-01-17T08:00:00Z"
}

Review (FSRS)

Spaced repetition review using the FSRS algorithm.

Get Due Cards

GET /api/v1/review

Response:

{
"cards": [
{
"id": "card-uuid",
"highlightId": "highlight-uuid",
"highlight": { ... },
"book": { "id": "...", "title": "...", "author": "..." },
"dueAt": "2024-01-15T08:00:00Z",
"stability": 1.5,
"difficulty": 5.0,
"reps": 3
}
]
}

Submit Highlight Review

POST /api/v1/review/:highlightId
Content-Type: application/json

{
"rating": "good" // "again" | "hard" | "good" | "easy"
}

Get Review Stats

GET /api/v1/review/stats

Response:

{
"totalCards": 150,
"dueToday": 12,
"reviewedToday": 8,
"streak": 5
}

Import

Upload File

Import highlights from supported formats.

POST /api/v1/import
Content-Type: multipart/form-data

source: "kindle" | "koreader" | "readwise" | "lektr"
file: <file>

Response:

{
"source": "kindle",
"booksImported": 3,
"highlightsImported": 45,
"highlightsSkipped": 2
}

Manual Entry

Add a single highlight manually.

POST /api/v1/import/manual
Content-Type: application/json

{
"title": "Book Title",
"author": "Author Name",
"content": "Highlight text",
"note": "Optional note"
}

Export

List Export Providers

GET /api/v1/export/providers

Response:

{
"providers": [
{
"id": "markdown",
"name": "Markdown",
"description": "Export as Markdown files"
}
]
}

Export Highlights

POST /api/v1/export/:providerId
Content-Type: application/json

{
"bookIds": ["uuid1", "uuid2"],
"format": "single-file"
}

Digest Preferences

Manage per-user daily digest email settings.

Get Digest Preferences

GET /api/v1/digest

Response:

{
"digestEnabled": true,
"digestFrequency": "daily",
"digestHour": 8,
"digestTimezone": "America/New_York"
}
FieldTypeDescription
digestEnabledbooleanWhether digest emails are active
digestFrequencystring"daily", "weekdays", or "weekly"
digestHournumberDelivery hour in user's timezone (0–23)
digestTimezonestringIANA timezone (e.g. America/New_York)

Update Digest Preferences

All fields are optional — only send what you want to change.

PATCH /api/v1/digest
Content-Type: application/json

{
"digestFrequency": "weekdays",
"digestHour": 9,
"digestTimezone": "Europe/London"
}

Response: Returns the full updated preferences object.

Note: The timezone is validated server-side using IANA timezone names. Invalid timezones return a 400 error.


Rediscovery

Passive resurfacing of random highlights from the user's library.

Get Random Highlights

GET /api/v1/rediscovery?count=5
ParameterTypeDefaultDescription
countnumber5Number of highlights to return (1–20)

Response:

{
"highlights": [
{
"id": "highlight-uuid",
"content": "The highlight text...",
"note": "User's note or null",
"chapter": "Chapter 1",
"page": 42,
"highlightedAt": "2024-06-01T00:00:00Z",
"bookId": "book-uuid",
"bookTitle": "Atomic Habits",
"bookAuthor": "James Clear",
"coverImageUrl": "/api/v1/covers/...",
"tags": [{ "id": "tag-uuid", "name": "habits", "color": "#8B5CF6" }]
}
]
}

[!NOTE] Returns an empty highlights array (not a 404) if the user has no highlights. The count parameter is clamped to the 1–20 range; values outside this range are adjusted automatically.


Settings

User preferences and settings.

Get Settings

GET /api/v1/settings

Response:

{
"theme": "dark",
"highlightReduction": true
}

Update Settings

PATCH /api/v1/settings
Content-Type: application/json

{
"theme": "light"
}

Digest settings have moved to GET/PATCH /api/v1/digest.

Check Reduction Status

Check if highlight reduction processing is needed.

GET /api/v1/settings/check-reduction

Covers

Get Cover Image

GET /api/v1/covers/:filename

Returns the cover image file.


Admin

Admin-only endpoints (require admin role).

Get Email Settings

GET /api/v1/admin/email-settings

Update Email Settings

PUT /api/v1/admin/email-settings
Content-Type: application/json

{
"smtp_host": "smtp.gmail.com",
"smtp_port": "587",
"smtp_user": "user@gmail.com",
"smtp_pass": "app-password",
"mail_from_name": "Lektr",
"mail_from_email": "noreply@example.com"
}

Test Email

POST /api/v1/admin/email-settings/test
Content-Type: application/json

{
"email": "test@example.com"
}

Job Queue Status

GET /api/v1/admin/job-queue/status

Response:

{
"pending": 5,
"processing": 1,
"failed": 0,
"completed": 150
}

Refresh All Metadata

Trigger metadata refresh for all books from external sources.

POST /api/v1/admin/refresh-metadata

Refresh Book Metadata

Refresh metadata for a specific book.

POST /api/v1/admin/refresh-metadata/:bookId

Trigger Digest Emails

Manually trigger daily digest emails for all users.

POST /api/v1/admin/trigger-digest

Response:

{
"success": true,
"message": "Digest emails are being generated. Check server logs for progress."
}