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"
}
]
}
Search
Hybrid Search
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"
}
| Field | Type | Description |
|---|---|---|
digestEnabled | boolean | Whether digest emails are active |
digestFrequency | string | "daily", "weekdays", or "weekly" |
digestHour | number | Delivery hour in user's timezone (0–23) |
digestTimezone | string | IANA 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
400error.
Rediscovery
Passive resurfacing of random highlights from the user's library.
Get Random Highlights
GET /api/v1/rediscovery?count=5
| Parameter | Type | Default | Description |
|---|---|---|---|
count | number | 5 | Number 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
highlightsarray (not a 404) if the user has no highlights. Thecountparameter 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."
}