Skip to content

Latest commit

 

History

History
3886 lines (2881 loc) · 113 KB

File metadata and controls

3886 lines (2881 loc) · 113 KB

Poznote REST API Documentation

Poznote provides a comprehensive RESTful API v1 for programmatic access to notes, folders, workspaces, tags, attachments, backups, settings, and more.

Table of Contents


Authentication

All API endpoints (except public ones) require authentication. Poznote supports HTTP Basic Authentication, OIDC Bearer JWT tokens, and the internal Bearer token used by the MCP server.

curl -u 'username:password' http://YOUR_SERVER/api/v1/notes

Use the current password of the profile you authenticate with. Default local passwords are admin for administrators and user for standard users until they are changed in the Poznote UI.

App passwords. Instead of the account password, a user can create an app password in Settings > App passwords and hand it to one client (the browser extension, the Android app, a script, an MCP server). It is sent exactly like the account password, with the account's username, over Basic auth. What makes it different is what it is not accepted for: it never opens a browser session, never reaches /api/v1/admin/*, cannot change the password, delete the account or manage app passwords, and is bound to its own profile, so X-User-ID is optional and may only name that profile. On an SSO-only instance (OIDC with Disable HTTP Basic Auth for API enabled) it is the only credential the API accepts over Basic auth. Each one can be given an expiry and revoked at any time.

curl -u 'username:pzn_2f7c…' http://YOUR_SERVER/api/v1/notes

What an app password can and cannot do:

