Self-hosted knowledge keeper for 3D printing projects
Complete documentation of all 3DKenji REST API endpoints.
http://localhost:8000/api/v1
All protected endpoints require Authorization header with JWT bearer token:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Tokens are obtained from the authentication endpoints and expire after 24 hours.
All responses are JSON with consistent structure:
Success (2xx):
{
"id": 1,
"title": "Project Name",
...
}
Error (4xx/5xx):
{
"detail": "Error message describing what went wrong"
}
Create a new user account and receive JWT token.
Request:
POST /auth/register
Content-Type: application/json
Body:
{
"username": "alice",
"nickname": "alice-prints",
"email": "alice@example.com",
"password": "SecurePassword123!",
"display_name": "Alice Smith"
}
Parameters:
username (string, required) – Unique username, 3-50 charsnickname (string, optional) – Storage-safe nickname (3-64 chars, lowercase letters/numbers/_/-)email (string, required) – Valid email addresspassword (string, required) – Minimum 8 charactersdisplay_name (string, optional) – User’s display nameResponse (201):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}
Errors:
400 – Email already exists400 – Weak password (< 8 chars)400 – Invalid email formatAuthenticate with username and password.
Request:
POST /auth/login
Content-Type: application/json
Body:
{
"username": "alice",
"password": "SecurePassword123!"
}
Response (200):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}
Errors:
401 – Invalid credentials404 – User not foundUpdate user password (requires authentication).
Request:
POST /auth/password-change
Authorization: Bearer <token>
Content-Type: application/json
Body:
{
"old_password": "SecurePassword123!",
"new_password": "NewSecurePassword456!"
}
Response (200):
{
"message": "Password changed successfully"
}
Errors:
401 – Unauthorized (no valid token)400 – Old password incorrect400 – New password too weakThese endpoints enable external OIDC authentication and account linking. Replace {provider} with your configured provider slug (default: oidc). OAuth configuration is now stored in the database through the admin settings screen. See docs/oauth-setup.md for operator setup and examples.
GET /auth/oauth/{provider}/authorize — Initiate OIDC login (redirects to provider).
400 for misconfiguration, 503 if IdP unreachable.GET /auth/oauth/{provider}/callback — OIDC callback endpoint.
state and ID token, issues 3DKenji JWT, sets cookie (if configured), and redirects to frontend or returns JSON depending on client.400 invalid/expired state, 401 invalid ID token, 500 internal validation error.POST /auth/oauth/{provider}/link — Start link flow for logged-in users.
{ "authorize_url": "https://..." } — a URL the client can open to complete linking.401 unauthenticated, 400 provider misconfigured.DELETE /auth/oauth/{provider}/unlink — Unlink a provider from your account.
401 unauthenticated, 403 local password not set (prevents lockout), 404 provider not linked.POST /auth/admin/recovery-login — Admin-only local credential recovery.
{ "username": "admin", "password": "LocalPassword!" }
{ "access_token": "...", "token_type": "bearer" }401 invalid credentials, 403 account is not an admin.POST /auth/admin/set-local-password — Admin sets a local password (useful if admin account was created via OIDC).
{ "new_password": "NewRecoveryPassword!" }
{ "message": "Local password set" }401 unauthenticated, 403 not an admin, 400 weak password.POST /admin/settings/oauth — Save database-backed OAuth / OIDC settings from the admin settings page.
application/x-www-form-urlencoded (used by the admin UI)enabled — checkbox; enables OIDC loginprovider_name — URL slug such as oidcissuer_url — provider issuer URLclient_id — provider client IDclient_secret — new client secret value; leave blank to keep the stored secretcallback_url — full OIDC callback URLscopes — space-separated scopescookie_secure — checkbox; mark auth cookie as secureclear_client_secret — checkbox; removes the stored secret400 invalid or incomplete settings, 403 admin access requiredviewer (read) or editor (write) roles.Create a new project (requires authentication).
Request:
POST /projects
Authorization: Bearer <token>
Content-Type: application/json
Body:
{
"title": "Benchy Calibration",
"description": "Printer calibration prints",
"visibility": "private"
}
Parameters:
title (string, required) – Project name, 1-200 charsdescription (string, optional) – Project descriptionvisibility (string, optional) – private or publicResponse (201):
{
"id": "a5e4420a-3e22-41c3-a219-0bd9d8b7ab07",
"title": "Benchy Calibration",
"slug": "benchy-calibration",
"category": "Uncategorized",
"visibility": "private",
"directory_path": "Projects/alice-prints/benchy-calibration",
"owner_id": "user_123",
"created_at": "2026-02-21T10:30:45Z",
"updated_at": "2026-02-21T10:30:45Z"
}
Errors:
401 – Unauthorized400 – Title missing or too longGet all projects accessible to the authenticated user (owned + collaborator + public).
Request:
GET /projects?skip=0&limit=10
Authorization: Bearer <token>
Query Parameters:
skip (integer, optional, default=0) – Number of items to skiplimit (integer, optional, default=10) – Number of items to returnResponse (200):
{
"items": [
{
"id": "project_123",
"title": "Benchy Calibration",
"owner_id": "user_123",
"slug": "benchy-calibration",
"category": "Calibration",
"visibility": "private",
"directory_path": "/data/storage/Projects/alice/benchy-calibration",
"disk_size_bytes": 24576,
"is_archived": false,
"created_at": "2026-02-21T10:30:45Z",
"updated_at": "2026-02-21T10:30:45Z"
}
],
"total": 1,
"skip": 0,
"limit": 10
}
Errors:
401 – UnauthorizedGet details of a specific project.
Request:
GET /projects/{id}
Authorization: Bearer <token>
Parameters:
id (integer, required) – Project IDResponse (200):
{
"id": 1,
"title": "Benchy Calibration",
"description": "Printer calibration prints",
"owner_id": "user_123",
"created_at": "2026-02-21T10:30:45Z",
"updated_at": "2026-02-21T10:30:45Z"
}
Errors:
401 – Unauthorized403 – Access denied (not your project)404 – Project not foundUpdate project title/category/visibility.
Request:
PATCH /projects/{id}
Authorization: Bearer <token>
Content-Type: application/json
Body:
{
"title": "Updated Project Name",
"custom_metadata": {
"category": "Calibration",
"visibility": "public"
}
}
Parameters:
title (string, optional) – New project namedescription (string, optional) – New descriptionResponse (200):
{
"id": 1,
"title": "Updated Project Name",
"description": "Updated description",
"owner_id": "user_123",
"created_at": "2026-02-21T10:30:45Z",
"updated_at": "2026-02-21T10:30:46Z"
}
Errors:
401 – Unauthorized403 – Not the project owner404 – Project not foundList publicly visible projects without authentication.
Request:
GET /projects/public?skip=0&limit=100
Response (200):
{
"items": [
{
"id": "a5e4420a-3e22-41c3-a219-0bd9d8b7ab07",
"title": "Benchy Calibration",
"slug": "benchy-calibration",
"visibility": "public"
}
],
"total": 1,
"skip": 0,
"limit": 100
}
Read one publicly visible project without authentication.
Request:
GET /projects/public/{project_id}
Errors:
404 – Not found, private, or archivedAll collaboration management endpoints below require owner permissions on the target project.
GET /projects/{project_id}/collaborators
Authorization: Bearer <token>
POST /projects/{project_id}/collaborators
Authorization: Bearer <token>
Content-Type: application/json
Body:
{
"user_id": "b84a2750-c27f-43e4-a735-a09f8f4c5c5f",
"role": "editor"
}
DELETE /projects/{project_id}/collaborators/{user_id}
Authorization: Bearer <token>
POST /projects/{project_id}/invitations
Authorization: Bearer <token>
Content-Type: application/json
Body:
{
"email": "collaborator@example.com",
"role": "viewer",
"expires_in_days": 7
}
Returns a one-time invitation token and invitation metadata.
GET /projects/{project_id}/invitations?include_inactive=false
Authorization: Bearer <token>
PATCH /projects/{project_id}/invitations/{invitation_id}
Authorization: Bearer <token>
Content-Type: application/json
DELETE /projects/{project_id}/invitations/{invitation_id}
Authorization: Bearer <token>
POST /projects/invitations/{token}/accept
Authorization: Bearer <token>
POST /projects/invitations/id/{invitation_id}/accept
Authorization: Bearer <token>
GET /projects/invitations/mine
Authorization: Bearer <token>
Pending invitations are matched by authenticated user email. Acceptance fails if invitation email does not match the current user.
Delete a project and its associated metadata.
Request:
DELETE /projects/{id}
Authorization: Bearer <token>
Parameters:
id (integer, required) – Project IDResponse (204): No content
Errors:
401 – Unauthorized403 – Not the project owner404 – Project not foundList files in a project directory. This powers the Project Files browser in the web UI.
Request:
GET /projects/{project_id}/files?path=models
Authorization: Bearer <token>
Parameters:
project_id (integer, required, path) – Project IDpath (string, optional, query) – Directory inside the project to browse. Defaults to project root.format (string, optional, query) – Use html for server-rendered fragments; omit for JSON.Response (200):
{
"project_id": 1,
"path": "models",
"parent_path": "",
"items": [
{
"name": "benchy.stl",
"relative_path": "models/benchy.stl",
"is_dir": false,
"size_bytes": 1048576,
"size": "1.0 MB",
"extension": "stl",
"is_editable": false,
"viewer": {
"viewer_id": "stl-viewer",
"name": "STL Basic Preview",
"plugin_id": "stl-viewer-plugin",
"has_js": true
}
}
]
}
Errors:
401 – Unauthorized403 – Access denied400 – Invalid or traversal path404 – Directory not foundUpload a file into the selected project directory.
Request:
POST /projects/{project_id}/files/upload
Authorization: Bearer <token>
Content-Type: multipart/form-data
Parameters:
project_id (integer, required, path) – Project IDfile (file, required, multipart) – File to uploadpath (string, optional, multipart) – Target directory inside the project, such as models or imagesResponse (201):
{
"project_id": 1,
"path": "models",
"relative_path": "models/benchy.stl",
"name": "benchy.stl",
"size_bytes": 1048576,
"size": "1.0 MB"
}
Errors:
401 – Unauthorized403 – Not the project owner400 – Invalid filename or target path404 – Directory not found413 – File too large (> 100MB)Preview a file in JSON or HTML form. Text formats return inline preview text, and viewer-enabled formats can render through plugin-provided preview code.
Request:
GET /projects/{project_id}/files/preview?path=models/benchy.stl
Authorization: Bearer <token>
Parameters:
project_id (integer, required, path) – Project IDpath (string, required, query) – File path inside the projectformat (string, optional, query) – Use html for rendered preview markup; omit for JSONResponse (200):
{
"path": "models/benchy.stl",
"extension": "stl",
"size_bytes": 1048576,
"viewer": {
"viewer_id": "stl-viewer",
"name": "STL Basic Preview",
"plugin_id": "stl-viewer-plugin",
"has_js": true
},
"preview_mode": "binary",
"preview_text": null
}
Errors:
401 – Unauthorized403 – Access denied400 – Invalid path or directory requested instead of a file404 – File not foundDownload a file from a project.
Request:
GET /projects/{project_id}/files/download?path=models/benchy.stl
Authorization: Bearer <token>
Parameters:
project_id (integer, required, path) – Project IDpath (string, required, query) – File path inside the projectResponse (200): Binary file download
Errors:
401 – Unauthorized403 – Access denied400 – Invalid path404 – File not foundGenerate a new API key for programmatic access.
Request:
POST /keys
Authorization: Bearer <token>
Content-Type: application/json
Body:
{
"name": "CI/CD Pipeline"
}
Parameters:
name (string, required) – Key name for identificationResponse (201):
{
"id": "key_abc123",
"name": "CI/CD Pipeline",
"created_at": "2026-02-21T10:30:45Z",
"secret": "sk_live_abc123def456ghi789jkl..."
}
Important: The secret is only shown once. Store it securely.
Errors:
401 – Unauthorized400 – Name missingGet all API keys for authenticated user (secrets not shown).
Request:
GET /keys
Authorization: Bearer <token>
Response (200):
{
"items": [
{
"id": "key_abc123",
"name": "CI/CD Pipeline",
"created_at": "2026-02-21T10:30:45Z"
}
],
"total": 1
}
Errors:
401 – UnauthorizedDelete and disable an API key immediately.
Request:
DELETE /keys/{id}
Authorization: Bearer <token>
Parameters:
id (string, required) – Key ID (e.g., “key_abc123”)Response (204): No content
Errors:
401 – Unauthorized403 – Permission denied404 – Key not foundGet complete health status of all components.
Request:
GET /health
Response (200):
{
"status": "ok",
"timestamp": "2026-02-21T10:30:45.123Z",
"components": [
{
"name": "storage_backend",
"status": "ok",
"timestamp": "2026-02-21T10:30:45.123Z"
},
{
"name": "database",
"status": "ok",
"timestamp": "2026-02-21T10:30:45.123Z"
}
]
}
Check if service is ready to accept requests (Kubernetes).
Request:
GET /health/ready
Response:
200 – Service is ready503 – Service not readyCheck if service process is alive (Kubernetes).
Request:
GET /health/live
Response:
200 – Service is alive503 – Service failed| Code | Meaning |
|---|---|
| 200 | OK – Request succeeded |
| 201 | Created – Resource created successfully |
| 204 | No Content – Successful, no response body |
| 400 | Bad Request – Invalid parameters |
| 401 | Unauthorized – Missing or invalid token |
| 403 | Forbidden – Permission denied |
| 404 | Not Found – Resource not found |
| 500 | Internal Server Error – Server error |
| 503 | Service Unavailable – Service not ready |
Currently no rate limiting is implemented. Production deployments should add rate limiting via reverse proxy (nginx, CloudFlare, etc.).
See getting-started.md for common cURL examples.
import requests
BASE_URL = "http://localhost:8000/api/v1"
TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# List projects
response = requests.get(
f"{BASE_URL}/projects",
headers={"Authorization": f"Bearer {TOKEN}"}
)
projects = response.json()
const BASE_URL = "http://localhost:8000/api/v1";
const TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
// List projects
const response = await fetch(`${BASE_URL}/projects`, {
headers: {
"Authorization": `Bearer ${TOKEN}`
}
});
const projects = await response.json();