API Reference¶
Readur provides a comprehensive REST API for integrating with external systems and building custom workflows.
Table of Contents¶
- Base URL
- Authentication
- Error Handling
- Rate Limiting
- Pagination
- Endpoints
- Authentication
- Documents
- Search
- OCR Queue
- Settings
- Sources
- Labels
- Users
- Shared Links
- API Keys
- Comments
- Notifications
- Metrics
- WebSocket API
- Examples
Base URL¶
For production deployments, replace with your configured domain and ensure HTTPS is used.
Authentication¶
Readur supports multiple authentication methods:
JWT Authentication¶
Include the token in the Authorization header:
Obtaining a Token¶
POST /api/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "your_password"
}
Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "admin",
"email": "[email protected]",
"role": "admin"
},
"expires_at": "2025-01-16T12:00:00Z"
}
Refresh Token¶
OIDC Authentication¶
For OIDC/SSO authentication:
This will redirect to your configured OIDC provider. After successful authentication, the callback URL will receive the token.
API Key Authentication¶
For scripts, CLIs, and long-running integrations that need to authenticate without an interactive login, Readur supports personal API keys. A key is sent in the same header as a JWT:
Keys are prefixed with readur_pat_ so they're easy to recognize in code and are pickable by secret-scanning tools. Each key carries the full permissions of the user who created it — treat them like passwords.
Key properties:
- Generated with 256 bits of OS randomness
- Stored server-side only as a SHA-256 hash; the plaintext is shown exactly once at creation time and cannot be retrieved later
- Each request re-reads the owning user's role, so promoting or demoting the owner takes effect immediately on the next request
- Optional expiration of up to 365 days (90 days is the UI default)
- Revocable at any time; revocation is effective on the next request
Create and use a key:
# 1. Create a key (authenticated with your JWT)
curl -X POST https://readur.example.com/api/auth/keys \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"name": "backup-script", "expires_in_days": 90}'
# → { "api_key": { ... }, "plaintext": "readur_pat_XXXXXX..." }
# Save the plaintext immediately. It will not be shown again.
# 2. Use it to call any authenticated endpoint
curl https://readur.example.com/api/documents \
-H "Authorization: Bearer readur_pat_XXXXXX..."
Limits:
- Maximum 20 active (non-revoked) keys per user
- Creation is rate-limited to 10 keys/hour per user
- Maximum expiration: 365 days
See the Security Guide for recommended handling practices.
Error Handling¶
All API errors follow a consistent format:
{
"error": {
"code": "DOCUMENT_NOT_FOUND",
"message": "Document with ID 'abc123' not found",
"details": {
"document_id": "abc123",
"user_id": "user456"
},
"timestamp": "2025-01-15T10:30:45Z",
"request_id": "req_xyz789"
}
}
Error Codes¶
| Code | HTTP Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Invalid or missing authentication |
FORBIDDEN | 403 | Insufficient permissions |
NOT_FOUND | 404 | Resource not found |
VALIDATION_ERROR | 400 | Invalid request parameters |
DUPLICATE_RESOURCE | 409 | Resource already exists |
RATE_LIMITED | 429 | Too many requests |
INTERNAL_ERROR | 500 | Server error |
SERVICE_UNAVAILABLE | 503 | Service temporarily unavailable |
Rate Limiting¶
Rate limits are applied to specific endpoint categories:
- Public shared link password verification: 10 requests/minute per IP
- Public shared link access (download/view): 60 requests/minute per IP
- Comment creation: 10 comments/minute per user
- Shared link creation: 20 links/hour per user
- API key creation: 10 keys/hour per user
When rate limited, the API returns HTTP 429 with a JSON body containing retry_after_secs indicating how long to wait before retrying.
Pagination¶
List endpoints support pagination using query parameters:
Pagination Parameters: - page: Page number (default: 1) - per_page: Items per page (default: 20, max: 100) - sort: Sort field - order: Sort order (asc or desc)
Response includes pagination metadata:
{
"data": [...],
"pagination": {
"page": 1,
"per_page": 20,
"total": 150,
"total_pages": 8,
"has_next": true,
"has_prev": false
}
}
Endpoints¶
Authentication Endpoints¶
Login¶
Request Body:
Response: 200 OK
{
"token": "string",
"user": {
"id": "uuid",
"username": "string",
"email": "string",
"role": "admin|editor|viewer"
}
}
Register¶
Request Body:
Response: 201 Created
Logout¶
Response: 200 OK
Document Endpoints¶
List Documents¶
Query Parameters: - page: Page number - per_page: Items per page - status: Filter by status (pending, processing, completed, failed) - source_id: Filter by source - label_ids: Comma-separated label IDs - date_from: Start date (ISO 8601) - date_to: End date (ISO 8601)
Response: 200 OK
{
"data": [
{
"id": "uuid",
"title": "Document Title",
"filename": "document.pdf",
"content": "Extracted text content...",
"status": "completed",
"mime_type": "application/pdf",
"size": 1048576,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:05:00Z",
"ocr_confidence": 0.95,
"labels": ["label1", "label2"],
"metadata": {
"author": "John Doe",
"pages": 10
}
}
],
"pagination": {...}
}
Get Document¶
Response: 200 OK
Upload Document¶
Request Body: - file: File to upload (required) - title: Document title (optional) - labels: Comma-separated label IDs (optional) - ocr_enabled: Enable OCR (default: true) - language: OCR language code (default: "eng")
Response: 201 Created
Bulk Upload¶
Request Body: - files: Multiple files - default_labels: Labels to apply to all files
Response: 201 Created
Update Document¶
Request Body:
{
"title": "Updated Title",
"labels": ["label1", "label3"],
"metadata": {
"custom_field": "value"
}
}
Response: 200 OK
Delete Document¶
Response: 204 No Content
Download Document¶
Response: 200 OK with file attachment
Get Document Thumbnail¶
Query Parameters: - width: Thumbnail width (default: 200) - height: Thumbnail height (default: 200)
Response: 200 OK with image
Retry OCR¶
Request Body:
Response: 200 OK
Search Endpoints¶
Search Documents¶
Query Parameters: - q: Search query (required) - page: Page number - per_page: Items per page - filters: JSON-encoded filters - highlight: Enable highlighting (default: true) - fuzzy: Enable fuzzy search (default: false)
Response: 200 OK
{
"results": [
{
"document_id": "uuid",
"title": "Document Title",
"score": 0.95,
"highlights": [
{
"field": "content",
"snippet": "...matched <mark>text</mark> here..."
}
]
}
],
"total": 42,
"facets": {
"mime_types": {
"application/pdf": 30,
"text/plain": 12
},
"labels": {
"important": 15,
"archive": 27
}
}
}
Advanced Search¶
Request Body:
{
"query": {
"must": [
{"field": "content", "value": "invoice", "type": "match"}
],
"should": [
{"field": "title", "value": "2024", "type": "contains"}
],
"must_not": [
{"field": "labels", "value": "draft", "type": "exact"}
]
},
"filters": {
"date_range": {
"from": "2024-01-01",
"to": "2024-12-31"
},
"mime_types": ["application/pdf"],
"min_confidence": 0.8
},
"sort": [
{"field": "created_at", "order": "desc"}
],
"page": 1,
"per_page": 20
}
OCR Queue Endpoints¶
Get Queue Status¶
Response: 200 OK
{
"pending": 15,
"processing": 3,
"completed_today": 142,
"failed": 2,
"average_processing_time": 5.2,
"estimated_completion": "2025-01-15T11:30:00Z"
}
List Queue Items¶
Query Parameters: - status: Filter by status - priority: Filter by priority
Response: 200 OK
Update Queue Priority¶
Request Body:
Cancel OCR Job¶
Settings Endpoints¶
Get User Settings¶
Response: 200 OK
{
"theme": "dark",
"language": "en",
"notifications_enabled": true,
"ocr_default_language": "eng",
"items_per_page": 20
}
Update Settings¶
Request Body:
Sources Endpoints¶
List Sources¶
Response: 200 OK
{
"sources": [
{
"id": "uuid",
"name": "Shared Documents",
"type": "webdav",
"url": "https://nextcloud.example.com/remote.php/dav/files/user/",
"status": "active",
"last_sync": "2025-01-15T10:00:00Z",
"next_sync": "2025-01-15T11:00:00Z",
"document_count": 150
}
]
}
Create Source¶
Request Body:
{
"name": "Company Drive",
"type": "webdav",
"url": "https://drive.company.com/dav/",
"username": "user",
"password": "encrypted_password",
"sync_interval": 3600,
"recursive": true,
"file_patterns": ["*.pdf", "*.docx"]
}
Update Source¶
Delete Source¶
Trigger Source Sync¶
Response: 202 Accepted
Get Sync Status¶
Labels Endpoints¶
List Labels¶
Response: 200 OK
{
"labels": [
{
"id": "uuid",
"name": "Important",
"color": "#FF5733",
"description": "High priority documents",
"document_count": 42,
"created_by": "admin",
"created_at": "2025-01-01T00:00:00Z"
}
]
}
Create Label¶
Request Body:
Update Label¶
Delete Label¶
Assign Label to Documents¶
Request Body:
User Endpoints¶
List Users (Admin only)¶
Get User Profile¶
Update Profile¶
Request Body:
{
"email": "[email protected]",
"display_name": "John Doe"
}
Change Password¶
Request Body:
Notification Endpoints¶
List Notifications¶
Query Parameters: - unread_only: Show only unread notifications
Response: 200 OK
{
"notifications": [
{
"id": "uuid",
"type": "ocr_completed",
"title": "OCR Processing Complete",
"message": "Document 'Invoice.pdf' has been processed",
"read": false,
"created_at": "2025-01-15T10:00:00Z",
"data": {
"document_id": "doc123"
}
}
]
}
Mark as Read¶
Mark All as Read¶
Metrics Endpoints¶
System Metrics¶
Response: 200 OK
{
"cpu_usage": 45.2,
"memory_usage": 67.8,
"disk_usage": 34.5,
"active_connections": 23,
"uptime_seconds": 864000
}
OCR Analytics¶
Response: 200 OK
{
"total_processed": 5432,
"success_rate": 0.98,
"average_processing_time": 4.5,
"languages_used": {
"eng": 4500,
"spa": 700,
"fra": 232
},
"daily_stats": [...]
}
Shared Link Endpoints¶
Authenticated Endpoints (require Authorization: Bearer <token>)¶
Create Shared Link¶
Request body:
{
"document_id": "uuid",
"password": "optional-password",
"expires_at": "2025-12-31T23:59:59Z",
"max_views": 10
}
All fields except document_id are optional. Returns the created shared link with its public URL.
List Shared Links¶
Returns all shared links for the current user. Admins see all shared links across all users.
List Shared Links for Document¶
Returns all shared links for a specific document (must have access to the document).
Revoke Shared Link¶
Revokes a shared link. Users can revoke their own links; admins can revoke any link. Returns 204 No Content on success.
Public Endpoints (no authentication required)¶
Get Shared Document Metadata¶
Returns document metadata (filename, size, type) and whether a password is required. Does not increment view count.
Verify Password¶
Request body:
Returns { "valid": true } if the password is correct. Rate limited to 10 attempts/minute per IP.
Download Document¶
Request body:
Returns the document file as a binary download. Increments view count. Rate limited to 60 requests/minute per IP.
View Document¶
Same as download but with Content-Disposition: inline for in-browser viewing.
Error Codes¶
| Code | HTTP Status | Description |
|---|---|---|
SHARED_LINK_NOT_FOUND | 404 | Token does not match any link |
SHARED_LINK_EXPIRED | 410 | Link has passed its expiration date |
SHARED_LINK_REVOKED | 410 | Link was manually revoked |
SHARED_LINK_MAX_VIEWS | 410 | Link has reached its view limit |
SHARED_LINK_PASSWORD_REQUIRED | 401 | Password is required but not provided |
SHARED_LINK_INVALID_PASSWORD | 403 | Provided password is incorrect |
SHARED_LINK_RATE_LIMITED | 429 | Too many requests from this IP |
API Key Endpoints¶
Personal API keys let users authenticate scripts and integrations without interactive login. See API Key Authentication for an overview. All endpoints require a standard authenticated session (either a JWT or another valid API key).
Create API Key¶
Request Body:
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | 1–100 characters. A label to identify the key. |
expires_in_days | integer | null | no | 1–365. Omit or set null for no expiration. |
Response: 200 OK
{
"api_key": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "backup-script",
"key_prefix": "readur_pat_X",
"expires_at": "2026-07-20T00:00:00Z",
"last_used_at": null,
"revoked_at": null,
"is_expired": false,
"created_at": "2026-04-21T12:34:56Z"
},
"plaintext": "readur_pat_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
⚠️
plaintextis returned exactly once. The server has no way to recover it — if you lose it, revoke the key and create a new one.
Error Responses: - 400 Bad Request — name missing/too long, or expires_in_days outside 1–365 - 409 Conflict — already at the 20-active-keys-per-user cap - 429 Too Many Requests — created more than 10 keys in the last hour
List API Keys¶
Returns the caller's keys. Admins may pass ?all=true to list every user's keys for incident response.
Response: 200 OK
[
{
"id": "a1b2c3d4-...",
"user_id": "550e8400-...",
"name": "backup-script",
"key_prefix": "readur_pat_X",
"expires_at": "2026-07-20T00:00:00Z",
"last_used_at": "2026-04-21T13:00:00Z",
"revoked_at": null,
"is_expired": false,
"created_at": "2026-04-21T12:34:56Z"
}
]
Neither the plaintext nor the stored hash ever appears in this response.
Revoke API Key¶
Revocation is immediate — the key fails authentication on the next request. Regular users may only revoke their own keys; admins may revoke any key.
Response: 204 No Content
Error Responses: - 404 Not Found — key does not exist, was already revoked, or belongs to another user (non-admins cannot distinguish these cases by design)
Error Codes¶
| Code | HTTP Status | Description |
|---|---|---|
API_KEY_NOT_FOUND | 404 | Key does not exist or caller cannot see it |
API_KEY_INVALID_REQUEST | 400 | Name or expires_in_days is invalid |
API_KEY_MAX_REACHED | 409 | User already has 20 active keys |
API_KEY_PERMISSION_DENIED | 403 | Caller cannot perform this action |
API_KEY_RATE_LIMITED | 429 | Exceeded 10 creations/hour |
Authentication failures for API keys (invalid, expired, revoked, or malformed) return 401 Unauthorized from the auth middleware, intentionally without distinguishing which case occurred.
Comment Endpoints¶
All comment endpoints require authentication (Authorization: Bearer <token>) and verify the user has access to the specified document.
List Comments¶
Returns top-level comments with reply counts and the first 3 replies inline. Maximum limit is 100.
Create Comment¶
Request body:
Content must be 1-10,000 characters. Replies can only be one level deep (you can reply to a comment, but not to a reply). Rate limited to 10 comments/minute per user. Returns 201 Created.
Get Replies¶
Returns replies to a specific comment, ordered by creation date ascending. Maximum limit is 100.
Update Comment¶
Request body:
Only the comment author can edit. Sets is_edited to true.
Delete Comment¶
The comment author or an admin can delete. Returns 204 No Content.
Get Comment Count¶
Returns { "count": 42 }.
Error Codes¶
| Code | HTTP Status | Description |
|---|---|---|
COMMENT_NOT_FOUND | 404 | Comment does not exist |
COMMENT_DOCUMENT_NOT_FOUND | 404 | Document does not exist or user lacks access |
COMMENT_PERMISSION_DENIED | 403 | Cannot edit/delete another user's comment |
COMMENT_CONTENT_EMPTY | 400 | Comment text is empty |
COMMENT_CONTENT_TOO_LONG | 400 | Exceeds 10,000 character limit |
COMMENT_NESTING_TOO_DEEP | 400 | Replying to a reply (only one level allowed) |
COMMENT_RATE_LIMITED | 429 | Too many comments created recently |
WebSocket API¶
Connect to real-time updates:
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onopen = () => {
// Authenticate
ws.send(JSON.stringify({
type: 'auth',
token: 'your_jwt_token'
}));
// Subscribe to events
ws.send(JSON.stringify({
type: 'subscribe',
events: ['ocr_progress', 'sync_progress', 'notifications']
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch(data.type) {
case 'ocr_progress':
console.log(`OCR Progress: ${data.progress}% for document ${data.document_id}`);
break;
case 'sync_progress':
console.log(`Sync Progress: ${data.processed}/${data.total} files`);
break;
case 'notification':
console.log(`New notification: ${data.message}`);
break;
}
};
WebSocket Events¶
| Event Type | Description | Data Structure |
|---|---|---|
ocr_progress | OCR processing updates | {document_id, progress, status} |
sync_progress | Source sync updates | {source_id, processed, total, current_file} |
notification | Real-time notifications | {id, type, title, message} |
document_created | New document added | {document_id, title, source} |
document_updated | Document modified | {document_id, changes} |
queue_status | OCR queue updates | {pending, processing, completed} |
Examples¶
Python Client Example¶
import requests
import json
class ReadurClient:
def __init__(self, base_url, username, password):
self.base_url = base_url
self.token = self._authenticate(username, password)
self.headers = {'Authorization': f'Bearer {self.token}'}
def _authenticate(self, username, password):
response = requests.post(
f'{self.base_url}/auth/login',
json={'username': username, 'password': password}
)
return response.json()['token']
def upload_document(self, file_path):
with open(file_path, 'rb') as f:
files = {'file': f}
response = requests.post(
f'{self.base_url}/documents/upload',
headers=self.headers,
files=files
)
return response.json()
def search(self, query):
response = requests.get(
f'{self.base_url}/search',
headers=self.headers,
params={'q': query}
)
return response.json()
# Usage
client = ReadurClient('http://localhost:8080/api', 'admin', 'password')
result = client.upload_document('/path/to/document.pdf')
print(f"Document uploaded: {result['id']}")
search_results = client.search('invoice 2024')
for result in search_results['results']:
print(f"Found: {result['title']} (score: {result['score']})")
JavaScript/TypeScript Example¶
class ReadurAPI {
private token: string;
private baseURL: string;
constructor(baseURL: string) {
this.baseURL = baseURL;
}
async login(username: string, password: string): Promise<void> {
const response = await fetch(`${this.baseURL}/auth/login`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username, password})
});
const data = await response.json();
this.token = data.token;
}
async uploadDocument(file: File): Promise<any> {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${this.baseURL}/documents/upload`, {
method: 'POST',
headers: {'Authorization': `Bearer ${this.token}`},
body: formData
});
return response.json();
}
async search(query: string): Promise<any> {
const response = await fetch(
`${this.baseURL}/search?q=${encodeURIComponent(query)}`,
{headers: {'Authorization': `Bearer ${this.token}`}}
);
return response.json();
}
}
// Usage
const api = new ReadurAPI('http://localhost:8080/api');
await api.login('admin', 'password');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput.files?.[0]) {
const result = await api.uploadDocument(fileInput.files[0]);
console.log('Uploaded:', result.id);
}
cURL Examples¶
# Login and save token
TOKEN=$(curl -s -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"password"}' \
| jq -r '.token')
# Upload document
curl -X POST http://localhost:8080/api/documents/upload \
-H "Authorization: Bearer $TOKEN" \
-F "[email protected]" \
-F "labels=important,invoice"
# Search documents
curl -X GET "http://localhost:8080/api/search?q=invoice%202024" \
-H "Authorization: Bearer $TOKEN" | jq
# Get OCR queue status
curl -X GET http://localhost:8080/api/ocr/queue/status \
-H "Authorization: Bearer $TOKEN" | jq
# Create a new source
curl -X POST http://localhost:8080/api/sources \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Company Drive",
"type": "webdav",
"url": "https://drive.company.com/dav/",
"username": "user",
"password": "pass",
"sync_interval": 3600
}'
API Versioning¶
The API uses URL versioning. The current version is v1. Future versions will be available at:
Deprecated endpoints will include a Deprecation header with the sunset date:
SDK Support¶
Official SDKs are available for:
- Python:
pip install readur-sdk - JavaScript/TypeScript:
npm install @readur/sdk - Go:
go get github.com/readur/readur-go-sdk
API Limits¶
- Maximum request size: 100MB (configurable)
- Maximum file upload: 500MB
- Maximum bulk upload: 10 files
- Maximum search results: 1000
- WebSocket connections per user: 5
- API calls per minute: 100 (configurable)