nia-todo API

Public API documentation for this instance. Authentication uses JWT or API key depending on the endpoint.

API Documentation

Authentication

> All endpoints except /api/login and /api/setup/** require auth.

Login

POST /api/login

Body

{
  "username": "demo",
  "password": "***"
}

Response

{
  "access_token": "eyJhbGciOi...",
  "token_type": "bearer",
  "user": {
    "id": 1,
    "username": "demo",
    "display_name": "Max Mustermann",
    "email": "user@example.com",
    "avatar_url": "/api/avatars/user-1.webp",
    "is_admin": true
  }
}

Login with 2FA Challenge

If 2FA is active for the user or globally enforced, POST /api/login can return a challenge instead of a token. A verified email with working SMTP counts as an email-code factor:

{
  "mfa_required": true,
  "challenge": {
    "challenge_token": "...",
    "methods": ["totp", "recovery_code"]
  },
  "state": {
    "enabled": true,
    "has_totp": true,
    "has_passkey": false,
    "recovery_codes_remaining": 8
  }
}

Completion:

POST /api/2fa/challenge/verify

{
  "challenge_token": "...",
  "method": "totp",
  "code": "123456",
  "remember_device": true
}

The response matches the normal login (access_token, csrf_token, user). The login challenge is consumed atomically and only creates login MFA assurance; sensitive actions such as password changes/API key management still require a fresh one-time MFA reauth. With remember_device=true, an HttpOnly trusted-device cookie is also set; it replaces later login MFA, but likewise does not count for sensitive actions.

POST /api/2fa/passkey/options and POST /api/2fa/passkey/verify complete the same login challenge via passkey. Passkey login requires user verification and also consumes the challenge exactly once.

Logout

POST /api/logout

Response

{ "ok": true }

Current User

GET /api/me

Response

{
  "id": 1,
  "username": "demo",
  "display_name": "Max Mustermann",
  "email": "user@example.com",
  "avatar_url": "/api/avatars/user-1.webp",
  "avatar_updated_at": "2026-05-21T00:00:00+00:00",
  "is_admin": true,
  "attachments_enabled": true,
  "attachment_usage_bytes": 1048576,
  "attachment_quota_bytes": 5368709120,
  "attachment_quota_remaining_bytes": 5367660544,
  "attachments_allowed_types": [".png", ".jpg", ".jpeg", ".gif", ".webp", ".pdf"]
}

Attachment fields describe the current user's effective upload policy and storage usage. A quota of 0 means uploads are locked/full for that user.

Change Own Profile

PATCH /api/me/profile

Body

{ "display_name": "Max Mustermann" }

Response

{
  "id": 1,
  "username": "demo",
  "display_name": "Max Mustermann",
  "email": "user@example.com",
  "avatar_url": "/api/avatars/user-1.webp",
  "avatar_updated_at": "2026-05-21T00:00:00+00:00",
  "is_admin": true
}

Upload Own Avatar

PUT /api/me/avatar

Request

Storage

Response

{
  "avatar_url": "/api/avatars/user-1.webp",
  "avatar_updated_at": "2026-05-21T00:00:00+00:00"
}

Change Own Email

PATCH /api/me/email

Body

{ "email": "neue@example.com" }

Validation

Response (with SMTP configured)

{
  "email": "alte@example.com",
  "pending_email": "neue@example.com",
  "email_verified_at": "2026-05-20T00:00:00+00:00",
  "email_verification_sent": true
}

Response (without SMTP)

{
  "email": "neue@example.com",
  "email_verified_at": null,
  "email_trust_source": "unverified_no_smtp"
}

Note: With SMTP, the new email is stored as pending_email and a verification email is sent. The old email remains active until verification. Without SMTP, the email is active immediately, but not verified (cannot be used for login/sharing).

Change Own Password

POST /api/me/change-password

Body

{ "old_password": "alt123!", "new_password": "neu123!" }

Response

{ "ok": true }

Two-Factor Authentication

2FA Status

GET /api/me/2fa

Returns enabled/available factors, recovery-code count, global requirement, and passkey count. The status contains no secrets and remains readable with a valid interactive JWT even when no fresh action reauth is present, so clients can start the appropriate reauth flow.

Start/Confirm TOTP

POST /api/me/2fa/totp/start returns secret and otpauth_url.

POST /api/me/2fa/totp/confirm

{ "secret": "BASE32...", "code": "123456", "password": "..." }

Activates TOTP after password confirmation and returns new recovery codes exactly once, plus a fresh MFA JWT. The endpoint may also be used with an enrollment-only JWT; in this state, no additional MFA reauth is possible or required. Recovery codes are backup factors for TOTP/passkey, not the primary 2FA state.

Disable 2FA / Revoke Factors / Regenerate Recovery Codes

Active Device Sessions / Trusted Devices

Passkeys

Passkeys are bound to the configured public base URL (public_base_url). HTTPS is mandatory for non-localhost hosts; without public_base_url, production passkey flows for non-localhost hosts fail closed. Windows Native uses a native WebAuthn bridge with server-provided origin; for this, the server URL configured in the app must match the public_base_url origin/RP ID.

Android Native uses AndroidX Credential Manager. Each self-hosted server instance serves /.well-known/assetlinks.json for the bundled Android app:

In addition to the HTTPS web origin, the server accepts the pinned Android app origin android:apk-key-hash:..., while the RP ID hash continues to be checked against public_base_url. Self-hosters run their own server and connect the bundled Android app to their server URL. Custom package names, F-Droid/re-sign builds, and signing-key rotation are not part of the current 2.0 model and will later need an explicit config/migration strategy.

Admin Policy

Security-sensitive account actions require a fresh, single-use MFA action grant for 2FA-required accounts. Login MFA and trusted devices count only for app access, not for sensitive actions. Initial TOTP/passkey setup is the exception: with an enrollment-only JWT, password confirmation is sufficient because no second factor exists yet. Reauth codes are hardened against replay: email reauth codes are deleted after success, TOTP reauth can issue only one grant per timestep, recovery codes are table-backed and consumed single-use. API keys (ApiKey nt_...) are intentionally exempt from interactive MFA during use as machine tokens. Creating and revoking own API keys always requires a new reauth for MFA-required accounts; the settings UI starts a reauth flow when needed. Existing API keys are not automatically revoked when MFA is enabled; the admin UI shows active keys as a warning. An enrollment-only token is issued only when global 2FA is enforced and no usable factor is available at all. Email-code fallback is a transitional/login fallback, not a user-configured primary factor.

Email / SMTP

Verify Own Email

POST /api/me/email/verify

Body

{ "token": "abc123..." }

Response

{
  "email": "neue@example.com",
  "email_verified_at": "2026-05-23T00:00:00+00:00",
  "ok": true
}

Note: One-time token from the verification email. After successful verification, pending_email becomes email and email_verified_at is set.

Request Password Reset (public)

POST /api/password-setup/request

Body

{ "identifier": "user@example.com" }

Response (always neutral)

{
  "message": "Falls ein passendes Konto existiert, wurde eine E-Mail gesendet."
}

Note: For security reasons, a neutral response is always returned (no enumeration). Reset emails are sent only to verified emails.

Fetch Password Setup Features (public)

GET /api/password-setup/features

Response

{
  "email_configured": true,
  "password_reset_available": true
}

GET /api/password-setup/validate?token=...

Response (valid)

{
  "valid": true,
  "username": "demo",
  "display_name": "Max Mustermann",
  "purpose": "reset",
  "expires_at": "2026-05-24 12:00:00"
}

POST /api/password-setup/resend

Body

{ "token": "..." }

Response

{
  "message": "Neuer Link wurde per E-Mail gesendet.",
  "password_setup_delivery": "email",
  "password_setup_expires_hours": 24
}

POST /api/admin/users/{user_id}/password-link

Response (with SMTP + verified email)

{
  "email_sent": true,
  "message": "Passwort-Setup-Link wurde per E-Mail gesendet."
}

Response (without SMTP or unverified email)

{
  "email_sent": false,
  "password_setup_url": "https://todo.example.com/set-password?token=..."
}

Note: Admins can generate password setup links for users. With SMTP + verified email, the link is sent by email; otherwise it is returned as a manual link.

Fetch Instance Configuration

GET /api/admin/instance-config

Response

{
  "public_base_url": "https://todo.example.com",
  "allowed_origins": ["https://todo.example.com"],
  "trusted_proxies": ["192.0.2.10"]
}

Update Instance Configuration

PATCH /api/admin/instance-config

Body

{
  "public_base_url": "https://todo.example.com",
  "allowed_origins": ["https://todo.example.com"],
  "trusted_proxies": ["192.0.2.10"]
}

Note: public_base_url is used among other things for password/invitation links and production passkey origin/RP ID validation. CORS accepts only configured origins; forwarded headers are evaluated only from trusted proxies.

Admin: Email Configuration

Fetch SMTP Configuration

GET /api/admin/email-config

Response

{
  "smtp_enabled": true,
  "smtp_host": "smtp.example.com",
  "smtp_port": 587,
  "smtp_security": "starttls",
  "smtp_auth_enabled": true,
  "smtp_username": "nia@example.com",
  "smtp_password_configured": true,
  "mail_from_address": "nia@example.com",
  "mail_from_name": "nia-todo",
  "mail_reply_to": null
}

Note: smtp_password_configured is a boolean field; the actual password is never returned.

Update SMTP Configuration

PATCH /api/admin/email-config

Body

{
  "smtp_enabled": true,
  "smtp_host": "smtp.example.com",
  "smtp_port": 587,
  "smtp_security": "starttls",
  "smtp_auth_enabled": true,
  "smtp_username": "nia@example.com",
  "smtp_password": "***",
  "mail_from_address": "nia@example.com",
  "mail_from_name": "nia-todo"
}

Response

{ "ok": true }

Send Test Email

POST /api/admin/email-config/test

Body

{ "to": "user@example.com" }

Response

{
  "ok": true,
  "message": "Test-Mail erfolgreich gesendet."
}

Error (SMTP not configured)

{
  "ok": false,
  "error": "SMTP ist nicht konfiguriert."
}

Project Sharing

Share Project

POST /api/projects/{project_id}/share

Body

{ "username": "user@example.com" }

Response (username invite)

{
  "member": {
    "id": 42,
    "user_id": 5,
    "username": "alice",
    "display_name": "Alice Example",
    "status": "pending"
  },
  "notification_delivery": "in_app"
}

Response (email invite — neutral)

{
  "notification_delivery": "email"
}

Note: For email identifiers (contains @), no member info is returned for security reasons (no enumeration). The invited user receives an email with a link.

List Members

GET /api/projects/{project_id}/members

Response

{
  "members": [
    {
      "id": 1,
      "user_id": 1,
      "username": "demo",
      "display_name": "Max Mustermann",
      "status": "accepted"
    }
  ]
}

Note: Shows only accepted members. Pending invites are not visible for privacy reasons (not even to owners).

Accept/Decline Invitation

POST /api/projects/{project_id}/invites/{invite_id}

Body

{ "accept": true }

Response

{ "ok": true }

Fetch Pending Invitations

GET /api/projects/invites

Response

{
  "invites": [
    {
      "id": 42,
      "project_id": 5,
      "project_name": "Einkaufsliste",
      "invited_by_username": "demo",
      "status": "pending"
    }
  ]
}

Admin: Users

Set Admin Password

POST /api/setup/admin

Body

{ "admin_password": "***" }

Response

{ "ok": true }

Create First User

POST /api/setup/first-user

Body

{
  "username": "demo",
  "email": "user@example.com",
  "password": "***",
  "display_name": "Max Mustermann"
}

Response

{ "ok": true }

Setup Status

GET /api/setup/status

Response

{
  "admin_password_set": true,
  "first_user_created": true,
  "needs_setup": false
}

Admin

Admin Login

POST /api/admin/login

Body

{ "password": "***" }

Response

{
  "access_token": "eyJhbGciOi...",
  "token_type": "bearer",
  "admin": true,
  "csrf_token": "..."
}

Admin Logout

POST /api/admin/logout

Invalidates all admin sessions by increasing the admin token version.

Attachment Configuration

GET /api/admin/attachment-config

Response

{
  "enabled": true,
  "allowed_types": [".png", ".jpg", ".jpeg", ".gif", ".webp", ".pdf"],
  "default_quota_bytes": 5368709120
}

PATCH /api/admin/attachment-config

Body

{
  "enabled": true,
  "allowed_types": ["png", "jpg", "jpeg", "gif", "webp", "pdf"],
  "default_quota_bytes": 5368709120
}

allowed_types may be bare extensions or dot-prefixed extensions; the server normalizes them to dot-prefixed lowercase values. default_quota_bytes=0 makes the global default quota locked/full unless a user override is set.

List Users

GET /api/admin/users

Response

{
  "users": [
    {
      "id": 1,
      "username": "demo",
      "display_name": "Max Mustermann",
      "email": "user@example.com",
      "is_admin": true,
      "attachment_quota_bytes": null,
      "attachment_quota_effective_bytes": 5368709120,
      "attachment_usage_bytes": 1048576
    }
  ]
}

Create User

POST /api/admin/users

Body

{
  "username": "neu",
  "display_name": "Neuer User",
  "email": "neu@example.com"
}

The admin no longer sets a password directly. A one-time password setup link is generated during creation.

Validation

Response

{
  "id": 2,
  "username": "neu",
  "display_name": "Neuer User",
  "email": "neu@example.com",
  "created_at": "2026-05-20T21:30:00Z",
  "password_setup_url": "https://todo.example.com/set-password?token=...",
  "password_setup_expires_hours": 24
}

Update User

PATCH /api/admin/users/{id}

Body

{ "email": "neu@example.com", "attachment_quota_bytes": 1073741824 }

Optionally, display_name, username, braindump_enabled, and attachment_quota_bytes can be provided as well. attachment_quota_bytes=null uses the global default; 0 locks uploads for that user.

Response

{ "id": 2, "email": "neu@example.com", "display_name": null }

Delete User

DELETE /api/admin/users/{id}

Response

{ "deleted": true }

POST /api/admin/users/{id}/change-password / POST /api/admin/users/{user_id}/change-password

> Compatibility endpoint: admins no longer set passwords directly. The endpoint generates a one-time link.

Response

{
  "password_setup_url": "https://todo.example.com/set-password?token=...",
  "password_setup_expires_hours": 24
}

POST /api/admin/users/{id}/password-link

Response

{
  "password_setup_url": "https://todo.example.com/set-password?token=...",
  "password_setup_expires_hours": 24
}

POST /api/password-setup/complete

Body

{ "token": "...", "password": "NeuesPasswort123!" }

Response

{ "message": "Passwort gesetzt" }

Links are valid for 24 hours and can be used only once.

Change Admin Password

POST /api/admin/change-password

Body

{
  "old_password": "alt123!",
  "new_password": "neu123!"
}

Response

{ "ok": true }

API Keys

List

GET /api/me/api-keys

Response

{
  "api_keys": [
    {
      "id": 1,
      "name": "Nia-Integration",
      "key_prefix": "nt_e3b",
      "created_at": "2026-05-16T11:30:00",
      "last_used_at": "2026-05-16T12:00:00",
      "revoked_at": null
    }
  ]
}

Create

POST /api/me/api-keys

Body

{ "name": "Nia-Integration" }

Response

{
  "id": 12,
  "name": "Nia-Integration",
  "prefix": "nt_abcd1234",
  "key": "nt_...",
  "created_at": "2026-05-16T11:30:00+00:00"
}

The full key is shown only once during creation.

Revoke

DELETE /api/me/api-keys/{id} / DELETE /api/me/api-keys/{key_id}

Response

{ "revoked": 12 }

Auth with API Key

Authorization: ApiKey nt_...

Notes

Todos

List

GET /api/todos

Query

Response

{
  "todos": [
    {
      "id": 1,
      "title": "Nia-Todo aufbauen",
      "description": "",
      "priority": 3,
      "status": "pending",
      "due_date": "2026-05-14T10:00:00+00:00",
      "completed_at": null,
      "project_id": 3,
      "section_id": null,
      "project_name": "Arbeit",
      "section_name": null,
      "created_at": "2026-05-12T21:39:40",
      "updated_at": "2026-05-12T21:39:40",
      "reminders": [],
      "subtasks": [
        {
          "id": 10,
          "title": "Backup prüfen",
          "is_done": false,
          "sort_order": 0
        }
      ]
    }
  ]
}

Single Todo

GET /api/todos/{id} / GET /api/todos/{todo_id}

Response

{
  "id": 1,
  "title": "Nia-Todo aufbauen",
  "description": "",
  "priority": 3,
  "status": "pending",
  "project_id": 3,
  "section_id": null,
  "reminders": [],
  "subtasks": []
}

Create

POST /api/todos

Body

{
  "title": "Wäsche waschen",
  "description": "Nicht vergessen",
  "priority": 3,
  "project_id": 2,
  "section_id": 1,
  "due_date": "2026-05-14T10:00:00Z",
  "remind_at": "2026-05-14T09:00:00Z",
  "recurring_rule": {"frequency": "daily", "interval": 1},
  "subtasks": [
    {"title": "Waschmaschine starten", "is_done": false},
    {"title": "Wäsche aufhängen", "is_done": false}
  ]
}

Fields

Response

{
  "id": 17,
  "title": "Wäsche waschen",
  "status": "pending"
}

Update

PATCH /api/todos/{id} / PATCH /api/todos/{todo_id}

Body

Response

{
  "id": 17,
  "title": "Wäsche waschen",
  "status": "done"
}

Delete

DELETE /api/todos/{id} / DELETE /api/todos/{todo_id}

Response

{ "deleted": true }

Deleting a todo also removes its attachment metadata and stored attachment files.

Attachment Usage

GET /api/todos/attachments/usage

Returns the current user's effective attachment upload policy and usage.

Response

{
  "enabled": true,
  "usage_bytes": 1048576,
  "quota_bytes": 5368709120,
  "remaining_bytes": 5367660544,
  "allowed_types": [".png", ".jpg", ".jpeg", ".gif", ".webp", ".pdf"],
  "max_file_size_bytes": 52428800
}

List Attachments

GET /api/todos/{todo_id}/attachments

Requires read access to the todo/project.

Response

{
  "attachments": [
    {
      "id": 12,
      "todo_id": 17,
      "user_id": 1,
      "original_filename": "screenshot.png",
      "content_type": "image/png",
      "size_bytes": 18342,
      "created_at": "2026-06-17T10:00:00+00:00",
      "download_url": "/api/todos/17/attachments/12/download"
    }
  ]
}

Upload Attachment

POST /api/todos/{todo_id}/attachments

Requires write access to the todo/project. Existing attachments remain readable/downloadable when uploads are disabled, but new uploads are rejected.

Request

Response

{
  "attachment": {
    "id": 12,
    "todo_id": 17,
    "original_filename": "screenshot.png",
    "content_type": "image/png",
    "size_bytes": 18342,
    "download_url": "/api/todos/17/attachments/12/download"
  },
  "todo": { "id": 17, "attachments_count": 1 },
  "usage": { "usage_bytes": 18342, "quota_bytes": 5368709120 }
}

Download Attachment

GET /api/todos/{todo_id}/attachments/{attachment_id}/download

Requires read access to the todo/project and returns the stored file with the original filename in Content-Disposition.

Delete Attachment

DELETE /api/todos/{todo_id}/attachments/{attachment_id}

Requires write access to the todo/project. Shared/invited project members with current write access can delete attachments.

Response

{ "deleted": 12, "todo": { "id": 17, "attachments_count": 0 } }

Attachment create/delete actions emit realtime delta events (todo_attachment_create, todo_attachment_delete).

Workspaces

List

GET /api/workspaces

Response

{
  "workspaces": [
    {
      "id": 1,
      "name": "Privat",
      "color": "#10b981",
      "icon": "home",
      "sort_order": 0,
      "is_default": 1
    }
  ]
}

Notes

Create

POST /api/workspaces

Body

{ "name": "Arbeit", "color": "#6366f1", "icon": "briefcase", "sort_order": 10 }

Response

{ "id": 2, "name": "Arbeit", "color": "#6366f1", "icon": "briefcase", "is_default": 0 }

Update

PATCH /api/workspaces/{id} / PATCH /api/workspaces/{workspace_id}

Body

{ "name": "Arbeit Neu", "color": "#0ea5e9", "icon": "folder", "sort_order": 20 }

Response

{ "id": 2, "name": "Arbeit Neu", "color": "#0ea5e9", "icon": "folder" }

Delete

DELETE /api/workspaces/{id} / DELETE /api/workspaces/{workspace_id}

Response

{
  "deleted": 2,
  "moved_projects_to": 1,
  "moved_projects": []
}

Note: The default workspace cannot be deleted. When deleting, projects are moved to the default workspace; todos from the workspace inbox end up in the default inbox.

Projects

List

GET /api/projects

Response

{
  "projects": [
    {
      "id": 1,
      "name": "Inbox",
      "color": "#6366f1",
      "parent_id": null,
      "sort_order": 0,
      "is_inbox": 1,
      "is_owner": true,
      "is_shared": false,
      "owner_username": "demo",
      "owner_display_name": "Max Mustermann"
    }
  ]
}

Notes

Create

POST /api/projects

Body

{ "name": "Hobby", "color": "#ec4899", "icon": "folder", "workspace_id": 1, "sort_order": 5 }

Response

{ "id": 7, "name": "Hobby" }

Update

PATCH /api/projects/{id}

Body

{ "name": "Hobby Neu", "icon": "folder-open" }

parent_id can be set to a project ID or removed again with null:

{ "parent_id": null }

Project owners can move their own non-inbox projects to another of their workspaces:

{ "workspace_id": 2 }

When an owner moves a project, descendant projects move with it. For subtree moves the response includes an updated_projects array with the caller-specific authoritative project views so offline clients can update all affected local rows.

Shared-project members can also patch workspace_id, but only for their own display workspace. Owner moves do not change member display workspaces.

Response

{ "id": 7, "name": "Hobby Neu" }

Subtree workspace move response:

{
  "id": 7,
  "name": "Hobby Neu",
  "workspace_id": 2,
  "updated_projects": [
    { "id": 7, "name": "Hobby Neu", "workspace_id": 2 },
    { "id": 8, "name": "Child project", "parent_id": 7, "workspace_id": 2 }
  ]
}

Delete

DELETE /api/projects/{id}

Response

{ "deleted": true }

Delete Completed Todos in Project

POST /api/projects/{id}/clear-done / POST /api/projects/{project_id}/clear-done

Response

{ "deleted_count": 3 }

Project Sharing

Fetch Shared Projects

GET /api/projects/shared

Response

{
  "projects": [
    {
      "id": 5,
      "name": "Gemeinsam",
      "member_status": "accepted",
      "member_color": "#f59e0b",
      "owner_username": "demo"
    }
  ]
}

Pending Invitations

GET /api/projects/invites

Response

{
  "invites": [
    {
      "id": 12,
      "project_id": 5,
      "project_name": "Gemeinsam",
      "project_color": "#6366f1",
      "invited_by_username": "demo",
      "invited_by_display_name": "Max Mustermann",
      "status": "pending"
    }
  ]
}

Share Project

POST /api/projects/{project_id}/share

Owner-only.

Body

{ "username": "alice" }

Response

{
  "member": {
    "project_id": 5,
    "user_id": 2,
    "username": "alice",
    "display_name": "Alice Example",
    "status": "pending"
  }
}

Accept/Decline Invitation

POST /api/projects/{project_id}/invites/{invite_id}

Body

{ "accept": true }

Response

{ "id": 12, "status": "accepted", "project_id": 5 }

List Members

GET /api/projects/{project_id}/members

Owner and accepted members may see the list.

Response

{
  "members": [
    {
      "project_id": 5,
      "user_id": 2,
      "username": "alice",
      "display_name": "Alice Example",
      "status": "accepted"
    }
  ]
}

Remove Member

DELETE /api/projects/{project_id}/members/{member_user_id}

Owner can remove members; members can remove themselves. Removal is undo-capable and internally sets status=removed.

Response

{ "removed": 12, "project_id": 5 }

Restore Removed/Left Member

POST /api/projects/{project_id}/members/{member_user_id}/restore

Body

{ "status": "accepted" }

Response

{ "member": { "project_id": 5, "user_id": 2, "status": "accepted" } }

Override Member Color

PATCH /api/projects/{project_id}/members/{member_user_id}/color

Owner-only. Sets a project-specific color marker for a member.

Body

{ "color": "#f59e0b" }

Response

{ "project_id": 5, "user_id": 2, "color": "#f59e0b" }

Leave Shared Project / Undo

POST /api/projects/{project_id}/leave

Owners cannot leave their own projects.

Response

{ "left": 12, "project_id": 5 }

POST /api/projects/{project_id}/leave/undo

Response

{ "member": { "project_id": 5, "user_id": 2, "status": "accepted" } }

Sections

All Sections

GET /api/sections

Response

{ "sections": [] }

Sections of a Project

GET /api/sections/by-project/{projectId} / GET /api/sections/by-project/{project_id}

Response

{
  "sections": [
    { "id": 1, "name": "Einkauf", "project_id": 2, "sort_order": 0 }
  ]
}

Create

POST /api/sections/by-project/{projectId} / POST /api/sections/by-project/{project_id}

Body

{ "name": "Einkauf", "sort_order": 0 }

Response

{ "id": 9, "name": "Einkauf" }

Update

PATCH /api/sections/{id} / PATCH /api/sections/{section_id}

Body

{ "name": "Einkauf Neu" }

Response

{ "id": 9, "name": "Einkauf Neu" }

Delete

DELETE /api/sections/{id} / DELETE /api/sections/{section_id}

Response

{ "deleted": true }

Reminders

List

GET /api/reminders

Response

{
  "reminders": [
    {
      "id": 1,
      "todo_id": 1,
      "remind_at": "2026-05-14T09:00:00",
      "sent_at": null,
      "title": "Nia-Todo aufbauen",
      "status": "pending"
    }
  ]
}

Mark as Sent

POST /api/reminders/{id}/sent / POST /api/reminders/{reminder_id}/sent

Response

{ "ok": true }

Dashboard

Statistics

GET /api/dashboard

Response

{
  "total": 5,
  "pending": 3,
  "in_progress": 1,
  "done": 1,
  "overdue": 0,
  "due_today": 2
}

Push

Status

GET /api/push/status

VAPID Key

GET /api/push/vapid-public-key

Subscribe

POST /api/push/subscribe

Unsubscribe

POST /api/push/unsubscribe

Test

POST /api/push/test

Public Runtime/Native Endpoints

API Documentation

GET /api

Returns this API documentation as a public HTML page for users and integrations. The JSON/app API remains available under the specific /api/... endpoints.

Instance Info

GET /api/instance

Returns public instance metadata for web/native clients, including configured public base URL and passkey/WebAuthn-relevant origin information.

Native App Download Manifest

GET /downloads/app-downloads.json

Returns the available Windows, Android, and Debian desktop artifacts with version, platform, architecture, filename, size, and SHA256. Intentionally served with no-store. If no release artifacts are published, the endpoint responds with 200 and an empty apps list so clients can hide the section cleanly without 404 console noise.

GET /.well-known/assetlinks.json

Returns the pinned relationship between server and bundled Android app for native passkeys.

OIDC / SSO

OIDC support is global/admin-configured and uses the configured public_base_url as redirect base. Provider secrets are write-only in admin responses.

Status

GET /api/oidc/status

Public status for login screens. Returns whether OIDC is enabled, display provider name, and login availability without exposing provider secrets.

User Login

GET /api/oidc/login?redirect_after=/&native=false

Starts the user OIDC authorization flow. native=true marks the flow for native app completion handoff.

Admin Login

GET /api/oidc/admin/login

Starts the admin OIDC authorization flow.

Callback

GET /api/oidc/callback

OIDC redirect URI. Handles user/admin/native state, exchanges the provider code, links or signs in the matching account, and returns redirect/completion HTML.

Admin Linked Identities

Native Exchange

POST /api/oidc/native/exchange

Exchanges a native completion code for the final login payload after the browser/provider flow returns to the app.

Body

{ "code": "native-completion-code" }

Admin: OIDC Configuration

Fetch OIDC Configuration

GET /api/admin/oidc-config

Returns OIDC configuration without client_secret; secret state is exposed as client_secret_configured.

Update OIDC Configuration

PATCH /api/admin/oidc-config

Body

{
  "enabled": true,
  "provider_name": "OIDC",
  "issuer_url": "https://idp.example.com/realms/home",
  "client_id": "nia-todo",
  "client_secret": "write-only-secret",
  "public_client": false,
  "token_auth_method": "auto",
  "scopes": "openid email profile"
}

issuer_url and the computed redirect URI must use HTTPS unless loopback-only for development. token_auth_method supports auto, client_secret_basic, and client_secret_post.

Places / Location Reminders

Saved places are user-scoped helpers for location reminders. Address data stays attached to the user's saved place and is copied to linked location reminders when a place address changes.

Todo create/update payloads can include location_reminder metadata for Android-native arrival/departure reminders; BrainDump can also return location reminder candidates for saved place names.

Admin: Sessions and Statistics

BrainDump Learning

User-scoped BrainDump learning endpoints store lightweight correction preferences for future extraction.

Profile / Preferences

Additional profile/preferences endpoints:

Current Route Coverage Notes

This document is intentionally example-oriented, not a generated OpenAPI spec. During the 2026-06-14 docs audit it was checked against the FastAPI router inventory and updated for OIDC, places/location reminders, admin sessions/statistics, BrainDump learning, and newer profile routes.

Profile / Preferences

Change Own Language

PATCH /api/me/language

Body

{ "language": "de" }

Response

Returns the updated current-user payload.

Passwordless Passkey Login

These are public login endpoints for users who already have passkeys:

2FA challenge passkeys remain separate:

Admin: BrainDump Configuration

Fetch BrainDump Configuration

GET /api/admin/braindump-config

Returns the stored admin configuration without secrets. Secret fields are represented as configured booleans; API keys/tokens are never echoed back.

Update BrainDump Configuration

PATCH /api/admin/braindump-config

Body

{
  "enabled": false,
  "llm_provider": "openai_compatible",
  "llm_base_url": "https://llm.example/v1",
  "llm_api_key_secret": "***",
  "llm_model": "openclaw/default",
  "llm_extra_headers_json": "{}",
  "llm_timeout_seconds": 180,
  "system_prompt_mode": "default",
  "system_prompt_custom": "",
  "stt_provider": "whisper_cpp_remote",
  "stt_url": "http://127.0.0.1:8766/inference",
  "stt_token_secret": "***",
  "stt_language": "de",
  "stt_timeout_seconds": 60
}

Supported STT providers are whisper_cpp_remote and local_whisper_cpp. Secrets are write-only; omitted/empty secret fields preserve or clear according to the admin config service rules.

Test BrainDump Configuration

POST /api/admin/braindump-config/test

Probes the configured LLM model endpoint and STT health endpoint. If BrainDump is disabled, both probes return disabled status instead of performing network calls.

Admin: Server Updates

These endpoints support package-based self-updates for installed Debian servers. They do not participate in the development release workflow.

BrainDump v2

BrainDump requires both global admin enablement and per-user access. All user endpoints below require normal user auth and return 403 when BrainDump is disabled for the caller.

Access

GET /api/braindump/v2/access

Response

{ "enabled": true }

Live Audio Transcription

POST /api/braindump/v2/live/audio-segment/transcribe?segment_id=1&audio_start_ms=0&audio_end_ms=4000&model=base

Request body is raw audio (webm, ogg, or another browser-provided content type). The endpoint returns transcript plus timing data and does not run LLM extraction.

Live Text Extraction

POST /api/braindump/v2/live/text-segment/extract

Body

{
  "transcript": "Milch kaufen und morgen Rechnung bezahlen",
  "segment_id": 1,
  "audio_start_ms": 0,
  "audio_end_ms": 4000
}

Returns normalized candidate JSON, raw model output, token/usage metadata when available, and timing data.

Combined Live Audio Segment

POST /api/braindump/v2/live/audio-segment?segment_id=1&audio_start_ms=0&audio_end_ms=4000&model=base

Backward-compatible combined endpoint: transcribes one audio segment and immediately extracts candidates.

Create Todos from Confirmed Candidates

POST /api/braindump/v2/todos

Body

{
  "candidates": [
    {
      "title": "Milch kaufen",
      "notes": "",
      "project_name": "Einkaufsliste",
      "section_name": null,
      "deadline": null,
      "reminder": null,
      "kind": "shopping"
    }
  ]
}

Creates real todos from user-confirmed candidates and broadcasts normal todo create events.

Session Pipeline

The session pipeline is primarily for incremental text/session state and debugging; live audio production flows use the live segment endpoints.

Public Web Routes

Notes