This document describes the current, implemented API surface for Chatbook-adjacent features in the multi-user API: Chat Dictionaries, World Books (lorebooks), Document Generator, and Chatbooks (import/export). It reflects the code as of v0.1.0 in tldw_Server_API and corrects any previously published mismatches.
- Single-user:
X-API-KEY: <key> - Multi-user:
Authorization: Bearer <JWT> - Standard limits apply; background jobs (export/import, document generator) may have additional concurrency limits.
Notes on conventions used here:
- Base API prefix is
/api/v1. - Authentication uses either
X-API-KEY(single-user) orAuthorization: Bearer <JWT>(multi-user). - Pagination uses
limitandoffsetwhere applicable and responses include atotalfield in the body (no page/per_page headers). - Rate limits are applied via RG ingress policies and may differ by route.
- Chat Dictionary API
- World Book Manager API
- Chat Tools (Slash Commands)
- Document Generator API
- Chatbooks Import/Export API
Pattern-based text replacement for conversations. Supports literal and regex patterns with probability and optional grouping.
/api/v1/chat/dictionaries
POST /api/v1/chat/dictionaries
Creates a new chat dictionary for the authenticated user.
Request body:
{
"name": "Fantasy Terms",
"description": "Convert modern terms to fantasy equivalents"
}Response body (ChatDictionaryResponse):
{
"id": 1,
"name": "Fantasy Terms",
"description": "Convert modern terms to fantasy equivalents",
"is_active": true,
"version": 1,
"entry_count": 0,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}GET /api/v1/chat/dictionaries
Lists all dictionaries for the authenticated user.
Query parameters:
include_inactive(boolean): Include inactive dictionaries (default: false)
Response body:
{
"dictionaries": [
{
"id": 1,
"name": "Fantasy Terms",
"description": "Convert modern terms to fantasy equivalents",
"is_active": true,
"version": 1,
"entry_count": 25,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
],
"total": 1,
"active_count": 1,
"inactive_count": 0
}GET /api/v1/chat/dictionaries/{dictionary_id}
Response body:
{
"id": 1,
"name": "Fantasy Terms",
"description": "Convert modern terms to fantasy equivalents",
"is_active": true,
"entries": [
{
"id": 1,
"pattern": "car",
"replacement": "carriage",
"type": "literal",
"probability": 1.0,
"max_replacements": 0,
"enabled": true,
"case_sensitive": true,
"group": null,
"timed_effects": null
}
],
"created_at": "2024-01-01T00:00:00Z"
}POST /api/v1/chat/dictionaries/{dictionary_id}/entries
Notes:
typemust beliteralorregex.probabilityis a float in [0.0, 1.0].case_sensitiveapplies to literal matching.max_replacementsof 0 means “unlimited”.timed_effectssupports{ sticky, cooldown, delay }in seconds (optional).
Request body:
{
"pattern": "phone",
"replacement": "sending stone",
"type": "literal",
"probability": 1.0,
"max_replacements": 0,
"enabled": true,
"case_sensitive": true
}Response body:
{
"id": 2,
"dictionary_id": 1,
"pattern": "phone",
"replacement": "sending stone",
"probability": 1.0,
"group": null,
"timed_effects": null,
"max_replacements": 0,
"type": "literal",
"enabled": true,
"case_sensitive": true,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}PUT /api/v1/chat/dictionaries/entries/{entry_id}
Request body (all fields optional):
{
"pattern": "telephone",
"replacement": "communication crystal",
"probability": 0.9,
"type": "literal",
"enabled": true,
"case_sensitive": true,
"group": "tech",
"max_replacements": 0
}DELETE /api/v1/chat/dictionaries/entries/{entry_id}
POST /api/v1/chat/dictionaries/process
Request body:
{
"text": "I'll call you on my phone from the car",
"token_budget": 1000,
"dictionary_id": 1,
"max_iterations": 5
}Response body:
{
"original_text": "I'll call you on my phone from the car",
"processed_text": "I'll call you on my sending stone from the carriage",
"replacements": 2,
"iterations": 1,
"entries_used": [3, 5],
"token_budget_exceeded": false,
"processing_time_ms": 2.4
}GET /api/v1/chat/dictionaries/{dictionary_id}/entries
Query parameters:
group(string, optional)
Response body:
{
"entries": [
{
"id": 1,
"dictionary_id": 1,
"pattern": "car",
"replacement": "carriage",
"probability": 1.0,
"group": null,
"timed_effects": null,
"max_replacements": 0,
"type": "literal",
"enabled": true,
"case_sensitive": true,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
],
"total": 1,
"dictionary_id": 1,
"group": null
}POST /api/v1/chat/dictionaries/import
Notes:
- Markdown import is intended for lightweight interoperability and may not preserve all advanced entry settings.
- Use JSON import/export for full-fidelity round trips.
Request body:
{
"name": "Fantasy Terms",
"content": "# Fantasy Terms\n\n## Entry: AI\n- **Type**: literal\n- **Replacement**: Artificial Intelligence\n- **Enabled**: true\n",
"activate": true
}POST /api/v1/chat/dictionaries/import/json
Request body:
{
"data": {
"name": "Fantasy Terms",
"entries": [
{"pattern": "car", "replacement": "carriage", "type": "literal"}
]
},
"activate": true
}GET /api/v1/chat/dictionaries/{dictionary_id}/export
Notes:
- Markdown export is human-readable, but advanced fields such as timed effects,
max_replacements, and other non-standard metadata may not round-trip fully. - Prefer JSON export when you need guaranteed full-fidelity backup/restore behavior.
Response body:
{
"name": "Fantasy Terms",
"content": "# Fantasy Terms\n\ncar: carriage\nphone: sending stone\n",
"entry_count": 25,
"group_count": 3
}GET /api/v1/chat/dictionaries/{dictionary_id}/export/json
Notes:
- JSON export is the canonical full-fidelity format for dictionary backup, migration, and re-import.
Response body:
{
"name": "Fantasy Terms",
"description": "Convert modern terms to fantasy equivalents",
"entries": [
{"pattern": "car", "replacement": "carriage", "type": "literal", "probability": 1.0}
]
}PUT /api/v1/chat/dictionaries/{dictionary_id}
Update dictionary metadata (name, description, is_active).
DELETE /api/v1/chat/dictionaries/{dictionary_id}
GET /api/v1/chat/dictionaries/{dictionary_id}/statistics
Aggregate statistics for a dictionary (counts by type, groups, average probability, usage if available).
POST /api/v1/chat/dictionaries/validate
Validate dictionary structure, regex safety, and template syntax. Returns a structured report.
Request body:
{
"data": {
"name": "Example",
"entries": [
{"type": "literal", "pattern": "today", "replacement": "It is {{ now('%B %d') }}."},
{"type": "regex", "pattern": "User:(\\w+)", "replacement": "Hello, {{ match.group(1) }}!"}
]
},
"schema_version": 1,
"strict": false
}Response body:
{
"ok": true,
"schema_version": 1,
"errors": [],
"warnings": [{"code": "regex_ambiguous", "field": "entries[1].pattern", "message": "…"}],
"entry_stats": {"total": 2, "regex": 1, "literal": 1},
"suggested_fixes": []
}Semantics:
strict=false(default) returns a report witherrorsandwarningsbut does not cause the API call itself to fail; callers decide how to handle issues.strict=trueis intended for server-side workflows (e.g., Chatbooks import) where certain error classes (schema, regex safety/timeouts, template parse/forbidden constructs, output/size limits) should be treated as fatal.- Unknown or unsupported
schema_versionvalues still return HTTP 200 with aschema_invaliderror code in the response payload; HTTP 400 is reserved for malformed request bodies that do not matchValidateDictionaryRequest.
Planned additions (not yet implemented): Clone dictionary, toggle active status, bulk add/update entries, search entries across dictionaries.
Contextual lore injection for character conversations. Supports keyword-based entry matching and character-specific attachments.
/api/v1/characters/world-books
POST /api/v1/characters/world-books
Request body:
{
"name": "Fantasy Campaign",
"description": "D&D campaign setting",
"scan_depth": 3,
"token_budget": 1000,
"recursive_scanning": true,
"enabled": true
}Response body:
{
"id": 1,
"name": "Fantasy Campaign",
"description": "D&D campaign setting",
"scan_depth": 3,
"token_budget": 1000,
"recursive_scanning": true,
"enabled": true,
"version": 1,
"entry_count": 0,
"created_at": "2024-01-01T00:00:00Z",
"last_modified": "2024-01-01T00:00:00Z"
}GET /api/v1/characters/world-books
Query parameters:
include_disabled(boolean): Include disabled world books
Response body:
{
"world_books": [
{
"id": 1,
"name": "Fantasy Campaign",
"description": "D&D campaign setting",
"entry_count": 50,
"enabled": true,
"created_at": "2024-01-01T00:00:00Z"
}
],
"total": 1,
"enabled_count": 1,
"disabled_count": 0
}GET /api/v1/characters/world-books/{world_book_id}
Response body:
{
"id": 1,
"name": "Fantasy Campaign",
"entries": [
{
"id": 1,
"keywords": ["dragon", "wyrm"],
"content": "Dragons are ancient magical beings",
"priority": 100,
"enabled": true
}
]
}POST /api/v1/characters/world-books/{world_book_id}/entries
Request body:
{
"keywords": ["magic", "wizard", "spell"],
"content": "Magic in this world comes from ley lines",
"priority": 90,
"enabled": true
}PUT /api/v1/characters/world-books/entries/{entry_id}
DELETE /api/v1/characters/world-books/entries/{entry_id}
POST /api/v1/characters/{character_id}/world-books
Request body:
{
"world_book_id": 1,
"enabled": true,
"priority": 0
}DELETE /api/v1/characters/{character_id}/world-books/{world_book_id}
GET /api/v1/characters/{character_id}/world-books
POST /api/v1/characters/world-books/process
Request body:
{
"text": "Tell me about dragons and magic",
"character_id": 1,
"max_tokens": 1000
}Response body:
{
"injected_content": "[World Info: Dragons are ancient magical beings...]\n[World Info: Magic in this world comes from ley lines...]",
"entries_matched": 2,
"tokens_used": 200,
"books_used": 1,
"entry_ids": [1, 2]
}POST /api/v1/characters/{character_id}/world-books
Body:
{ "world_book_id": 10, "enabled": true, "priority": 100 }DELETE /api/v1/characters/{character_id}/world-books/{world_book_id}
Not implemented. (Planned)
POST /api/v1/characters/world-books/import
Request body:
{
"world_book": {"name": "Imported World", "description": "Imported lore"},
"entries": [
{"keywords": ["test"], "content": "Test content", "priority": 100}
],
"merge_on_conflict": false
}GET /api/v1/characters/world-books/{world_book_id}/export
Not implemented. (Planned)
DELETE /api/v1/characters/world-books/{world_book_id}
POST /api/v1/characters/world-books/entries/bulk
Request body:
{
"entry_ids": [1, 2, 3],
"operation": "disable"
}GET /api/v1/characters/world-books/{world_book_id}/statistics
Creates structured documents from conversations. Supports multiple document types and async job management.
/api/v1/chat/documents
timelinestudy_guidebriefingsummaryq_and_ameeting_notes
POST /api/v1/chat/documents/generate
Request body (GenerateDocumentRequest):
{
"conversation_id": 123,
"document_type": "timeline",
"provider": "openai",
"model": "gpt-4o",
"api_key": "...",
"specific_message": null,
"custom_prompt": "Focus on technical decisions",
"stream": false,
"async_generation": false
}Response (synchronous):
{
"document_id": 42,
"conversation_id": 123,
"document_type": "timeline",
"title": "Conversation Timeline",
"content": "Timeline...",
"provider": "openai",
"model": "gpt-4o",
"generation_time_ms": 2500,
"created_at": "2024-01-01T00:00:00Z"
}Response (asynchronous):
{
"job_id": "job_456",
"status": "pending",
"conversation_id": 123,
"document_type": "timeline",
"message": "Document generation job created",
"created_at": "2024-01-01T00:00:00Z"
}GET /api/v1/chat/documents/{document_id}
GET /api/v1/chat/documents
DELETE /api/v1/chat/documents/{document_id}
GET /api/v1/chat/documents/jobs/{job_id}
DELETE /api/v1/chat/documents/jobs/{job_id}
POST /api/v1/chat/documents/prompts
GET /api/v1/chat/documents/prompts/{document_type}
Query parameters:
conversation_id(integer, optional)document_type(enum, optional)limit(integer, default 50)
DELETE /api/v1/chat/documents/{document_id}
POST /api/v1/chat/documents/bulk
Request body (BulkGenerateRequest):
{
"conversation_ids": [123, 456],
"document_types": ["timeline", "summary", "q_and_a"],
"provider": "openai",
"model": "gpt-4o",
"api_key": "...",
"async_generation": true
}GET /api/v1/chat/documents/jobs/{job_id}
Response body (JobStatusResponse):
{
"job_id": "job_456",
"status": "completed",
"conversation_id": 123,
"document_type": "timeline",
"provider": "openai",
"model": "gpt-4o",
"result_content": "...",
"created_at": "2024-01-01T00:00:00Z",
"started_at": "2024-01-01T00:00:01Z",
"completed_at": "2024-01-01T00:00:05Z",
"progress_percentage": 100
}DELETE /api/v1/chat/documents/jobs/{job_id}
POST /api/v1/chat/documents/prompts
Request body:
{
"document_type": "timeline",
"system_prompt": "You are a helpful document generator...",
"user_prompt": "Write a timeline...",
"temperature": 0.7,
"max_tokens": 2000
}GET /api/v1/chat/documents/prompts/{document_type}
GET /api/v1/chat/documents/statistics
Export/import collections of chat-related content with job management and secure downloads.
Chatbooks can include chat dictionaries and other template-aware content whose behavior is defined in the Chatbook Tools PRD and related APIs:
-
Embedded dictionaries are validated during
POST /api/v1/chatbooks/importusing the same validator exposed atPOST /api/v1/chat/dictionaries/validate. Validation findings (schema/regex/template issues) appear as per-item warnings/errors in Chatbook import job results. -
The
strictflag in/chat/dictionaries/validateis not forwarded directly from Chatbooks; instead, import always calls the validator withstrict=falseand uses theCHATBOOKS_IMPORT_DICT_STRICTenvironment flag to decide whether dictionaries with fatal errors are skipped or imported with warnings. -
Template-related manifest metadata (
metadata.template_mode,metadata.template_defaults,metadata.template_timezone,metadata.template_locale) controls template evaluation during Chatbook exports and imports. Thetemplate_modefield accepts three values:pass_through(default) — template expressions (e.g.{{ project }}) are preserved verbatim in the artifact. No rendering occurs.render_on_export— template expressions are evaluated at export time using the variables intemplate_defaults, the timezone fromtemplate_timezone(defaultUTC), and the locale fromtemplate_locale. The rendered result replaces the raw template in-place in the exported artifact (note markdown files, dictionary JSON files); the original raw template text is not retained alongside the rendered output.render_on_import— same rendering behavior, but applied at import time. The content stored in the database contains the rendered text, not the raw template.
Affected fields: note titles and content, dictionary names, descriptions, and entry
replacement/contentvalues. For dictionary entry rendering, theCHAT_DICT_TEMPLATES_ENABLEDenvironment flag must also be set totrue.Failure semantics: rendering uses a fail-safe strategy. If template evaluation fails for any reason (syntax error, undefined variable, timeout, output exceeding the size cap), the original raw template text is preserved unchanged in the artifact — the export or import is never aborted due to a render failure. Failures are logged at
DEBUGlevel and tracked via thetemplate_render_failure_totalmetrics counter (labeled by source and reason), but no error codes, warnings, or error metadata are surfaced to the client in the job result. To detect silent render failures, operators should monitor thetemplate_render_failure_totalandtemplate_render_timeout_totalmetrics.
/api/v1/chatbooks
POST /api/v1/chatbooks/export
Request body (CreateChatbookRequest):
{
"name": "My Chatbook Export",
"description": "Backup of my conversations",
"content_selections": {
"conversation": ["conv123", "conv456"],
"note": ["note789"],
"character": ["char001"],
"world_book": [1],
"dictionary": [2],
"generated_document": [42]
},
"author": "Jane Doe",
"include_media": false,
"media_quality": "compressed",
"include_embeddings": false,
"include_generated_content": true,
"tags": ["backup"],
"categories": ["personal"],
"async_mode": true
}Response body (async mode):
{
"success": true,
"job_id": "0c9d9a3a-6d1c-4c8f-9c84-9a0c2c2d8f77",
"status": "pending",
"message": "Export job created successfully"
}POST /api/v1/chatbooks/preview
Preview an uploaded chatbook file (no import).
Request (multipart/form-data):
file: ZIP archive
Response body (manifest summary):
{
"manifest": {
"version": "1.0",
"name": "My Chatbook Export",
"total_conversations": 10,
"total_notes": 5,
"total_world_books": 2,
"total_dictionaries": 1,
"total_documents": 3,
"include_media": false
}
}GET /api/v1/chatbooks/export/jobs/{job_id}
Response body (ExportJobResponse):
{
"job_id": "0c9d9a3a-6d1c-4c8f-9c84-9a0c2c2d8f77",
"status": "completed",
"chatbook_name": "My Chatbook Export",
"download_url": "/api/v1/chatbooks/download/0c9d9a3a-6d1c-4c8f-9c84-9a0c2c2d8f77",
"file_size_bytes": 15900000,
"created_at": "2024-01-01T00:00:00Z",
"completed_at": "2024-01-01T00:00:30Z"
}GET /api/v1/chatbooks/download/{job_id}
Returns a ZIP file with secure headers.
POST /api/v1/chatbooks/import
Request (multipart/form-data):
file: The chatbook archive file (ZIP)- Additional fields (ImportChatbookRequest via form fields):
conflict_resolution: one ofskip,overwrite,rename,mergeprefix_imported: booleanimport_media: booleanimport_embeddings: booleanasync_mode: boolean
Response body (async mode):
{
"success": true,
"job_id": "import_job_456",
"status": "pending",
"message": "Import job created successfully"
}GET /api/v1/chatbooks/import/jobs/{job_id}
Response body (ImportJobResponse):
{
"job_id": "import_job_456",
"status": "completed",
"items_imported": 55,
"conflicts_found": 5,
"successful_items": 50,
"failed_items": 3,
"skipped_items": 2,
"conflicts": [],
"created_at": "2024-01-01T00:00:00Z",
"completed_at": "2024-01-01T00:01:00Z"
}No dedicated /validate endpoint. Use /chatbooks/preview (upload) to inspect a manifest without importing.
GET /api/v1/chatbooks/export/jobs
Query parameters:
limit(integer, default 100)offset(integer, default 0)
GET /api/v1/chatbooks/import/jobs
DELETE /api/v1/chatbooks/export/jobs/{job_id}
DELETE /api/v1/chatbooks/import/jobs/{job_id}
DELETE /api/v1/chatbooks/export/jobs/{job_id}/remove
DELETE /api/v1/chatbooks/import/jobs/{job_id}/remove
POST /api/v1/chatbooks/cleanup
GET /api/v1/chatbooks/health
Lightweight health indicator for the Chatbooks subsystem.
Endpoints return standard FastAPI error responses with meaningful HTTP status codes and a detail message. Some success responses include a success boolean for convenience (e.g., Chatbooks export/import). Domain-specific errors may include additional fields (see response schemas).
Endpoint-specific limits are enforced with RG policies where applied:
- Chatbooks
POST /export: 5/minute - Chatbooks
POST /import: 5/minute - Chatbooks
POST /preview: 10/minute - Chatbooks
GET /download/{job_id}: 20/minute
A global limiter may also be active depending on environment. When rate limits are hit, a 429 is returned.
List endpoints use limit and offset query parameters and include a total count in the response body.
Webhook notifications for Chatbooks are planned but not yet implemented. See the Chatbook Developer Guide for design notes.
import json
from urllib.request import Request, urlopen
class ChatbookAPI:
def __init__(self, base_url, token):
self.base_url = base_url.rstrip("/")
self.headers = {"Authorization": f"Bearer {token}"}
def _request_json(self, method, path, payload):
url = f"{self.base_url}{path}"
data = json.dumps(payload).encode("utf-8")
headers = {"Content-Type": "application/json", **self.headers}
req = Request(url, data=data, headers=headers, method=method)
with urlopen(req) as resp:
return json.loads(resp.read().decode("utf-8"))
def create_dictionary(self, name, description):
return self._request_json(
"POST",
"/api/v1/chat/dictionaries",
{"name": name, "description": description},
)
def process_text(self, text, token_budget=1000, dictionary_id=None):
return self._request_json(
"POST",
"/api/v1/chat/dictionaries/process",
{"text": text, "token_budget": token_budget, "dictionary_id": dictionary_id},
)class ChatbookAPI {
constructor(baseUrl, token) {
this.baseUrl = baseUrl;
this.headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
}
async exportChatbook(req) {
const response = await fetch(`${this.baseUrl}/api/v1/chatbooks/export`, {
method: 'POST',
headers: this.headers,
body: JSON.stringify(req)
});
return response.json();
}
async getExportStatus(jobId) {
const response = await fetch(
`${this.baseUrl}/api/v1/chatbooks/export/jobs/${jobId}`,
{ headers: this.headers }
);
return response.json();
}
}For users migrating from the TUI application:
- Authentication is required for all API calls.
- User content is isolated per-user (per-user DBs, per-user export/import paths).
- Large operations support async jobs with status polling and secure download URLs.
- Import conflict resolution supports
skip,overwrite,rename, andmerge. - Endpoint-level rate limiting is enforced; plan for 429 handling.
Discovery endpoint for slash commands that run before LLM dispatch (e.g., /time, /weather, /skills, /skill).
Injection behavior (configurable):
CHAT_COMMAND_INJECTION_MODE=system(default): insert command result as a separatesystemmessage and strip the/commandtoken from the user's text.CHAT_COMMAND_INJECTION_MODE=preface: prefix the user's text with the command result.CHAT_COMMAND_INJECTION_MODE=replace: replace the user's text entirely with the command result.
GET /api/v1/chat/commands
Returns available commands (RBAC-filtered where applicable).
Response body:
{
"commands": [
{
"name": "time",
"description": "Show the current time (optional TZ).",
"required_permission": "chat.commands.time",
"usage": "/time [timezone]",
"args": ["timezone"],
"requires_api_key": true,
"rate_limit": "per-user 10/min, global 100/min",
"rbac_required": true
},
{
"name": "weather",
"description": "Show current weather for a location.",
"required_permission": "chat.commands.weather",
"usage": "/weather [location]",
"args": ["location"],
"requires_api_key": true,
"rate_limit": "per-user 10/min, global 100/min",
"rbac_required": true
},
{
"name": "skills",
"description": "List invocable skills for this user.",
"required_permission": "chat.commands.skills",
"usage": "/skills [filter]",
"args": ["filter"],
"requires_api_key": true,
"rate_limit": "per-user 10/min, global 100/min",
"rbac_required": true
},
{
"name": "skill",
"description": "Execute an invocable skill by name.",
"required_permission": "chat.commands.skill",
"usage": "/skill <name> [args]",
"args": ["name", "args"],
"requires_api_key": true,
"rate_limit": "per-user 10/min, global 100/min",
"rbac_required": true
}
]
}Field definitions:
requires_api_key— whether invoking this command requires authenticated API access.
Notes:
- The
commandslist is filtered per-user based on AuthNZ/RBAC and deployment configuration. /skillsonly lists invocable skills (user_invocable=trueanddisable_model_invocation=false) for the authenticated user./skillreturns explicit error text for missing name, unknown skills, and non-invocable skills.- Clients should treat
GET /api/v1/chat/commandsas the per-session source of truth and avoid caching the list long-term, since RBAC or configuration changes can add or remove commands at any time.