✅ Read and write the notes, folders, tags and attachments of its own profile ❌ Open the web interface: it is refused at the login form
✅ Work when the instance is SSO-only, including with Disable HTTP Basic Auth for API enabled ❌ Reach any /api/v1/admin/* endpoint, even when the account is an administrator
✅ Be given an expiry date, and be revoked at any moment ❌ Change your password, edit or delete your account, or create further app passwords
✅ Omit the X-User-ID header: it is bound to the profile that created it ❌ Act on another profile, even for an administrator

The list in Settings > App passwords shows, for each one, the first characters of the secret (to tell them apart), when it was created, and when it was last used, which makes an unused credential easy to spot and remove. Each account can hold up to 25.

Two-factor authentication. A user can turn on two-factor authentication in Settings > Two-factor authentication: the login form then asks for a TOTP code from an authenticator app after the password. It applies to password sign-in only, an SSO login is left to the identity provider. On an account with two-factor on, the API no longer accepts the account password on its own over Basic auth, otherwise the API would be the way around the second factor. Two options remain. Give each client an app password, which works unchanged. Or send the current 6-digit code with the account password in the X-Poznote-OTP header, which is what keeps /api/v1/admin/* (closed to app passwords) reachable from a terminal:

curl -u 'username:password' -H 'X-User-ID: 1' -H 'X-Poznote-OTP: 123456' \
  http://YOUR_SERVER/api/v1/notes

Without the header the API answers 401 with an X-Poznote-OTP: required response header and a message saying so. A code sent this way stays valid for its whole window (up to 90 seconds), so a script can make several requests with it. Recovery codes are not accepted here.

OIDC Bearer JWT authentication is available when OIDC is enabled. Poznote validates the token signature with the provider JWKS, checks issuer, expiration, and audience, then maps the token claims to a Poznote profile using the same OIDC linking rules as interactive login (sub, then preferred_username, then email). Group and user allowlists, disabled profiles, and auto-create settings are also enforced.

curl -H "Authorization: Bearer $OIDC_ACCESS_TOKEN" -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes

For OIDC Bearer JWT requests, data endpoints default to the profile linked to the token subject when X-User-ID is omitted. Admin JWTs may still include X-User-ID to access another profile.

By default, the accepted JWT aud claim is the configured OIDC Client ID. If your identity provider issues API access tokens with a dedicated audience, set API JWT audience in Settings > Admin Tools > OIDC / SSO. Multiple accepted audiences can be separated with commas.

Authentication Levels

Level Description Used by
No auth No credentials needed GET /api/v1/users/profiles, GET /api_health.php, Public tasks
User auth Valid credentials, no X-User-ID needed /api/v1/users/me, /api/v1/system/*, /api/v1/shared/*
Data auth Valid credentials; X-User-ID required for Basic/service token, optional for OIDC JWT and app passwords (own profile) All user data endpoints (notes, folders, tags, etc.)
Admin auth Admin credentials, no X-User-ID needed /api/v1/admin/*, /api/v1/users/lookup/*

Multi-User Mode

Poznote supports multiple user profiles, each with their own isolated data. For API calls that access user data (notes, folders, workspaces, tags, attachments, backups, settings, etc.), Basic Auth with an account password and internal service-token requests must include the X-User-ID header. OIDC Bearer JWT requests use the token-linked profile by default and only need X-User-ID when an admin token targets another profile. App passwords always act on the profile they belong to.

Account-access grants configured in the web admin UI only apply to interactive browser sessions after login account selection. They do not allow non-admin API credentials to use X-User-ID for another profile. API access to another user's data requires administrator credentials or the internal service token.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes

Endpoints that do NOT require the X-User-ID header:

  • Admin endpoints: /api/v1/admin/*
  • Public endpoints: /api/v1/users/profiles
  • User profile endpoints: /api/v1/users/me, /api/v1/users/me/password, /api/v1/users/me/password-status, /api/v1/users/me/app-passwords, /api/v1/users/me/two-factor
  • System endpoints: /api/v1/system/* (version, updates, i18n)
  • Shared endpoints: /api/v1/shared, /api/v1/shared/with-me

Use GET /api/v1/users/profiles to list available user profiles and their IDs.


Base URL

/api/v1

All endpoints in this document are relative to this base URL unless otherwise noted (legacy endpoints use full paths).


Response Format

All endpoints return JSON. Successful responses typically follow this structure:

{
  "success": true,
  "data": { ... }
}

Error responses:

{
  "success": false,
  "error": "Error description"
}

HTTP Status Codes

Code Description
200 OK – Request succeeded
201 Created – Resource created successfully
204 No Content – CORS preflight
400 Bad Request – Invalid parameters
401 Unauthorized – Missing or invalid credentials
403 Forbidden – Insufficient permissions
404 Not Found – Resource does not exist
405 Method Not Allowed
409 Conflict – Resource already exists
413 Payload Too Large
500 Internal Server Error

Interactive Documentation (Swagger)

Access the Swagger UI directly from Poznote at Settings > API Documentation to browse all endpoints, view request/response schemas, and test API calls interactively.


Notes

List Notes

GET /notes

List all notes for a user with optional filtering and sorting.

Query Parameters:

Parameter Type Description
workspace string Filter by workspace name
folder string Filter by folder name
folder_id integer Filter by folder ID
search string Search in heading and content
created_from date Filter notes created on or after this date (YYYY-MM-DD)
created_to date Filter notes created on or before this date (YYYY-MM-DD)
favorite boolean Filter favorites only
sort string Sort order: updated_desc, created_desc, heading_asc, type_asc, manual; defaults to the account's own setting
get_folders boolean Include folder information
limit integer Page size, 1 to 1000. Omit to get every matching note
offset integer Number of matching notes to skip, for the next page
curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes

The response carries count, the number of notes in this answer, and total, the number of notes the filters match, with offset, limit and has_more. A page is therefore never mistaken for the whole list: while has_more is true, ask again with offset raised by the page size.

curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/notes?workspace=Personal&limit=50&offset=50"

Filter notes by workspace and folder:

curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/notes?workspace=Personal&folder=Projects"

Filter notes by creation date:

curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/notes?created_from=2026-01-01&created_to=2026-01-31"

List Notes with Attachments

GET /notes/with-attachments

List all notes that have file attachments.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/with-attachments

List Templates

GET /notes/templates

List the notes the /template slash command can insert. A template is an ordinary HTML or Markdown note kept in a folder named Templates (sub-folders included) or in a workspace named Templates. The folder or workspace name is matched case-insensitively, in English or in the language of the interface (Modèles, Vorlagen, Plantillas, Modelos, Шаблоны, 模板).

Query Parameters:

  • workspace (optional): Only look at the Templates folders of this workspace. Notes of a Templates workspace are always included.
curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/notes/templates?workspace=Poznote"

Response:

{
  "success": true,
  "notes": [
    {
      "id": 12,
      "heading": "Meeting notes",
      "type": "note",
      "workspace": "Poznote",
      "folder_id": 3,
      "icon": "lucide-file-text",
      "icon_color": null
    }
  ],
  "count": 1
}

Fetch the content of a template with GET /notes/{id}.

Get Note

GET /notes/{id}

Get a specific note by ID, including its content.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123

Resolve Note by Reference

GET /notes/resolve

Resolve a note by ID or title (reference). The response includes the note's id, heading and workspace.

Query Parameters:

Parameter Type Description
reference string Note ID, or title to search for
workspace string Workspace to search in (omit to search every workspace)
curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/notes/resolve?reference=My+Note&workspace=Personal"

Search Notes

GET /notes/search

Search notes by heading or content.

Query Parameters:

Parameter Type Description
q string Search query (required)
workspace string Restrict search to a workspace
limit integer Max results (1–100, default 10)
created_from date Filter notes created on or after this date (YYYY-MM-DD)
created_to date Filter notes created on or before this date (YYYY-MM-DD)
curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/notes/search?q=docker&created_from=2026-01-01&created_to=2026-01-31"

Create Note

POST /notes

Create a new note with title, content, tags, folder and workspace.

Request Body (JSON):

Field Type Required Description
heading string Yes Note title
content string No Note content (HTML or Markdown)
entry string No Alternative field for content
tags string No Comma-separated tags
folder_id integer No Target folder ID
folder string No Target folder, as a name or a path such as Projects/2026/Q3, whose missing levels are created. A bare name matches an existing folder at any depth when only one folder of the workspace carries it; see below when several do. folder_name is accepted as an older spelling
workspace string No Target workspace
type string No Note type: note (HTML), markdown, tasklist
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{
    "heading": "My New Note",
    "content": "This is the content of my note",
    "tags": "work,important",
    "folder_id": 12,
    "workspace": "Personal",
    "type": "markdown"
  }' \
  http://YOUR_SERVER/api/v1/notes

Ambiguous folder names: a name is not an address when several folders of the workspace carry it, for instance 08 in both Diary/2026/08 and Archive/2025/08. Rather than guess, or create a third one at the root, the request is refused with 409 and the candidates, so the caller can retry with a full path or a folder_id. A root folder of that name, when there is one, is still used directly.

{
  "success": false,
  "error": "Several folders are named \"08\" in this workspace. Pass the full path, or folder_id.",
  "code": "ambiguous_folder_name",
  "candidates": [
    {"id": 41, "path": "Archive/2025/08"},
    {"id": 57, "path": "Diary/2026/08"}
  ]
}

Update Note

PATCH /notes/{id}

Update an existing note by ID. Only include fields you want to modify.

Request Body (JSON):

Field Type Description
heading string Updated title
content string Updated content
tags string Updated comma-separated tags
folder_id integer Move to folder. When the same request changes the workspace, it must be a folder of the destination (400 otherwise)
folder string Move to folder by name or path, resolved like on Create Note (ambiguous names included). An empty string moves the note to the workspace root; folder_id wins when both are sent
workspace string Move to workspace, keeping the note's id and history. Without a folder of the destination in the same request, the note lands at that workspace's root, since its current folder belongs to the workspace it leaves. Leave it out of ordinary saves: sending the workspace you happen to have selected moves the note there
git_push boolean Trigger Git sync after update
if_version string Optimistic concurrency token (see below)
state_hash string Fingerprint of the editor state being saved, used by the web editor's draft recovery. GET /notes/{id} returns it as state_hash until the note is written again by anyone, null otherwise. Other API clients can leave it out.
curl -X PATCH -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{
    "heading": "Updated Title",
    "content": "Updated content here",
    "tags": "work,updated"
  }' \
  http://YOUR_SERVER/api/v1/notes/123

Optimistic concurrency:

GET /notes/{id} returns a version token (also sent as an ETag header). Pass it back in the PATCH body as if_version, or as an If-Match header. If the note was modified since that version, the write is rejected with a 409 response that includes the current version, updated, heading and content, so you can merge your change into the latest content and retry in a single round trip. Without if_version, writes behave as before (last write wins). A successful PATCH returns the new version token, which lets you chain conditional writes without re-reading the note.

curl -X PATCH -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Updated content here",
    "if_version": "9b74c9897bac770ffc029102a200c5de"
  }' \
  http://YOUR_SERVER/api/v1/notes/123

Edit locks:

The web editor takes the note's edit lock as soon as the note is opened (see Note Locks). A write without an editor_session_id (the MCP server, a script using your credentials) is not blocked by your own lock: the open tab picks the change up within a few seconds, and its autosave sends if_version, so it cannot overwrite yours. The write is rejected with 423 Locked only while another user of the account, or a visitor on a public share link, is editing the note; the response names the holder in error and carries the lock details in lock.

Delete Note

DELETE /notes/{id}

Move a note to trash (soft delete by default).

Query Parameters:

Parameter Type Description
permanent boolean If true, permanently delete (bypass trash)

Move to trash:

curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123

Permanently delete:

curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/notes/123?permanent=true"

Restore Note

POST /notes/{id}/restore

Restore a note from trash.

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/restore

Duplicate Note

POST /notes/{id}/duplicate

Create a copy of an existing note. Without a body the copy lands next to the original.

Body (optional):

  • folder_id: Folder for the copy ("" or 0 for the root)
  • workspace: Workspace the target folder belongs to
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/duplicate

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"folder_id": 12, "workspace": "Poznote"}' \
  http://YOUR_SERVER/api/v1/notes/123/duplicate

Convert Note Type

POST /notes/{id}/convert

Convert a note between Markdown and HTML formats.

Request Body (JSON):

Field Type Required Description
target string Yes Target format: html or markdown
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"target": "markdown"}' \
  http://YOUR_SERVER/api/v1/notes/123/convert

Update Tags

PUT /notes/{id}/tags

Replace all tags on a note.

Request Body (JSON):

Field Type Description
tags string Comma-separated tag list
curl -X PUT -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"tags": "work,urgent,meeting"}' \
  http://YOUR_SERVER/api/v1/notes/123/tags

Update Note Icon

PUT /notes/{id}/icon

Set a custom icon and color for a note. Send an empty icon to reset to the default.

Request Body (JSON):

Field Type Description
icon string Icon class name (e.g. lucide-file-text)
icon_color string Icon color (e.g. #ff9800), empty to reset
curl -X PUT -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"icon": "lucide-file-text", "icon_color": "#ff9800"}' \
  http://YOUR_SERVER/api/v1/notes/123/icon

Set Note Color

PUT /notes/{id}/color

Set the color used to tint the note card on the dashboard. Send an empty color to remove it.

Request Body (JSON):

Field Type Description
color string Palette id (e.g. blue) or custom hex (e.g. #8bc34a), empty to remove

Palette ids come from the user's note color palette, configurable in Settings. Notes storing a palette id follow the palette when it is edited; a custom hex is stored as-is. Note objects returned by the API expose both color (the stored value) and color_hex (the resolved color, null when the note has no color or its palette entry was deleted).

curl -X PUT -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"color": "blue"}' \
  http://YOUR_SERVER/api/v1/notes/123/color

Set Note Pinned State

PUT /notes/{id}/pinned

Pin or unpin a note, which keeps it at the top of the note list.

Request Body (JSON):

Field Type Required Description
pinned boolean Yes true to pin the note, false to unpin it

Response:

{
  "success": true,
  "message": "Note pinned state updated successfully",
  "pinned": true
}
curl -X PUT -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"pinned": true}' \
  http://YOUR_SERVER/api/v1/notes/123/pinned

Set Note Content Width

PUT /notes/{id}/content-width

Override the content width for one note. Notes without an override follow the global "Note content width" setting.

Request Body (JSON):

Field Type Required Description
content_width integer or null Yes Maximum content width as a percentage of the note column (10 to 100), 100 for full width, null to follow the global setting again

Response:

{
  "success": true,
  "message": "Note content width updated successfully",
  "content_width": 60
}
curl -X PUT -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"content_width": 60}' \
  http://YOUR_SERVER/api/v1/notes/123/content-width

Set Kanban Completed State

POST /notes/{id}/kanban-completed

Mark a note as completed on the kanban board, or clear that state. Only the completion flag changes: the note's content and its updated timestamp are left untouched.

Request Body (JSON):

Field Type Required Description
completed boolean Yes true to mark the note completed, false to reopen it
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"completed": true}' \
  http://YOUR_SERVER/api/v1/notes/123/kanban-completed

Toggle Favorite

POST /notes/{id}/favorite

Toggle favorite status for a note.

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/favorite

Keep Note Offline

PUT /notes/{id}/offline

Keep the note in the browsers where the account is used whatever its date, with all its attachments (Offline Copies), or stop doing so. The state is set explicitly.

Request Body (JSON):

Field Type Required Description
offline boolean Yes true to keep the note offline, false to stop

Response:

{
  "success": true,
  "message": "Note offline state updated successfully",
  "offline": true
}
curl -X PUT -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"offline": true}' \
  http://YOUR_SERVER/api/v1/notes/123/offline

Move Note to Folder

POST /notes/{id}/folder

Move a note to a different folder.

Request Body (JSON):

Field Type Description
folder_id integer Target folder ID
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"folder_id": 45}' \
  http://YOUR_SERVER/api/v1/notes/123/folder

Remove Note from Folder

POST /notes/{id}/remove-folder

Remove a note from its folder (move to root).

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/remove-folder

Reorder Notes

POST /notes/reorder

Move a note before or after another note (drag-and-drop order). The note lands in the target note's folder (or at the root when the target has none), every note there is renumbered, and the tree switches to the manual sort mode ("Custom" in the interface) so the position sticks. The saved positions are never erased, so leaving that mode and coming back restores the arrangement.

With "scope": "dashboard" the request reorders the note's card on the dashboard instead: the rank is written to the note's dashboard_order (a column of its own, the sidebar's display_order is untouched), the target must be in the same folder, and neither the tree's sort mode nor the note's updated date change. The dashboard shows pinned cards first, then placed cards in saved order, with cards that were never placed ahead of them by newest update.

Request Body (JSON):

Field Type Required Description
note_id integer Yes Note to move
target_note_id integer Yes Note used as anchor (same workspace)
position string Yes before or after the target note
workspace string No Workspace check (must match target note)
scope string No sidebar (default) or dashboard, see above
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"note_id": 123, "target_note_id": 456, "position": "after"}' \
  http://YOUR_SERVER/api/v1/notes/reorder

Archive Note

POST /notes/{id}/archive

Move a note to the Archives workspace. The workspace is created on first use, and the note's folder path is mirrored inside it so the note keeps its place in the tree. The source folders are left untouched and the note's updated date is preserved. Returns 409 when a note with the same title is already archived in the target folder.

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/archive

Emergency Save (Beacon)

POST /notes/{id}/beacon

Emergency save via sendBeacon API. Accepts FormData instead of JSON. Used internally by the browser when navigating away or closing the page.


Note Locks

Exclusive edit locks prevent two editors from modifying the same note simultaneously. All lock endpoints identify the editor with an editor_session_id, sent either in the JSON body, as an editor_session_id form field, or via the X-Editor-Session-ID header. When the note is already locked by another user, lock endpoints respond with 423 Locked and the current lock details. A user's own sessions never block each other: opening the note in a second tab takes the lock over.

Acquire Lock

POST /notes/{id}/lock

Acquire an exclusive edit lock for a note.

Request Body (JSON):

Field Type Required Description
editor_session_id string Yes Unique ID of the editing session
takeover boolean No Take the lock from its current holder (another user of the account, someone the workspace is shared with, or a visitor on a public share link) instead of getting 423 Locked. The previous holder's next heartbeat fails and their editor turns read-only; the edits they had not saved stay in their local draft. The response then carries "taken_over": true.
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"editor_session_id": "session-abc123"}' \
  http://YOUR_SERVER/api/v1/notes/123/lock

Get Lock Status

GET /notes/{id}/lock

Get the current edit lock status for a note.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/lock

Refresh Lock (Heartbeat)

POST /notes/{id}/lock/heartbeat

Refresh an existing edit lock to keep it alive.

Request Body (JSON):

Field Type Required Description
editor_session_id string Yes Editing session that holds the lock
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"editor_session_id": "session-abc123"}' \
  http://YOUR_SERVER/api/v1/notes/123/lock/heartbeat

Release Lock

POST /notes/{id}/lock/release

Release a note edit lock.

Request Body (JSON):

Field Type Required Description
editor_session_id string Yes Editing session that holds the lock
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"editor_session_id": "session-abc123"}' \
  http://YOUR_SERVER/api/v1/notes/123/lock/release

Change Detection

The web UI polls this endpoint to notice changes made outside the current tab (AI chat, MCP server, REST API, another tab or user) and refresh the sidebar or the open note in place. It returns opaque tokens that only change when the underlying data changes; compare them with the values from a previous call.

GET /changes?workspace={name}&note_ids={id,id,...}

Query Parameters:

Parameter Type Required Description
workspace string No Workspace whose sidebar tree is hashed (default: Poznote)
note_ids string No Comma-separated note IDs to get a version token for (max 20)
curl -u 'username:password' -H "X-User-ID: 1" \
  "https://your-poznote-instance.com/api/v1/changes?workspace=Poznote&note_ids=42,57"

Response:

{
  "success": true,
  "workspace": "Poznote",
  "tree_version": "3ee64a2b6a43cbfb8815cc864d2ae957",
  "notes": {
    "42": { "version": "8b03f2cfdfd041732b3534f0cddbb3e2", "content_version": "2a8d3b8d4e8ad894cb2872ef143824ee", "exists": true, "trash": false },
    "57": { "version": "missing", "content_version": null, "exists": false, "trash": false }
  }
}

tree_version covers the workspace list, the folders and the note rows of the workspace (titles, folder, icons, order, tags, update time, trash state...). A note version covers its title, content, tags, folder, attachments and display attributes. content_version is the same token GET /notes/{id} and PATCH /notes/{id} return, so it can be sent back as if_version (see Update Note); the web UI autosave does exactly that, so a save never silently overwrites an edit made elsewhere.

Offline Copies

The web UI keeps the notes modified recently in the browser (IndexedDB) so they can be opened and edited without a network, and sends the changes made offline back through Update Note and Create Note once the connection is back. These two endpoints feed that copy. How many days of notes are kept is the offline_notes_days user setting (default 5, 0 turns offline copies off, maximum 30). The favorites, and the notes and folders marked "Keep offline" (Keep Note Offline, Keep Folder Offline), are kept whatever their date, with all their attachments. Only the account's owner gets them: an account opened through a grant or a workspace shared with the session answers 403 with "code": "offline_unavailable".

Get Offline Manifest

GET /offline/manifest

Returns the notes kept offline, without their content: the ones modified in the last days days, the favorites and the notes marked "Keep offline" or sitting in a folder marked so, of type note, markdown or tasklist, within limits (notes notes and text_mb MB of text, the recent ones dropped first; the browser applies the file limits picture_mb, pictures and pictures_mb to the images these notes show and to every attachment of the notes kept whatever their date), with the same version token as Get Note, usable as if_version. kept says why each note is there: note, folder, favorite or recent. kept_folders lists the ids of the folders kept whole (marked "Keep offline", or under one). files changes whenever the note's list of attachments does (the version token only follows the date, title and content): a client compares both to know when to download the note again. folders holds only the folders of those notes and their parents. The response has an ETag: send it back as If-None-Match to get 304 Not Modified while nothing changed.

curl -u 'username:password' -H "X-User-ID: 1" \
  "https://your-poznote-instance.com/api/v1/offline/manifest"

Response:

{
  "success": true,
  "user": { "id": 1, "username": "alice", "email": "alice@example.com", "display_name": "Alice" },
  "days": 5,
  "limits": { "notes": 300, "text_mb": 50, "picture_mb": 25, "pictures": 400, "pictures_mb": 200 },
  "workspaces": ["Poznote"],
  "folders": [{ "id": 3, "name": "Lectures", "parent_id": null, "workspace": "Poznote" }],
  "notes": [
    { "id": 42, "heading": "Physics", "type": "markdown", "workspace": "Poznote", "folder_id": 3, "updated": "2026-09-23 14:02:11", "kept": "recent", "version": "8b03f2cfdfd041732b3534f0cddbb3e2", "files": "" }
  ]
}

Get Offline Notes

GET /offline/notes?ids={id,id,...}

Full content of up to 50 notes (not trashed), with their version token and the id, name, type and size of their attachments.

curl -u 'username:password' -H "X-User-ID: 1" \
  "https://your-poznote-instance.com/api/v1/offline/notes?ids=42,57"

Response:

{
  "success": true,
  "notes": [
    {
      "id": 42,
      "heading": "Physics",
      "type": "markdown",
      "workspace": "Poznote",
      "folder_id": 3,
      "tags": "school",
      "updated": "2026-09-23 14:02:11",
      "version": "8b03f2cfdfd041732b3534f0cddbb3e2",
      "attachments": [{ "id": "69f45110e619f", "file_type": "image/png", "file_size": 350386 }],
      "content": "# Physics\n..."
    }
  ]
}

Snapshots

Snapshots preserve daily versions of a note's content. One automatic snapshot is taken per day; the most recent automatic snapshots are kept per note, 3 by default (user setting snapshots_keep_count, 1 to 30, also available from Settings > Snapshots). Manual snapshots can be added on demand without limit and do not count toward that number. Every snapshot, automatic or manual, expires 30 days after it was taken.

A manual snapshot is also taken automatically right before the built-in AI assistant or the MCP server (requests authenticated with the MCP service token) changes the content or tasks of a note, unless the note is empty or the newest snapshot already holds the same content; the 20 most recent of them are kept per note, a number the snapshots_safety_keep_count setting changes (1 to 200). Such snapshots carry an origin field in the list and get responses, "ai" or "mcp"; user-made snapshots have an empty origin.

An attachment or image deleted from a note is kept on disk (hidden from the note) as long as a snapshot still references it, so restoring that snapshot brings it back. The file is removed for good once no snapshot references it any more (the last one expired or was purged), or when the note is permanently deleted.

Create Snapshot

POST /notes/{id}/snapshot

Create a snapshot for a note. Without parameters, creates/updates today's automatic snapshot.

Query Parameters:

Parameter Type Description
manual boolean If 1, create an additional manual snapshot (alias: force)
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/notes/123/snapshot?manual=1"

List Snapshots

GET /notes/{id}/snapshots

List available snapshots for a note.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/snapshots

Get Snapshot

GET /notes/{id}/snapshot

Get a snapshot's content.

Query Parameters:

Parameter Type Description
snapshot_key string Snapshot key from the list endpoint
date date Snapshot date (YYYY-MM-DD, defaults to today)
curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/notes/123/snapshot?date=2026-07-01"

Restore Snapshot

POST /notes/{id}/snapshot/restore

Restore a note to a snapshot state.

Query Parameters:

Parameter Type Description
snapshot_key string Snapshot key from the list endpoint
date date Snapshot date (YYYY-MM-DD, defaults to today)
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/notes/123/snapshot/restore?date=2026-07-01"

Delete Snapshot

DELETE /notes/{id}/snapshot

Delete one snapshot, manual or automatic. Attachments and images that only this snapshot still kept are deleted with it.

Query Parameters:

Parameter Type Description
snapshot_key string Snapshot key from the list endpoint
date date Snapshot date (YYYY-MM-DD), used when snapshot_key is omitted
curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/notes/123/snapshot?snapshot_key=2026-07-01--143022123-a1b2"

Tasks

Task list notes (type: tasklist) store their content as a JSON array of task objects. The array is the note's content: reading a tasklist note through GET /notes/{id} returns it as a JSON string, and rewriting the whole list means sending the full modified array back through PATCH /notes/{id}.

To manage a single task without rewriting the array, use the per-task endpoints below (GET/POST /notes/{id}/tasks, PATCH/DELETE /notes/{id}/tasks/{taskId}). They take due dates, reminders and flags as typed parameters, keep the notification scheduled for a task in sync automatically, and preserve the ordering the interface uses (important first, then normal, completed last).

Task object schema:

Field Type Description
id number Unique task identifier inside the note
text string Task text
completed boolean Whether the task is done
important boolean Important flag, sorts the task to the top of its list
dueAt string or null Due date as YYYY-MM-DD, or YYYY-MM-DDTHH:MM when a time is set (local time, no timezone)
dueReminder boolean Whether a reminder is scheduled for the due date
dueReminderEmail boolean Whether that reminder also sends an email. Only present once configured; defaults to enabled otherwise
dueRecurrence string Repeat interval of the reminder as <count><unit> with unit i/h/d/w/m/y (e.g. 1w weekly). Only present when set. Dismissing the notification schedules the next one and advances dueAt by the same interval

List All Tasks

GET /tasks

Aggregate the tasks of every non-trashed tasklist note, used by the tasks page (list and calendar views).

Query Parameters:

Parameter Type Description
workspace string Filter by workspace

Response:

{
  "success": true,
  "notes": [
    {
      "id": 123,
      "heading": "Groceries",
      "folder": "Home",
      "folder_id": 4,
      "workspace": "Poznote",
      "updated": "2026-08-10 09:12:00",
      "favorite": false,
      "tasks": [
        { "id": 1754820000123, "text": "Buy milk", "completed": false, "important": false, "dueAt": "2026-08-15", "dueReminder": false }
      ]
    }
  ]
}
curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/tasks?workspace=Poznote"

List Tasks Of A Note

GET /notes/{id}/tasks

List the tasks of one tasklist note. Use it to get a task's id before updating or deleting it.

Response:

{
  "success": true,
  "note_id": 123,
  "heading": "Groceries",
  "tasks": [
    { "id": 1754820000123.45, "text": "Buy milk", "completed": false, "important": false, "dueAt": "2026-08-15", "dueReminder": false }
  ]
}
curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/tasks

Add A Task

POST /notes/{id}/tasks

Append a task to a tasklist note. When reminder is enabled, the matching notification is scheduled automatically from due_at.

Request Body (JSON):

Field Type Required Description
text string Yes Task text
due_at string No Due date as YYYY-MM-DD, or YYYY-MM-DDTHH:MM with a time. Local wall-clock time in the user's configured timezone, no offset
reminder boolean No Whether the due date raises a notification. Requires due_at. A date without a time reminds at 09:00
reminder_email boolean No Whether that reminder also sends an email. Ignored when SMTP is not configured
recurrence string No Repeat interval as <count><unit> with unit i/h/d/w/m/y (e.g. 1w)
important boolean No Important flag, sorts the task to the top
completed boolean No Whether the task starts out done

Response:

{
  "success": true,
  "note_id": 123,
  "task": {
    "id": 1754820000123.45,
    "text": "Buy milk",
    "noteId": 123,
    "completed": false,
    "important": false,
    "dueAt": "2026-09-01T18:30",
    "dueReminder": true,
    "dueRecurrence": "1w"
  }
}
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Buy milk",
    "due_at": "2026-09-01T18:30",
    "reminder": true,
    "recurrence": "1w"
  }' \
  http://YOUR_SERVER/api/v1/notes/123/tasks

Update A Task

PATCH /notes/{id}/tasks/{taskId}

Update one task. Only the provided fields change. Completing a task clears its pending reminder, and setting due_at to null clears the due date and its reminder.

Request Body (JSON):

Field Type Required Description
text string No New task text
completed boolean No Whether the task is done
important boolean No Important flag
due_at string or null No New due date, or null to clear it
reminder boolean No Whether the due date raises a notification
reminder_email boolean No Whether that reminder also sends an email
recurrence string or null No Repeat interval, or null for a one-off reminder
curl -X PATCH -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"completed": true}' \
  http://YOUR_SERVER/api/v1/notes/123/tasks/1754820000123.45

Delete A Task

DELETE /notes/{id}/tasks/{taskId}

Remove one task from a tasklist note, along with its pending reminder.

curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/tasks/1754820000123.45

Reminders

Reminders schedule notifications for notes. Triggered reminders appear as notifications that can be read or dismissed.

Get Note Reminder

GET /notes/{id}/reminder

Get the reminder set on a note.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/reminder

Set Note Reminder

POST /notes/{id}/reminder

Set (or replace) a reminder on a note.

Request Body (JSON):

Field Type Required Description
reminder_at datetime Yes When to trigger the reminder (ISO 8601)
message string No Optional reminder message
email_enabled boolean No Also send an email (if email is configured)
recurrence string No Repeat interval as <count><unit> with unit i/h/d/w/m/y for minute/hour/day/week/month/year (e.g. 30i every 30 minutes, 1h hourly, 2w every 2 weeks). Dismissing the notification schedules the next occurrence.
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"reminder_at": "2026-07-10T09:00:00Z", "message": "Review this note", "recurrence": "1w"}' \
  http://YOUR_SERVER/api/v1/notes/123/reminder

Remove Note Reminder

DELETE /notes/{id}/reminder

Remove the reminder from a note.

curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/reminder

Set Task Reminder

POST /notes/{id}/task-reminder

Set (or replace) the reminder of one task inside a tasklist note. This materializes the task's dueReminder flag as a scheduled notification; the task's dueAt/dueReminder fields themselves live in the note content (see Tasks).

Request Body (JSON):

Field Type Required Description
task_id string Yes The id of the task inside the note's task JSON
reminder_at datetime Yes When to trigger the reminder (ISO 8601, converted to UTC)
message string No Notification text, typically the task text (defaults to the note heading)
email_enabled boolean No Also send an email (if email is configured)
recurrence string No Repeat interval as <count><unit> with unit i/h/d/w/m/y, like note reminders. Dismissing the triggered notification schedules the next occurrence and advances the task's dueAt by the same interval (a completed or deleted task ends the recurrence)
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"task_id": "1754820000123", "reminder_at": "2026-08-15T09:00:00Z", "message": "Buy milk"}' \
  http://YOUR_SERVER/api/v1/notes/123/task-reminder

Remove Task Reminder

DELETE /notes/{id}/task-reminder

Remove the pending reminder of one task. task_id can be sent in the JSON body or as a query parameter.

curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"task_id": "1754820000123"}' \
  http://YOUR_SERVER/api/v1/notes/123/task-reminder

List Notifications

GET /reminders

List triggered notifications (most recent first, max 50).

Query Parameters:

Parameter Type Description
workspace string Filter by workspace
curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/reminders

Get Notification Count

GET /reminders/count

Get unread notification counters (lightweight polling endpoint).

Query Parameters:

Parameter Type Description
workspace string Filter by workspace
curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/reminders/count

Mark Notification as Read

POST /reminders/{id}/read

Mark a notification as read.

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/reminders/45/read

Dismiss Notification

POST /reminders/{id}/dismiss

Dismiss a notification.

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/reminders/45/dismiss

Dismiss All Notifications

POST /reminders/dismiss-all

Dismiss all triggered notifications.

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/reminders/dismiss-all

Note Sharing

Get Share Status

GET /notes/{id}/share

Check if a note is shared and get share details.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/share

Create Share Link

POST /notes/{id}/share

Create a public share link for a note.

Request Body (JSON):

Field Type Description
theme string Display theme: light, dark, or black
indexable boolean Allow search engine indexing
password string Optional password protection
custom_token string Custom URL token (slug)
access_mode string Access mode. Tasklists: read_only, check_only, full (default full). HTML/markdown notes: read_only, edit (default read_only; edit lets visitors modify the note text)
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{
    "theme": "light",
    "indexable": false,
    "password": "optional-password"
  }' \
  http://YOUR_SERVER/api/v1/notes/123/share

Update Share Settings

PATCH /notes/{id}/share

Update share settings on an existing share.

Request Body (JSON):

Field Type Description
indexable boolean Allow indexing
password string Password protection (empty to remove)
custom_token string Custom URL token
access_mode string Access mode. Tasklists: read_only, check_only, full (default full). HTML/markdown notes: read_only, edit (default read_only; edit lets visitors modify the note text)
allowed_users array User IDs with access
curl -X PATCH -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"indexable": true, "password": "new-password"}' \
  http://YOUR_SERVER/api/v1/notes/123/share

Revoke Share Link

DELETE /notes/{id}/share

Remove sharing access for a note.

curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/share

List All Shared Notes

GET /shared

Get list of all shared notes and folders. Does not require the X-User-ID header.

Query Parameters:

Parameter Type Description
workspace string Filter by workspace
curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/shared

List Items Shared With Me

GET /shared/with-me

List notes and folders shared with the current user by other users.

curl -u 'username:password' \
  http://YOUR_SERVER/api/v1/shared/with-me

Folder Sharing

Get Folder Share Status

GET /folders/{id}/share

Check if a folder is shared.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/folders/5/share

Create Folder Share Link

POST /folders/{id}/share

Share a folder. All notes in the folder will also be shared.

Request Body (JSON):

Field Type Description
theme string Display theme: light, dark, or black
indexable integer Allow indexing (0 or 1)
password string Optional password protection
custom_token string Custom URL slug
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{
    "theme": "light",
    "indexable": 0,
    "password": "optional-password"
  }' \
  http://YOUR_SERVER/api/v1/folders/5/share

With custom token:

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"custom_token": "my-shared-folder"}' \
  http://YOUR_SERVER/api/v1/folders/5/share

Update Folder Share Settings

PATCH /folders/{id}/share

Request Body (JSON):

Field Type Description
indexable integer Allow indexing
password string Password protection
custom_token string Custom token
allowed_users array User IDs with access
curl -X PATCH -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"indexable": 1, "password": "new-password"}' \
  http://YOUR_SERVER/api/v1/folders/5/share

Revoke Folder Share Link

DELETE /folders/{id}/share

Revoke folder sharing. All notes in the folder will also be unshared.

curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/folders/5/share

Backlinks & Graph

Get Backlinks

GET /notes/{id}/backlinks

Get all notes that link to this note. Supports HTML links, URL parameters, and wiki-link syntax [[Note Title]]. Each backlink includes its id, heading and workspace. A [[Note Title]] only counts from notes of the same workspace, since wiki links resolve within their own workspace.

Query Parameters:

Parameter Type Description
workspace string Workspace of the note, also limits the linking notes to that workspace
all_workspaces string 1 to collect linking notes from every workspace while workspace still scopes the note itself
curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/backlinks

Get Note Graph

GET /graph

Return the note-link graph used by the graph view: one node per non-trashed note (note, markdown and tasklist types), and one edge per link between two notes.

Query Parameters:

Parameter Type Description
workspace string Filter by workspace

Response:

{
  "success": true,
  "nodes": [
    { "id": 123, "title": "Project plan", "folder": "Work", "type": "note", "favorite": false }
  ],
  "edges": [
    { "source": 123, "target": 456 }
  ]
}
curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/graph?workspace=Personal"

Folders

List Folders

GET /folders

List all folders in a workspace.

Query Parameters:

Parameter Type Description
workspace string Workspace whose folders to list. When omitted, the workspace that sorts first is used, which changes as workspaces are added: always pass it from a script
tree boolean Return hierarchical tree structure
curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/folders?workspace=Personal"

Get folder tree (nested structure):

curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/folders?workspace=Personal&tree=true"

Each folder carries its path and is_diary, true for the diary roots of the workspace.

Get Folder

GET /folders/{id}

Get details of a specific folder.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/folders/12

Get Folder Counts

GET /folders/counts

Get note counts for all folders.

Query Parameters:

Parameter Type Description
workspace string Filter by workspace
curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/folders/counts?workspace=Personal"

Get Suggested Folders

GET /folders/suggested

Get a list of suggested folders based on usage patterns.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/folders/suggested

Get Folder Path

GET /folders/{id}/path

Get the full breadcrumb path for a folder.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/folders/12/path

Get Note Count in Folder

GET /folders/{id}/notes

Get the number of notes in a folder (recursive).

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/folders/12/notes

Create Folder

POST /folders

Create a new folder.

Request Body (JSON):

Field Type Required Description
name string Yes Folder name
workspace string No Target workspace
parent_id integer No Parent folder ID (for subfolders)
folder_path string No Create a folder by path instead of by name, e.g. Projects/2026/Q3. Replaces name
create_parents boolean No With folder_path, create the missing levels on the way down instead of failing with 404 and the missing_segment
is_diary boolean No Create the folder as a diary, the root folder the "New diary entry" button files its dated notes into. A diary is always at the root: refused (400) with a parent. When a root folder of that name already exists, it becomes the diary and keeps its notes (200, "converted": true)
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Projects",
    "workspace": "Personal"
  }' \
  http://YOUR_SERVER/api/v1/folders

Create a subfolder:

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "2024",
    "workspace": "Personal",
    "parent_id": 12
  }' \
  http://YOUR_SERVER/api/v1/folders

Create a nested folder in one call:

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"folder_path": "Projects/2026/Q3", "create_parents": true, "workspace": "Personal"}' \
  http://YOUR_SERVER/api/v1/folders

Create a diary:

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"name": "Journal", "workspace": "Personal", "is_diary": true}' \
  http://YOUR_SERVER/api/v1/folders

Diary entries are ordinary notes filed under it, for instance with "folder": "Journal/2026/09" on Create Note.

Rename Folder

PATCH /folders/{id}

Rename an existing folder.

Request Body (JSON):

Field Type Description
name string New folder name
curl -X PATCH -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"name": "New Folder Name"}' \
  http://YOUR_SERVER/api/v1/folders/12

Move Folder

POST /folders/{id}/move

Move folder to a different parent or workspace.

Request Body (JSON):

Field Type Description
parent_id integer|null New parent folder ID (null or 0 for root). new_parent_folder_id is accepted as well
new_parent_folder string New parent by path, when its ID is not at hand. The folder must exist; a bare name is resolved like on Create Note
target_workspace string Target workspace (for cross-workspace move). The folder takes its subfolders and their notes along

Move to another parent:

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"parent_id": 56}' \
  http://YOUR_SERVER/api/v1/folders/34/move

Move to root:

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"parent_id": null}' \
  http://YOUR_SERVER/api/v1/folders/34/move

Move to another workspace:

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"target_workspace": "New Workspace", "parent_id": null}' \
  http://YOUR_SERVER/api/v1/folders/34/move

Duplicate Folder

POST /folders/{id}/duplicate

Copy a folder next to the original, with all of its notes (attachments included) and subfolders. The copy gets a unique name (Name (1) when Name already exists), keeps the folder icon, color, Kanban mode and sort setting, and starts unpinned, unfavorited and unshared. Links between notes of the copied tree point at the copies, so a folder can serve as a template.

Query Parameters:

Parameter Type Description
workspace string Restrict the lookup to a workspace (optional)
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/folders/34/duplicate

Response (201):

{
  "success": true,
  "message": "Folder duplicated successfully",
  "folder": { "id": 57, "name": "Project template (1)", "workspace": "Poznote", "parent_id": null },
  "folder_id": 57,
  "folder_name": "Project template (1)",
  "source_folder_id": 34,
  "notes_count": 8,
  "subfolders_count": 3
}

Returns 403 when the copy would exceed the note or storage quota, and 404 when the folder does not exist.

Move Files Between Folders

POST /folders/move-files

Move all files from one folder to another.

Request Body (JSON):

Field Type Description
source_folder_id integer Source folder ID
target_folder_id integer Target folder ID
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"source_folder_id": 10, "target_folder_id": 20}' \
  http://YOUR_SERVER/api/v1/folders/move-files

Reorder Folders

POST /folders/reorder

Reorder a folder before or after a sibling folder (same workspace). The siblings are renumbered from the order the current sort mode displays, and the tree switches to the manual mode, exactly like a note reorder. Moving a folder into or out of another one is POST /folders/{id}/move instead, which leaves the sort mode alone.

Request Body (JSON):

Field Type Required Description
folder_id integer Yes Folder to move
target_folder_id integer Yes Sibling folder used as anchor
position string Yes before or after the target folder
workspace string No Workspace check (must match target folder)
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"folder_id": 10, "target_folder_id": 20, "position": "after"}' \
  http://YOUR_SERVER/api/v1/folders/reorder

Create Kanban Structure

POST /folders/kanban-structure

Create a Kanban board folder structure (a parent folder with column subfolders).

Request Body (JSON):

Field Type Required Description
folder_name string Yes Name of the Kanban board folder
columns integer Yes Number of columns (1–9)
workspace string No Target workspace
parent_folder_id integer No Create inside this parent folder
language string No Language for default column names (default en)
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"workspace": "Personal", "folder_name": "Project Board", "columns": 3}' \
  http://YOUR_SERVER/api/v1/folders/kanban-structure

Update Folder Icon

PUT /folders/{id}/icon

Set a custom icon for a folder.

Request Body (JSON):

Field Type Description
icon string Icon class name (e.g. fa-folder-open)
curl -X PUT -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"icon": "fa-folder-open"}' \
  http://YOUR_SERVER/api/v1/folders/12/icon

Set Folder Color

PUT /folders/{id}/color

Set the color used to tint the folder card on the dashboard. Uses the same note color palette as notes. Send an empty color to remove it.

Request Body (JSON):

Field Type Description
color string Palette id (e.g. blue) or custom hex (e.g. #8bc34a), empty to remove

Folder objects returned by GET /folders and GET /folders/{id} expose both color (the stored value) and color_hex (the resolved color, null when the folder has no color or its palette entry was deleted).

curl -X PUT -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"color": "purple"}' \
  http://YOUR_SERVER/api/v1/folders/12/color

Set Folder Pinned State

PUT /folders/{id}/pinned

Pin or unpin a folder, which keeps it at the top of the folder list.

Request Body (JSON):

Field Type Required Description
pinned boolean Yes true to pin the folder, false to unpin it

Response:

{
  "success": true,
  "message": "Folder pinned state updated successfully",
  "pinned": true
}
curl -X PUT -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"pinned": true}' \
  http://YOUR_SERVER/api/v1/folders/12/pinned

Set Folder Favorite State

PUT /folders/{id}/favorite

Mark a folder as a favorite, or remove it from the favorites. Unlike POST /notes/{id}/favorite, this sets the state explicitly instead of toggling it.

Request Body (JSON):

Field Type Required Description
favorite boolean Yes true to mark the folder as a favorite, false to remove it

Response:

{
  "success": true,
  "message": "Folder favorite state updated successfully",
  "favorite": true
}
curl -X PUT -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"favorite": true}' \
  http://YOUR_SERVER/api/v1/folders/12/favorite

Keep Folder Offline

PUT /folders/{id}/offline

Keep the folder's notes, subfolders included, in the browsers where the account is used whatever their date, with all their attachments (Offline Copies), or stop doing so.

Request Body (JSON):

Field Type Required Description
offline boolean Yes true to keep the folder offline, false to stop

Response:

{
  "success": true,
  "message": "Folder offline state updated successfully",
  "offline": true
}
curl -X PUT -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"offline": true}' \
  http://YOUR_SERVER/api/v1/folders/12/offline

Empty Folder

POST /folders/{id}/empty

Move all notes in a folder to trash.

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/folders/12/empty

Delete Folder

DELETE /folders/{id}

Delete a folder and its subfolders. Their notes go to the trash.

curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/folders/12

The response carries a restore_snapshot describing what was removed, to be posted back to POST /folders/restore to undo the delete:

{
  "success": true,
  "restore_snapshot": {
    "workspace": "Poznote",
    "folder_id": 12,
    "folders": [{ "id": 12, "name": "Projects", "parent_id": null, "icon": null, "color": null, "display_order": 0 }],
    "notes": [{ "id": 123, "folder_id": 12 }]
  }
}

Restore Deleted Folder

POST /folders/restore

Rebuild a deleted folder tree from the restore_snapshot returned by Delete Folder. Folders are recreated (parents first, with their icon, color, order and settings) and the listed notes are taken out of the trash into the recreated folders. A folder that already exists again at the same place under the same name is reused. Public share links are not restored.

Body: the restore_snapshot object as returned by Delete Folder.

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"workspace": "Poznote", "folder_id": 12, "folders": [{"id": 12, "name": "Projects", "parent_id": null}], "notes": [{"id": 123, "folder_id": 12}]}' \
  http://YOUR_SERVER/api/v1/folders/restore

Response: folder_id (new id of the root folder), folder_id_map (old id to new id) and restored_notes.


Trash

List Trash

GET /trash

Get all notes in trash.

Query Parameters:

Parameter Type Description
workspace string Filter by workspace
search string Search in trashed notes
curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/trash

Filter by workspace:

curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/trash?workspace=Personal"

Empty Trash

DELETE /trash

Permanently delete all notes in trash.

Query Parameters:

Parameter Type Description
workspace string Only empty trash for a specific workspace
curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/trash

Permanently Delete Note from Trash

DELETE /trash/{id}

Delete a specific note permanently from trash.

curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/trash/123

Workspaces

List Workspaces

GET /workspaces

Get all workspaces. Each workspace carries its tags (a list of labels, empty when none) and its color (a palette id or #rrggbb, null when none) with the hex it resolves to in color_hex. Tags group workspaces on the dashboard scope selector; the color marks the workspace's cards when several workspaces are shown together.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/workspaces

Response:

{
  "success": true,
  "workspaces": [
    { "name": "Psycho 101", "created": "2026-09-01 10:00:00", "tags": ["school", "psycho"], "color": "blue", "color_hex": "#3b82f6" },
    { "name": "Poznote", "created": "2026-01-01 09:00:00", "tags": [], "color": null, "color_hex": null }
  ]
}

Create Workspace

POST /workspaces

Request Body (JSON):

Field Type Required Description
name string Yes Workspace name
tags array or string No Tags of the workspace, as a list or a comma-separated string (max 20 tags, 50 characters each)
color string No Palette id (blue, ...) or #rrggbb
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"name": "MyProject", "tags": ["school", "psycho"]}' \
  http://YOUR_SERVER/api/v1/workspaces

Rename Workspace / Set Tags / Set Color

PATCH /workspaces/{name}

Request Body (JSON): at least one field is required.

Field Type Description
new_name string New workspace name
tags array or string Replaces the whole tag list (send an empty list to clear the tags)
color string Palette id (blue, ...) or #rrggbb; an empty string clears the color
curl -X PATCH -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"new_name": "NewName"}' \
  http://YOUR_SERVER/api/v1/workspaces/OldName

curl -X PATCH -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"tags": ["school", "psycho"]}' \
  http://YOUR_SERVER/api/v1/workspaces/Psycho%20101

Delete Workspace

DELETE /workspaces/{name}

Delete a workspace and all its contents.

curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/workspaces/OldWorkspace

Tags

List Tags

GET /tags

Get all unique tags.

Query Parameters:

Parameter Type Description
workspace string Filter by workspace
curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/tags

Filter by workspace:

curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/tags?workspace=Personal"

Rename Tag

PATCH /tags/{tag}

Rename a tag across all notes.

Request Body (JSON):

Field Type Required Description
new_name string Yes New tag name
workspace string No Only rename within this workspace
curl -X PATCH -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"new_name": "projects"}' \
  http://YOUR_SERVER/api/v1/tags/work

Delete Tag

DELETE /tags/{tag}

Remove a tag from all notes.

Query Parameters:

Parameter Type Description
workspace string Only delete within this workspace
curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/tags/obsolete-tag

Attachments

List Attachments

GET /notes/{noteId}/attachments

Get all attachments for a specific note.

Query Parameters:

Parameter Type Description
workspace string Workspace context
curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/attachments

Upload Attachment

POST /notes/{noteId}/attachments

Upload a file attachment to a note.

Request Body (multipart/form-data):

Field Type Description
file file The file to upload
workspace string Workspace context
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -F "file=@/path/to/file.pdf" \
  http://YOUR_SERVER/api/v1/notes/123/attachments

Download Attachment

GET /notes/{noteId}/attachments/{attachmentId}

Download a specific attachment. This endpoint also supports unauthenticated access for publicly shared notes using the token query parameter.

Query Parameters:

Parameter Type Description
workspace string Workspace context
token string Public share token (for shared notes)
curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/attachments/456 \
  -o downloaded-file.pdf

Delete Attachment

DELETE /notes/{noteId}/attachments/{attachmentId}

Delete an attachment from a note. References to it are removed from the note content. If a snapshot of the note still contains the attachment, the file is kept on disk (hidden from the note) until that snapshot expires, and the response carries "retained_for_snapshots": true; otherwise the file is deleted immediately.

curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/notes/123/attachments/456

Move Attachment

POST /notes/{noteId}/attachments/{attachmentId}/move

Move an attachment to another note of the same account, in any workspace. The response carries its new attachment_id on that note.

The file follows it, unless the note it leaves still points at it, from its content or from one of its snapshots: the target then gets its own copy, so both addresses keep working and deleting one note's attachment never takes the other's file away. kept_in_source says which of the two happened. A shortcut note cannot be the target, since it serves the attachments of the note it points at.

Request Body (application/json):

Field Type Description
target_note_id integer The note that receives the attachment
workspace string Workspace context of the source note
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"target_note_id": 789}' \
  http://YOUR_SERVER/api/v1/notes/123/attachments/456/move

Backups

List Backups

GET /backups

Get a list of all backup files with sizes and timestamps.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/backups

Create Backup

POST /backups

Create a complete backup ZIP containing database, all notes, and attachments. Attachments stored in the S3 bucket (when S3 attachment storage is configured) are fetched into the archive; if the bucket cannot be read, the request fails instead of returning an archive with missing files.

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/backups

Download Backup

GET /backups/{filename}

Download a specific backup file.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/backups/poznote_backup_2025-01-05_12-00-00.zip \
  -o backup.zip

Upload Backup

POST /backups/upload

Upload a local backup ZIP to the server's backup directory. The file is stored with a standard timestamped name and its filename is returned for use with the restore endpoint.

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  -F "file=@demo.zip" \
  http://YOUR_SERVER/api/v1/backups/upload

Response (201):

{
  "success": true,
  "filename": "poznote_backup_2025-01-05_12-00-00.zip",
  "size": 102400,
  "size_mb": 0.1,
  "restore_url": "/api/v1/backups/poznote_backup_2025-01-05_12-00-00.zip/restore",
  "download_url": "/api/v1/backups/poznote_backup_2025-01-05_12-00-00.zip"
}

Restore Backup

POST /backups/{filename}/restore

Restore a backup file. This replaces all current user data. When S3 attachment storage is enabled, the user's bucket content is replaced too, so the restore refuses archives that do not carry every attachment file their metadata references (for example a backup made with the "lighter archive" option), and refuses to run while the bucket is unreachable. The archive's database/poznote_backup.sql is also validated before anything is touched: only the statements a Poznote backup is made of (DROP TABLE, CREATE TABLE, CREATE INDEX, INSERT ... VALUES with literal values) are accepted, any other SQL is refused. Nothing is modified when the restore is refused.

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/backups/poznote_backup_2025-01-05_12-00-00.zip/restore

Delete Backup

DELETE /backups/{filename}

Delete a backup file.

curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/backups/poznote_backup_2025-01-05_12-00-00.zip

Export (Legacy)

These endpoints use legacy URL paths (not under /api/v1) and are primarily used for file downloads.

Export Note

Export a single note in various formats.

Query Parameters:

Parameter Type Description
id integer Note ID (required)
format string html, markdown, or json
type string Note type hint
disposition string attachment (download) or inline (display)
curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api_export_note.php?id=123&format=html" \
  -o exported-note.html

Export Folder

Export a folder as ZIP.

Query Parameters:

Parameter Type Description
folder_id integer Folder ID
workspace string Workspace filter
curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api_export_folder.php?folder_id=123" \
  -o folder-export.zip

Export Structured Notes

Export all notes preserving folder hierarchy.

Query Parameters:

Parameter Type Description
workspace string Workspace filter
curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api_export_structured.php?workspace=Personal" \
  -o structured-export.zip

Export All Notes

Export all note files as ZIP.

Query Parameters:

Parameter Type Description
workspace string Workspace filter (optional)
curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api_export_entries.php \
  -o all-notes.zip

Export All Attachments

Export all attachments as ZIP with metadata.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api_export_attachments.php \
  -o all-attachments.zip

Download Note (with styling)

Download a note file with proper headers and inline styling.

Query Parameters:

Parameter Type Description
id integer Note ID (required)
type string Note type hint
curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api_download_note.php?id=123" \
  -o note.html

Settings

Get Settings

GET /settings

Get setting values in one request. Use keys to return only specific settings; omit it to return all user settings, plus global settings for admins.

curl -u 'username:password' -H "X-User-ID: 1" \
  "http://YOUR_SERVER/api/v1/settings?keys=language,note_age_filter_days"

Get Setting

GET /settings/{key}

Get a setting value (user-level or global).

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/settings/language

Update Setting

PUT /settings/{key}

Set a setting value. Global settings require admin privileges.

Request Body (JSON):

Field Type Description
value mixed The setting value
curl -X PUT -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"value": "fr"}' \
  http://YOUR_SERVER/api/v1/settings/language

Global settings (admin only):

  • login_display_name
  • custom_css_path (read-only via this API. Use POST /api_upload_css.php to upload a file, POST /api_upload_css.php with action=select&filename=<name> to apply a stored one, GET to list them, and DELETE /api_upload_css.php?filename=<name> to remove one)
  • theme_list (read-only via this API. Use POST /api_upload_css.php with action=theme_list&entries=<json> to set what the theme button walks through, in order, for example [{"id":"light"},{"id":"custom:catppuccin.css","mode":"dark"}])
  • git_sync_enabled
  • import_max_individual_files
  • import_max_zip_files

System

System endpoints do not require the X-User-ID header.

Get Version

GET /system/version

Get current version and system information.

curl -u 'username:password' \
  http://YOUR_SERVER/api/v1/system/version

Check for Updates

GET /system/updates

Check if a newer version is available on GitHub.

curl -u 'username:password' \
  http://YOUR_SERVER/api/v1/system/updates

Get Translations

GET /system/i18n

Get translation/localization strings.

Query Parameters:

Parameter Type Description
lang string Language code (e.g. fr, en)
curl -u 'username:password' \
  http://YOUR_SERVER/api/v1/system/i18n

Git Sync

Git sync endpoints allow managing synchronization with GitHub, GitLab or Forgejo repositories. Each user configures their own repository independently.

Both /git-sync/ and /github-sync/ prefixes are supported (the latter is a legacy alias).

Get Sync Status

GET /git-sync/status

Get Git sync configuration and status.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/git-sync/status

Test Connection

POST /git-sync/test

Test the Git connection and credentials.

curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/git-sync/test

Push Notes

POST /git-sync/push

Push all notes to the configured Git repository. When synced workspaces are restricted (see workspaces in the config endpoint), only notes and attachments from those workspaces are pushed, and repository files outside them are removed.

Request Body (JSON):

Field Type Description
async boolean Run the push in the background (poll /git-sync/progress)
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/git-sync/push

Pull Notes

POST /git-sync/pull

Pull all notes from the configured Git repository. When synced workspaces are restricted (see workspaces in the config endpoint), only notes and attachments from those workspaces are pulled, and local notes in other workspaces are never touched.

Request Body (JSON):

Field Type Description
async boolean Run the pull in the background (poll /git-sync/progress)
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/git-sync/pull

Get Sync Progress

GET /git-sync/progress

Get the current sync progress from the session.

curl -u 'username:password' -H "X-User-ID: 1" \
  http://YOUR_SERVER/api/v1/git-sync/progress

Save Git Configuration

PUT /git-sync/config

Save per-user Git sync configuration.

Request Body (JSON):

Field Type Description
provider string github, gitlab or forgejo
repo string Repository in owner/repo format (GitLab: full project path, e.g. group/subgroup/project)
token string Access token (PAT)
branch string Git branch (default: main)
api_base string API base URL (GitLab and Forgejo; GitLab defaults to https://gitlab.com/api/v4)
author_name string Commit author name
author_email string Commit author email
workspaces array or null Restrict sync to these workspace names. null or an empty array syncs all workspaces. Omit the field to keep the current setting.
curl -X PUT -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "github",
    "repo": "username/my-notes",
    "token": "ghp_xxxxxxxxxxxx",
    "branch": "main",
    "author_name": "John",
    "author_email": "john@example.com"
  }' \
  http://YOUR_SERVER/api/v1/git-sync/config

User Profile

List User Profiles (Public)

GET /users/profiles

Get list of active user profiles for the login selector. No authentication required.

curl http://YOUR_SERVER/api/v1/users/profiles

Get Current User

GET /users/me

Get the current authenticated user's profile.

curl -u 'username:password' \
  http://YOUR_SERVER/api/v1/users/me

Update Current User

PATCH /users/me

Update the current user's own profile. Only the provided fields change.

Request Body (JSON):

Field Type Required Description
username string No Letters, digits, dots, underscores and dashes only (max. 60 characters). Cannot be purely numeric
first_name string No Max. 100 characters
last_name string No Max. 100 characters
email string No Administrators only. A regular user changing their own email gets a 403

Response:

{
  "success": true,
  "id": 2,
  "username": "alice",
  "email": "alice@example.com",
  "first_name": "Alice",
  "last_name": "Martin",
  "display_name": "Alice Martin"
}
curl -X PATCH -u 'username:password' -H "X-User-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{"first_name": "Alice", "last_name": "Martin"}' \
  http://YOUR_SERVER/api/v1/users/me

Change Password

POST /users/me/password

Change the current user's password.

Request Body (JSON):

Field Type Required Description
current_password string Yes Current password
new_password string Yes New password (min. 4 characters)
confirm_password string Yes New password confirmation
curl -X POST -u 'username:password' \
  -H "Content-Type: application/json" \
  -d '{"current_password": "current", "new_password": "newpass", "confirm_password": "newpass"}' \
  http://YOUR_SERVER/api/v1/users/me/password

Get Password Status

GET /users/me/password-status

Check whether the current user has a custom password or is using the .env fallback.

curl -u 'username:password' \
  http://YOUR_SERVER/api/v1/users/me/password-status

List App Passwords

GET /users/me/app-passwords

List the current user's app passwords (see Authentication). The secrets themselves are never returned, only a short hint (the prefix and the first four characters) to match a row against the value pasted in a client. Dates are in the user's timezone and format. Not available to requests authenticated with an app password.

curl -u 'username:password' \
  http://YOUR_SERVER/api/v1/users/me/app-passwords

Response:

{
  "app_passwords": [
    {
      "id": 3,
      "label": "Chrome extension",
      "hint": "pzn_2f7c",
      "created_at": "2026-09-10 14:02",
      "last_used_at": "2026-09-10 15:31",
      "expires_at": null,
      "expired": false
    }
  ],
  "count": 1,
  "active_count": 1,
  "limit": 25
}

Create App Password

POST /users/me/app-passwords

Create an app password. The clear-text secret is in this response and nowhere else afterwards. A profile can hold at most 25 app passwords. Not available to requests authenticated with an app password.

Request Body (JSON):

Field Type Required Description
label string Yes A name for the client that will use it (max. 60 characters)
expires_in_days integer No Lifetime in days (1 to 3650). Omit for no expiry
curl -X POST -u 'username:password' \
  -H "Content-Type: application/json" \
  -d '{"label": "Chrome extension", "expires_in_days": 90}' \
  http://YOUR_SERVER/api/v1/users/me/app-passwords

Response (201):

{
  "success": true,
  "app_password": {
    "id": 3,
    "label": "Chrome extension",
    "hint": "pzn_2f7c",
    "created_at": "2026-09-10 14:02",
    "last_used_at": null,
    "expires_at": "2026-12-09 14:02",
    "expired": false
  },
  "secret": "pzn_2f7c…"
}

Revoke App Password

DELETE /users/me/app-passwords/{id}

Revoke an app password. A client still holding the secret is refused from the next request on. Not available to requests authenticated with an app password.

curl -X DELETE -u 'username:password' \
  http://YOUR_SERVER/api/v1/users/me/app-passwords/3

Two-Factor Authentication

Two-factor authentication (TOTP) on password sign-in, managed by the user it belongs to. None of these endpoints is available to requests authenticated with an app password, and none requires X-User-ID.

GET /users/me/two-factor

Returns whether two-factor is on.

{
  "enabled": true,
  "enabled_at": "2026-09-21 20:14",
  "recovery_codes_remaining": 9,
  "unavailable_reason": null
}

unavailable_reason is sso_only or no_local_password when the account never signs in with a local password, in which case there is no login for a second factor to protect.

POST /users/me/two-factor/setup

Start switching two-factor on. Nothing is stored yet: the secret waits in the session until it is confirmed, so an abandoned setup locks nobody out. Needs a session, so call it with cookies kept between requests.

Field Type Required Description
current_password string Yes The account password
{
  "secret": "RXEVC7EP35CLQTZNBVQ2TZZYRLDTMZIC",
  "otpauth_uri": "otpauth://totp/Poznote:username?secret=RXEVC7EP35CLQTZNBVQ2TZZYRLDTMZIC&issuer=Poznote&algorithm=SHA1&digits=6&period=30",
  "issuer": "Poznote",
  "account": "username"
}
POST /users/me/two-factor/enable

Confirm the setup with a first code from the authenticator app. The response carries the ten recovery codes, the only time they are returned.

Field Type Required Description
code string Yes 6-digit code for the secret returned by setup
{
  "success": true,
  "recovery_codes": ["2PZR5-UJEQU", "K7QF2-9WMXA"],
  "enabled": true,
  "recovery_codes_remaining": 10
}
POST /users/me/two-factor/recovery-codes

Replace the recovery codes; the previous ones stop working. Body: code, a 6-digit code from the app (a recovery code is not accepted). The response has the same shape as enable.

POST /users/me/two-factor/disable

Switch two-factor off. Both factors are asked for.

Field Type Required Description
current_password string Yes The account password
code string Yes 6-digit code from the app, or a recovery code

A wrong code answers 400 with "code": "invalid_code", a wrong password 403 with "code": "invalid_password". Both count towards the same progressive delay as failed logins, and 429 is returned once that limit is reached.

Delete My Account

DELETE /users/me

Permanently delete the current user's own account and all of its data (notes, files, attachments). This cannot be undone.

Request Body (JSON):

Field Type Required Description
confirm_username string Yes Must exactly match the current username
password string Yes* Current password (*not required for OIDC sessions)

User ID 1 and the last active admin cannot be deleted. On success the session is destroyed and the response includes a redirect URL for the login page.

curl -X DELETE -u 'username:password' \
  -H "Content-Type: application/json" \
  -d '{"confirm_username": "username", "password": "password"}' \
  http://YOUR_SERVER/api/v1/users/me

Admin (User Management)

Admin endpoints require administrator credentials and do not require the X-User-ID header.

List All Users with Statistics

GET /admin/users

Get detailed list of all users with storage info.

curl -u 'username:password' \
  http://YOUR_SERVER/api/v1/admin/users

Get Specific User

GET /admin/users/{id}

Get detailed information about a user.

curl -u 'username:password' \
  http://YOUR_SERVER/api/v1/admin/users/1

Create User

POST /admin/users

Create a new user profile.

Request Body (JSON):

Field Type Required Description
username string Yes Username
email string No Email address
curl -X POST -u 'username:password' \
  -H "Content-Type: application/json" \
  -d '{"username": "newuser"}' \
  http://YOUR_SERVER/api/v1/admin/users

Update User

PATCH /admin/users/{id}

Update user properties.

Request Body (JSON):

Field Type Description
username string New username
active boolean Active status
is_admin boolean Admin privileges
curl -X PATCH -u 'username:password' \
  -H "Content-Type: application/json" \
  -d '{
    "username": "renameduser",
    "active": true,
    "is_admin": false
  }' \
  http://YOUR_SERVER/api/v1/admin/users/2

Delete User

DELETE /admin/users/{id}

Delete a user profile and everything it owns: its notes, folders, tags and attachments on disk, and its attachments and backup archives in the S3 buckets. Nothing references a deleted user's data anymore, so anything left behind would be orphaned forever.

curl -X DELETE -u 'username:password' \
  http://YOUR_SERVER/api/v1/admin/users/2

This is immediate and irreversible. Nothing is kept and no backup is created beforehand, so download a complete backup ZIP first if the data still matters.

The former delete_data query parameter is gone: deletion always removes all data. Keeping the local notes while the S3 purge destroyed the only copy of their attachments produced a "preserved" data set with every attachment missing, so the partial mode was dropped.

The response reports s3_objects_deleted, and s3_error when a bucket could not be reached. A bucket error never blocks the deletion, so the account is removed either way and the leftover objects have to be cleaned up manually.

Reset User Password

POST /admin/users/{id}/reset-password

Reset a user's password to the default or set a custom one.

Request Body (JSON):

Field Type Required Description
action string No reset_to_default (default) or set_password
new_password string If set_password New password (min. 4 characters)

Reset to default:

curl -X POST -u 'username:password' \
  -H "Content-Type: application/json" \
  -d '{"action": "reset_to_default"}' \
  http://YOUR_SERVER/api/v1/admin/users/2/reset-password

Set a custom password:

curl -X POST -u 'username:password' \
  -H "Content-Type: application/json" \
  -d '{"action": "set_password", "new_password": "new-password"}' \
  http://YOUR_SERVER/api/v1/admin/users/2/reset-password

Get User Password Status

POST /admin/users/{id}/two-factor/reset

Switch a user's two-factor authentication off, for someone who lost their device and their recovery codes. They sign in with the password alone afterwards and can set it up again. Answers 409 when two-factor is not on for that user. The action is recorded in the activity log.

curl -X POST -u 'admin:password' \
  http://YOUR_SERVER/api/v1/admin/users/2/two-factor/reset
GET /admin/users/{id}/password-status

Check whether a user has a custom password or is using the .env fallback.

curl -u 'username:password' \
  http://YOUR_SERVER/api/v1/admin/users/2/password-status

Lookup User by Username

GET /users/lookup/{username}

Get user ID by username. Admin only. Used by backup scripts.

curl -u 'username:password' \
  http://YOUR_SERVER/api/v1/users/lookup/Nina

Get System Statistics

GET /admin/stats

Get aggregated statistics for all users.

curl -u 'username:password' \
  http://YOUR_SERVER/api/v1/admin/stats

Repair Master Database

POST /admin/repair

Scan and rebuild the master database registry.

curl -X POST -u 'username:password' \
  http://YOUR_SERVER/api/v1/admin/repair

Orphan Attachments

GET /admin/orphan-attachments
DELETE /admin/orphan-attachments

List, per account, the files of the local attachment folder that no note references any more (files kept in S3 storage are not listed). DELETE runs the same scan and deletes those files; it is not available with an app password.

curl -u 'username:password' \
  http://YOUR_SERVER/api/v1/admin/orphan-attachments

Response:

{
  "success": true,
  "users": [
    {"user_id": 1, "total_files": 42, "orphans_found": 2, "orphans_deleted": 0, "files": ["a1b2c3.png", "d4e5f6.pdf"], "error": null}
  ]
}

Public / Shared Tasks

These endpoints manage interactive tasks on publicly shared notes. They use a token query parameter for authentication instead of HTTP Basic Auth.

Update Task

PATCH /public/tasks/{id}

Update a task's status or text on a shared note.

Query Parameters:

Parameter Type Description
token string Public share token

Request Body (JSON):

Field Type Description
completed boolean Task completion status
text string Task text
curl -X PATCH \
  -H "Content-Type: application/json" \
  -d '{"completed": true}' \
  "http://YOUR_SERVER/api/v1/public/tasks/0?token=abc123"

Add Task

POST /public/tasks

Add a new task to a shared task list.

Query Parameters:

Parameter Type Description
token string Public share token

Request Body (JSON):

Field Type Description
text string Task text
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"text": "New task item"}' \
  "http://YOUR_SERVER/api/v1/public/tasks?token=abc123"

Delete Task

DELETE /public/tasks/{id}

Delete a task from a shared task list.

Query Parameters:

Parameter Type Description
token string Public share token
curl -X DELETE \
  "http://YOUR_SERVER/api/v1/public/tasks/0?token=abc123"

Update Note Content

PATCH /public/notes/content

Replace the text content of a publicly shared HTML or markdown note. Only allowed when the share's access_mode is edit. The submitted content is sanitized server-side, and the write is subject to the owner's storage quota, the share password (if any) and the share's allowed users restriction (if any).

Query Parameters:

Parameter Type Description
token string Public share token

Request Body (JSON):

Field Type Description
content string New note content (HTML for notes, raw source for markdown)
editor_session_id string Optional editor session identifier (see edit locks below)
curl -X PATCH -H "Content-Type: application/json" \
  -d '{"content": "<p>Updated text</p>"}' \
  "http://YOUR_SERVER/api/v1/public/notes/content?token=abc123"

The write is rejected with 423 Locked while another editor (a visitor on the public page, or an account user editing the note inside the app) holds the note's edit lock. Requests without an editor_session_id are accepted whenever nobody is editing.

Public Note Edit Lock

POST /public/notes/lock
POST /public/notes/lock/heartbeat
POST /public/notes/lock/release

Exclusive edit lock for publicly shared notes, so two people cannot edit the same note at the same time. The lock is shared with the in-app editor: while an account user has the note open in the app, public visitors cannot enter edit mode, and vice versa. Only allowed when the share's access_mode is edit; the share password (if any) and allowed users restriction (if any) apply.

The lock expires 90 seconds after the last acquire/heartbeat, so a heartbeat should be sent every 20-30 seconds while editing.

Query Parameters:

Parameter Type Description
token string Public share token

Request Body (JSON):

Field Type Description
editor_session_id string Opaque identifier of this editor (one per tab/client)
curl -X POST -H "Content-Type: application/json" \
  -d '{"editor_session_id": "my-client-1"}' \
  "http://YOUR_SERVER/api/v1/public/notes/lock?token=abc123"

Acquire and heartbeat answer 423 Locked when another editor currently holds the lock.


Health Check

GET /api_health.php

Health check endpoint. No authentication required. Returns service status, name, and version.

curl http://YOUR_SERVER/api_health.php

Response:

{
  "status": "ok",
  "service": "poznote",
  "version": "x.x.x"
}

Endpoint Reference (Quick Summary)

Notes

Method Endpoint Description
GET /notes List notes
GET /notes/with-attachments List notes with attachments
GET /notes/templates List notes usable as templates
GET /notes/resolve Resolve note by reference
GET /notes/search Search notes
GET /notes/{id} Get note
POST /notes Create note
PATCH /notes/{id} Update note
DELETE /notes/{id} Delete note
POST /notes/{id}/restore Restore from trash
POST /notes/{id}/duplicate Duplicate note
POST /notes/{id}/convert Convert type
POST /notes/{id}/beacon Emergency save
PUT /notes/{id}/tags Update tags
PUT /notes/{id}/icon Update note icon
PUT /notes/{id}/color Update card color
PUT /notes/{id}/pinned Pin or unpin a note
POST /notes/{id}/kanban-completed Set Kanban completed state
POST /notes/{id}/favorite Toggle favorite
POST /notes/{id}/folder Move to folder
POST /notes/{id}/remove-folder Remove from folder
POST /notes/reorder Reorder a note before/after another
POST /notes/{id}/archive Archive into the Archives workspace

Note Locks

Method Endpoint Description
POST /notes/{id}/lock Acquire edit lock
GET /notes/{id}/lock Lock status
POST /notes/{id}/lock/heartbeat Refresh lock
POST /notes/{id}/lock/release Release lock

Offline Copies

Method Endpoint Description
GET /offline/manifest Notes kept offline, with their versions
GET /offline/notes?ids= Full content of up to 50 notes

Snapshots

Method Endpoint Description
POST /notes/{id}/snapshot Create snapshot
GET /notes/{id}/snapshots List snapshots
GET /notes/{id}/snapshot Get snapshot
POST /notes/{id}/snapshot/restore Restore snapshot
DELETE /notes/{id}/snapshot Delete snapshot

Tasks

Method Endpoint Description
GET /tasks List all tasks
GET /notes/{id}/tasks List the tasks of one note
POST /notes/{id}/tasks Add a task to a note
PATCH /notes/{id}/tasks/{taskId} Update a task
DELETE /notes/{id}/tasks/{taskId} Delete a task

Reminders

Method Endpoint Description
GET /notes/{id}/reminder Get note reminder
POST /notes/{id}/reminder Set note reminder
DELETE /notes/{id}/reminder Remove note reminder
POST /notes/{id}/task-reminder Set task reminder
DELETE /notes/{id}/task-reminder Remove task reminder
GET /reminders List notifications
GET /reminders/count Notification count
POST /reminders/{id}/read Mark as read
POST /reminders/{id}/dismiss Dismiss notification
POST /reminders/dismiss-all Dismiss all

Note Sharing

Method Endpoint Description
GET /notes/{id}/share Get share status
POST /notes/{id}/share Create share link
PATCH /notes/{id}/share Update share settings
DELETE /notes/{id}/share Revoke share
GET /shared List shared notes
GET /shared/with-me Shared with me

Backlinks & Graph

Method Endpoint Description
GET /notes/{id}/backlinks Get backlinks
GET /graph Get the note-link graph

Folders

Method Endpoint Description
GET /folders List folders
GET /folders/counts Folder counts
GET /folders/suggested Suggested folders
GET /folders/{id} Get folder
GET /folders/{id}/notes Note count
GET /folders/{id}/path Folder path
POST /folders Create folder
PATCH /folders/{id} Rename folder
DELETE /folders/{id} Delete folder
POST /folders/{id}/move Move folder
POST /folders/{id}/duplicate Duplicate folder with notes and subfolders
POST /folders/{id}/empty Empty folder
PUT /folders/{id}/icon Update icon
PUT /folders/{id}/color Update card color
PUT /folders/{id}/pinned Pin or unpin a folder
PUT /folders/{id}/favorite Set favorite state
POST /folders/move-files Move files
POST /folders/reorder Reorder folders
POST /folders/kanban-structure Create Kanban

Folder Sharing

Method Endpoint Description
GET /folders/{id}/share Get share status
POST /folders/{id}/share Create share link
PATCH /folders/{id}/share Update share
DELETE /folders/{id}/share Revoke share

Trash

Method Endpoint Description
GET /trash List trash
DELETE /trash Empty trash
DELETE /trash/{id} Delete from trash

Workspaces

Method Endpoint Description
GET /workspaces List workspaces
POST /workspaces Create workspace
PATCH /workspaces/{name} Rename workspace
DELETE /workspaces/{name} Delete workspace

Tags

Method Endpoint Description
GET /tags List tags
PATCH /tags/{tag} Rename tag
DELETE /tags/{tag} Delete tag

Attachments

Method Endpoint Description
GET /notes/{noteId}/attachments List attachments
POST /notes/{noteId}/attachments Upload attachment
GET /notes/{noteId}/attachments/{attachmentId} Download attachment
DELETE /notes/{noteId}/attachments/{attachmentId} Delete attachment
POST /notes/{noteId}/attachments/{attachmentId}/move Move attachment to another note

Backups

Method Endpoint Description
GET /backups List backups
POST /backups Create backup
GET /backups/{filename} Download backup
POST /backups/upload Upload backup ZIP
POST /backups/{filename}/restore Restore backup
DELETE /backups/{filename} Delete backup

Settings

Method Endpoint Description
GET /settings Get settings
GET /settings/{key} Get setting
PUT /settings/{key} Update setting

System

Method Endpoint Description
GET /system/version Version info
GET /system/updates Check updates
GET /system/i18n Translations

Git Sync

Method Endpoint Description
GET /git-sync/status Sync status
POST /git-sync/test Test connection
POST /git-sync/push Push notes
POST /git-sync/pull Pull notes
GET /git-sync/progress Sync progress
PUT /git-sync/config Save config

User Profile

Method Endpoint Description
GET /users/profiles List profiles (public)
GET /users/me Current user
PATCH /users/me Update own profile
POST /users/me/password Change password
GET /users/me/password-status Password status
GET /users/me/app-passwords List app passwords
POST /users/me/app-passwords Create app password
DELETE /users/me/app-passwords/{id} Revoke app password
GET /users/me/two-factor Two-factor status
POST /users/me/two-factor/setup Start two-factor setup
POST /users/me/two-factor/enable Confirm setup, get recovery codes
POST /users/me/two-factor/recovery-codes Replace recovery codes
POST /users/me/two-factor/disable Turn two-factor off
DELETE /users/me Delete own account
GET /users/lookup/{username} Lookup by name

Admin

Method Endpoint Description
GET /admin/users List users
GET /admin/users/{id} Get user
POST /admin/users Create user
PATCH /admin/users/{id} Update user
DELETE /admin/users/{id} Delete user
POST /admin/users/{id}/reset-password Reset password
POST /admin/users/{id}/two-factor/reset Turn a user's two-factor off
GET /admin/users/{id}/password-status Password status
GET /admin/stats System stats
POST /admin/repair Repair database
GET /admin/orphan-attachments List orphan attachment files
DELETE /admin/orphan-attachments Delete orphan attachment files

Public Shares

Method Endpoint Description
PATCH /public/tasks/{id} Update task
POST /public/tasks Add task
DELETE /public/tasks/{id} Delete task
PATCH /public/notes/content Update shared note content (edit shares)
POST /public/notes/lock Acquire the edit lock on a shared note (edit shares)
POST /public/notes/lock/heartbeat Keep the edit lock alive
POST /public/notes/lock/release Release the edit lock