This document describes the Scriptorum REST API endpoints and their usage. Scriptorum provides a comprehensive API for managing book requests, user administration, system configuration, and integration with external services like Readarr.
The Scriptorum API is organized into several categories:
- Request Management (
/api/v1/requests/*): Create, approve, decline, and manage book requests - Book Details (
/api/v1/book/*): Retrieve normalized book metadata from various sources - Search (
/api/providers/search): Search for books across multiple providers - Readarr Integration (
/api/readarr/*): Access Readarr quality profiles and root folders - Notifications (
/api/notifications/*): Test notification delivery (ntfy, SMTP, Discord, generic webhook) - User Management (
/users/*): Admin endpoints for user administration - Settings (
/settings/*): System configuration management - System (
/healthz,/version): Health checks and version information - UI (
/ui/*): HTMX-powered dynamic UI fragments - Approval Tokens (
/approve/*): One-click approval from notification links
Scriptorum supports two authentication methods:
- Login via
/loginendpoint to receive session cookie - Cookie is automatically included in subsequent requests
- Used by the web interface
- Redirect to
/oauth/loginto initiate OAuth flow - Callback handled at
/oauth/callback - Session cookie set after successful authentication
All API endpoints are relative to your Scriptorum base URL:
https://your-scriptorum-instance.com/
Note: Some endpoints use different base paths:
- REST API endpoints:
/api/v1/ - UI fragments:
/ui/ - Admin endpoints:
/settings/,/users/,/notifications/ - System endpoints:
/healthz,/version
Scriptorum implements role-based access control:
GET /healthz- No authentication requiredGET /version- No authentication requiredGET /approve/{token}- Uses secure token instead of authentication
GET /api/v1/requests- List user's own requestsPOST /api/v1/requests- Create new requestsPOST /api/v1/book/*- Access book detailsGET /api/providers/search- Search for booksGET /ui/*- UI fragments and pages
POST /api/v1/requests/{id}/approve- Approve requestsPOST /api/v1/requests/{id}/decline- Decline requestsDELETE /api/v1/requests/{id}- Delete requestsPOST /api/v1/requests/{id}/hydrate- Hydrate requestsPOST /api/v1/requests/approve-all- Bulk approveDELETE /api/v1/requests- Delete all requestsGET /api/readarr/debug- Debug Readarr configPOST /api/notifications/test-*- Test notificationsGET /settings- Settings pagePOST /settings/save- Save settingsGET /users- User management pagePOST /users- Create usersPOST /users/edit- Edit usersGET /users/delete- Delete usersGET /notifications- Notification settings pagePOST /notifications/save- Save notification settings
All endpoints return standard HTTP status codes:
200- Success400- Bad Request401- Unauthorized403- Forbidden404- Not Found500- Internal Server Error
Error responses include a message:
{
"error": "Description of the error"
}Local username/password authentication.
Request Body:
{
"username": "string",
"password": "string"
}Response:
302- Redirect to dashboard on success401- Invalid credentials
Initiates OAuth authentication flow.
Response:
302- Redirect to OAuth provider
OAuth callback endpoint (handled automatically).
Query Parameters:
code- Authorization code from OAuth providerstate- State parameter for CSRF protection
Logs out the current user.
Response:
302- Redirect to login page
List requests based on user permissions.
Query Parameters:
limit- Maximum number of results (default: 200)
Response:
[
{
"id": 1,
"title": "Book Title",
"authors": ["Author Name"],
"requester_email": "user@example.com",
"status": "pending",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z",
"kind": "ebook",
"readarr_req": {
"title": "Book Title",
"author": "Author Name",
"isbn": "1234567890",
"asin": "B123456789"
}
}
]Permissions:
- Regular users: Only see their own requests
- Admins: See all requests
Create a new request.
Request Body:
{
"title": "Book Title",
"authors": ["Author Name"],
"kind": "ebook",
"selection": {
"title": "Book Title",
"author": "Author Name",
"isbn": "1234567890",
"asin": "B123456789",
"cover_url": "https://example.com/cover.jpg",
"description": "Book description"
}
}Response:
{
"id": 123,
"status": "created"
}Approve a pending request (admin only).
Path Parameters:
id- Request ID
Response:
{
"status": "queued"
}Notes:
- Sends request to appropriate Readarr instance
- Requires request to have valid selection payload
- Only pending requests can be approved
Decline a pending request (admin only).
Path Parameters:
id- Request ID
Response:
{
"status": "declined"
}Delete a request (admin only).
Path Parameters:
id- Request ID
Response:
{
"status": "deleted"
}Attempt to attach selection payload to a request (admin only).
Path Parameters:
id- Request ID
Response:
{
"status": "hydrated"
}Notes:
- Useful for older requests created before selection payloads
- Queries Readarr for book metadata based on stored identifiers
- May not always find a match
Approve all pending requests (admin only).
Response:
{
"status": "approved 5 requests"
}Notes:
- Only approves requests with valid selection payloads
- Useful for bulk processing
Delete all requests (admin only).
Response:
{
"status": "all requests deleted"
}Get normalized book details from various sources.
Request Body (JSON or Form Data):
{
"provider_payload": "provider-specific data",
"provider_payload_ebook": "ebook provider data",
"provider_payload_audiobook": "audiobook provider data",
"isbn13": "9781234567890",
"isbn10": "1234567890",
"asin": "B123456789",
"title": "Book Title",
"authors": ["Author Name"]
}Response:
{
"title": "Book Title",
"authors": ["Author Name"],
"isbn10": "1234567890",
"isbn13": "9781234567890",
"asin": "B123456789",
"cover": "https://example.com/cover.jpg",
"description": "Book description",
"provider_payload": "normalized provider data"
}Notes:
- Accepts multiple input formats (JSON or form data)
- Normalizes author data to string arrays
- Returns 404 if no details found
Get enriched book details with additional metadata.
Request Body (JSON or Form Data):
{
"provider_payload": "provider-specific data",
"isbn13": "9781234567890",
"title": "Book Title",
"authors": ["Author Name"]
}Response:
{
"title": "Book Title",
"authors": ["Author Name"],
"isbn10": "1234567890",
"isbn13": "9781234567890",
"asin": "B123456789",
"cover_url": "https://example.com/cover.jpg",
"description": "Book description",
"publication_date": "2025-01-01",
"page_count": 300,
"language": "en",
"genres": ["Fiction"],
"series": "Series Name",
"series_index": 1
}Notes:
- Provides richer metadata than basic details endpoint
- Includes publication info, genres, and series data
Search for books across all enabled providers.
Query Parameters:
q- Search query (required)kind- Media type:ebooks,audiobooks, orboth(default:both)
Response:
{
"results": [
{
"title": "Book Title",
"author": "Author Name",
"isbn": "1234567890",
"asin": "B123456789",
"cover_url": "https://example.com/cover.jpg",
"description": "Book description",
"publication_date": "2025-01-01",
"page_count": 300,
"language": "en",
"provider": "amazon",
"url": "https://amazon.com/dp/B123456789"
}
],
"provider_results": {
"amazon": 10,
"openlibrary": 5
}
}Notes:
- Results are automatically deduplicated
- Amazon results are prioritized for metadata quality
- Cover images are proxied through Scriptorum
Validate the shared Chaptarr connection and return the format-typed profiles and root folders used by Settings. Admin authentication is required.
For POST, form fields use_overrides=true, base_url, api_key, and
insecure test unsaved values without persisting them. A blank api_key
retains the saved key when the base URL is unchanged.
{
"version": "0.9.911.0",
"qualityProfiles": [
{"id": 1, "name": "E-Book", "profileType": "ebook"},
{"id": 2, "name": "Audiobook", "profileType": "audiobook"}
],
"metadataProfiles": [
{"id": 1, "name": "Audiobook Default", "profileType": 1},
{"id": 2, "name": "Ebook Default", "profileType": 2}
],
"rootFolders": [
{"id": 1, "name": "Books", "path": "/books", "accessible": true}
]
}The legacy /api/readarr/* endpoints below remain available when Chaptarr is
not configured.
Refresh Scriptorum's local Chaptarr catalog and reconcile request state. The
optional kind query value is ebooks, audiobooks, or all.
Get Readarr quality profiles.
Query Parameters:
kind-ebooksoraudiobooks(required)
Response:
[
{
"id": 1,
"name": "Standard",
"cutoff": {
"id": 1,
"name": "PDF"
}
}
]Get Readarr root folders.
Query Parameters:
kind-ebooksoraudiobooks(required)
Response:
[
{
"id": 1,
"path": "/books/ebooks",
"freespace": 1000000000,
"totalspace": 2000000000
}
]Get Readarr configuration debug information (admin only).
Query Parameters:
kind-ebooksoraudiobooks(optional, shows both if not specified)
Response:
{
"ebooks": {
"base_url": "https://readarr.example.com",
"api_key": "***redacted***",
"insecure_skip_verify": false,
"connected": true,
"version": "1.0.0"
},
"audiobooks": {
"base_url": "https://readarr-audio.example.com",
"api_key": "***redacted***",
"insecure_skip_verify": false,
"connected": true,
"version": "1.0.0"
}
}Notes:
- API keys are redacted for security
- Useful for troubleshooting Readarr connectivity issues
Returns the user management page (HTML).
Authentication: Admin required
Create a new user.
Request Body (Form Data):
username- Username (required)password- Password (required)is_admin- Set to "on" for admin privileges
Response:
302- Redirect to users page
Edit an existing user.
Request Body (Form Data):
user_id- User ID to edit (required)password- New password (optional)confirm_password- Password confirmation (required if password provided)is_admin- Set to "on" for admin privileges
Response:
302- Redirect to users page
Delete a user.
Query Parameters:
id- User ID to delete
Response:
302- Redirect to users page
Test ntfy.sh notification delivery.
Request Body:
{
"server": "https://ntfy.sh",
"topic": "test-topic",
"username": "optional-username",
"password": "optional-password"
}Response (Success):
{
"success": true,
"message": "Test notification sent successfully"
}Response (Error):
{
"success": false,
"error": "Failed to send notification: connection timeout"
}Test SMTP email delivery.
Request Body:
{
"host": "smtp.gmail.com",
"port": 587,
"username": "your-email@gmail.com",
"password": "your-app-password",
"from_email": "scriptorum@example.com",
"from_name": "Scriptorum",
"to_email": "admin@example.com",
"enable_tls": true
}Response (Success):
{
"success": true,
"message": "Test email sent successfully"
}Response (Error):
{
"success": false,
"error": "SMTP authentication failed"
}Test Discord webhook delivery.
Request Body:
{
"webhook_url": "https://discord.com/api/webhooks/...",
"username": "Scriptorum Bot"
}Response (Success):
{
"success": true,
"message": "Test message sent successfully"
}Response (Error):
{
"success": false,
"error": "Invalid webhook URL"
}Test generic webhook delivery. Unlike /test-discord, this posts a plain JSON event payload (no chat-app formatting) to any HTTP endpoint — useful for piping events into something not natively supported (n8n, Home Assistant, a custom relay, etc.).
Request Body:
{
"url": "https://example.com/hooks/scriptorum"
}Response (Success):
{
"success": true
}Response (Error):
{
"success": false,
"error": "webhook URL is required"
}Live events (request.created, request.approved, system.alert) are posted as JSON objects with an event field identifying the type, plus the relevant request/title/author/timestamp fields.
Health check endpoint.
Response:
ok
Notes:
- Always returns 200 OK if service is running
- Useful for monitoring and load balancer health checks
Get application version information.
Response:
{
"version": "1.0.0",
"commit": "abc123",
"build_time": "2025-01-01T00:00:00Z"
}Returns the settings management page (HTML).
Authentication: Admin required
Save application settings.
Request Body (Form Data):
debug- Enable debug mode ("on"/"off")server_url- Base server URLchaptarr_base- Shared Chaptarr base URLchaptarr_key- Chaptarr API key (blank preserves the saved key)chaptarr_insecure- Skip TLS verification ("on"/"off")chaptarr_ebook_qp/chaptarr_audiobook_qp- Format-compatible quality profile IDschaptarr_ebook_mp/chaptarr_audiobook_mp- Format-compatible metadata profile IDschaptarr_ebook_root/chaptarr_audiobook_root- Format-specific root paths- Legacy
ra_*form fields are still accepted for backwards compatibility - And many other configuration options...
Response:
302- Redirect to settings page
Get requests table HTML fragment for HTMX updates.
Authentication: Required
Response: HTML fragment
Returns the search interface page (HTML).
Authentication: Required
Query Parameters:
q- Search querypage- Page number (default: 1)limit- Results per page (default: 20, max: 50)
Proxy Readarr cover images.
Query Parameters:
url- Cover image URL to proxy
Response: Image data
One-click request approval from notification links.
Path Parameters:
token- Secure approval token
Response:
302- Redirect to dashboard with success/error message
Notes:
- Tokens expire after 1 hour
- Can be used without authentication
- Useful for email/discord notification approvals
The web interface uses HTMX for dynamic updates. Many endpoints return HTML fragments instead of JSON when called with HTMX headers:
HX-Request: true- Indicates HTMX requestHX-Target: #element-id- Target element for responseHX-Swap: innerHTML- How to swap content
Some endpoints emit HTMX events:
request:updated- When a request is modifieduser:created- When a user is createdsystem:error- When an error occurs
Per-IP, per-path rate limiting is enforced in-process (see rateLimiting middleware in internal/httpapi/security.go), applied globally to every request:
- Auth endpoints (
/login,/oauth/*): 10 requests / 15 minutes. - API endpoints (
/api/*): 100 requests / 5 minutes. - Everything else: 200 requests / 5 minutes.
Requests over the limit receive 429 Too Many Requests. Limits are tracked in memory per server process, so they reset on restart and don't share state across multiple replicas — fine for the typical single-instance self-hosted deployment this app targets.
No Access-Control-* headers are sent, which is intentional: this is a same-origin, server-rendered HTMX app, not a JSON API meant to be called from other origins in a browser. Browsers therefore block cross-origin fetch/XHR access by default. Server-to-server API calls (e.g. via curl or a backend script) are unaffected, since CORS is a browser-enforced policy only.
Scriptorum does not currently support WebSocket connections. All updates are handled via HTMX polling or user-initiated requests.
curl -X POST http://localhost:8080/api/v1/requests \
-H "Content-Type: application/json" \
-b "scriptorum_session=your-session-cookie" \
-d '{
"title": "The Great Gatsby",
"authors": ["F. Scott Fitzgerald"],
"kind": "ebook",
"selection": {
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"isbn": "9780743273565",
"asin": "B004EHZDE8"
}
}'curl "http://localhost:8080/api/providers/search?q=great+gatsby&kind=ebooks" \
-b "scriptorum_session=your-session-cookie"curl -X POST http://localhost:8080/api/v1/requests/123/approve \
-H "Content-Type: application/json" \
-b "scriptorum_session=your-session-cookie"curl -X POST http://localhost:8080/api/v1/book/details \
-H "Content-Type: application/json" \
-b "scriptorum_session=your-session-cookie" \
-d '{
"isbn13": "9780743273565",
"title": "The Great Gatsby",
"authors": ["F. Scott Fitzgerald"]
}'curl -X POST http://localhost:8080/api/notifications/test-ntfy \
-H "Content-Type: application/json" \
-b "scriptorum_session=your-session-cookie" \
-d '{
"server": "https://ntfy.sh",
"topic": "scriptorum-test",
"username": "",
"password": ""
}'curl http://localhost:8080/healthz
# Returns: okcurl http://localhost:8080/version
# Returns: {"version":"1.0.0","commit":"abc123","build_time":"2025-01-01T00:00:00Z"}// Search for books
async function searchBooks(query) {
const response = await fetch(`/api/providers/search?q=${encodeURIComponent(query)}`);
return await response.json();
}
// Create request
async function createRequest(requestData) {
const response = await fetch('/api/v1/requests', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
});
return await response.json();
}
// Get user requests
async function getUserRequests() {
const response = await fetch('/api/v1/requests');
return await response.json();
}<!-- Auto-updating request table -->
<div id="request-table"
hx-get="/ui/requests/table"
hx-trigger="every 30s">
<!-- Table content loaded here -->
</div>
<!-- Request approval button -->
<button hx-post="/api/v1/requests/123/approve"
hx-target="closest tr"
hx-swap="outerHTML">
Approve
</button>async function handleApiCall(apiFunction) {
try {
const result = await apiFunction();
return result;
} catch (error) {
if (error.status === 401) {
// Redirect to login
window.location.href = '/login';
} else if (error.status === 403) {
// Show access denied message
alert('Access denied');
} else {
// Handle other errors
console.error('API Error:', error);
}
}
}Most endpoints follow this error format:
{
"error": "Detailed error message",
"code": "ERROR_CODE",
"details": {
"field": "Additional context"
}
}