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) - 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
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": "0.1.0.0"
},
"audiobooks": {
"base_url": "https://readarr-audio.example.com",
"api_key": "***redacted***",
"insecure_skip_verify": false,
"connected": true,
"version": "0.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"
}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 URLra_ebooks_base- Readarr ebooks base URLra_ebooks_key- Readarr ebooks API keyra_ebooks_insecure- Skip TLS verification for ebooks ("on"/"off")ra_audiobooks_base- Readarr audiobooks base URLra_audiobooks_key- Readarr audiobooks API keyra_audiobooks_insecure- Skip TLS verification for audiobooks ("on"/"off")- 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
Currently, no rate limiting is implemented. Consider implementing reverse proxy rate limiting in production.
CORS is not explicitly configured. Cross-origin requests may be blocked by browsers.
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"
}
}