Poznote provides a comprehensive RESTful API v1 for programmatic access to notes, folders, workspaces, tags, attachments, backups, settings, and more.
- Authentication
- Multi-User Mode
- Base URL
- Response Format
- HTTP Status Codes
- Interactive Documentation (Swagger)
- Notes
- Note Locks
- Change Detection
- Offline Copies
- Snapshots
- Tasks
- Reminders
- Note Sharing
- Folder Sharing
- Backlinks & Graph
- Folders
- Trash
- Workspaces
- Tags
- Attachments
- Backups
- Export (Legacy)
- Settings
- System
- Git Sync
- User Profile
- Admin (User Management)
- Public / Shared Tasks
- Health Check
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/notesUse 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/notesWhat 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/notesWithout 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/notesFor 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.
| 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/* |
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/notesEndpoints 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.
/api/v1
All endpoints in this document are relative to this base URL unless otherwise noted (legacy endpoints use full paths).
All endpoints return JSON. Successful responses typically follow this structure:
{
"success": true,
"data": { ... }
}Error responses:
{
"success": false,
"error": "Error description"
}| 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 |
Access the Swagger UI directly from Poznote at Settings > API Documentation to browse all endpoints, view request/response schemas, and test API calls interactively.
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/notesThe 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"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-attachmentsGET /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 theTemplatesfolders of this workspace. Notes of aTemplatesworkspace 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 /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/123GET /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"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"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/notesAmbiguous 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"}
]
}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/123Optimistic 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/123Edit 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 /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/123Permanently delete:
curl -X DELETE -u 'username:password' -H "X-User-ID: 1" \
"http://YOUR_SERVER/api/v1/notes/123?permanent=true"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/restorePOST /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 (""or0for 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/duplicatePOST /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/convertPUT /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/tagsPUT /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/iconPUT /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/colorPUT /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/pinnedPUT /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-widthPOST /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-completedPOST /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/favoritePUT /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/offlinePOST /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/folderPOST /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-folderPOST /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/reorderPOST /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/archivePOST /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.
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.
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/lockGET /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/lockPOST /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/heartbeatPOST /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/releaseThe 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}¬e_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¬e_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.
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
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?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 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.
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"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/snapshotsGET /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"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 /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"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 |
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"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/tasksPOST /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/tasksPATCH /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.45DELETE /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.45Reminders schedule notifications for notes. Triggered reminders appear as notifications that can be read or dismissed.
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/reminderPOST /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/reminderDELETE /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/reminderPOST /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-reminderDELETE /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-reminderGET /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/remindersGET /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/countPOST /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/readPOST /reminders/{id}/dismiss
Dismiss a notification.
curl -X POST -u 'username:password' -H "X-User-ID: 1" \
http://YOUR_SERVER/api/v1/reminders/45/dismissPOST /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-allGET /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/sharePOST /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/sharePATCH /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/shareDELETE /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/shareGET /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/sharedGET /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-meGET /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/sharePOST /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/shareWith 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/sharePATCH /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/shareDELETE /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/shareGET /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/backlinksGET /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"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 /folders/{id}
Get details of a specific folder.
curl -u 'username:password' -H "X-User-ID: 1" \
http://YOUR_SERVER/api/v1/folders/12GET /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 /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/suggestedGET /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/pathGET /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/notesPOST /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/foldersCreate 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/foldersCreate 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/foldersCreate 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/foldersDiary entries are ordinary notes filed under it, for instance with "folder": "Journal/2026/09" on Create Note.
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/12POST /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/moveMove 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/moveMove 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/movePOST /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/duplicateResponse (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.
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-filesPOST /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/reorderPOST /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-structurePUT /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/iconPUT /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/colorPUT /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/pinnedPUT /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/favoritePUT /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/offlinePOST /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/emptyDELETE /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/12The 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 }]
}
}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/restoreResponse: folder_id (new id of the root folder), folder_id_map (old id to new id) and restored_notes.
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/trashFilter by workspace:
curl -u 'username:password' -H "X-User-ID: 1" \
"http://YOUR_SERVER/api/v1/trash?workspace=Personal"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/trashDELETE /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/123GET /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/workspacesResponse:
{
"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 }
]
}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/workspacesPATCH /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%20101DELETE /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/OldWorkspaceGET /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/tagsFilter by workspace:
curl -u 'username:password' -H "X-User-ID: 1" \
"http://YOUR_SERVER/api/v1/tags?workspace=Personal"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/workDELETE /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-tagGET /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/attachmentsPOST /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/attachmentsGET /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.pdfDELETE /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/456POST /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/moveGET /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/backupsPOST /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/backupsGET /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.zipPOST /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/uploadResponse (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"
}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/restoreDELETE /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.zipThese endpoints use legacy URL paths (not under /api/v1) and are primarily used for file downloads.
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.htmlExport 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.zipExport 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.zipExport 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.zipExport 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.zipDownload 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.htmlGET /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 /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/languagePUT /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/languageGlobal settings (admin only):
login_display_namecustom_css_path(read-only via this API. UsePOST /api_upload_css.phpto upload a file,POST /api_upload_css.phpwithaction=select&filename=<name>to apply a stored one,GETto list them, andDELETE /api_upload_css.php?filename=<name>to remove one)theme_list(read-only via this API. UsePOST /api_upload_css.phpwithaction=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_enabledimport_max_individual_filesimport_max_zip_files
System endpoints do not require the X-User-ID header.
GET /system/version
Get current version and system information.
curl -u 'username:password' \
http://YOUR_SERVER/api/v1/system/versionGET /system/updates
Check if a newer version is available on GitHub.
curl -u 'username:password' \
http://YOUR_SERVER/api/v1/system/updatesGET /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/i18nGit 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 /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/statusPOST /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/testPOST /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/pushPOST /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/pullGET /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/progressPUT /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/configGET /users/profiles
Get list of active user profiles for the login selector. No authentication required.
curl http://YOUR_SERVER/api/v1/users/profilesGET /users/me
Get the current authenticated user's profile.
curl -u 'username:password' \
http://YOUR_SERVER/api/v1/users/mePATCH /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/mePOST /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/passwordGET /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-statusGET /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-passwordsResponse:
{
"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
}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-passwordsResponse (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…"
}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/3Two-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 /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/meAdmin endpoints require administrator credentials and do not require the X-User-ID header.
GET /admin/users
Get detailed list of all users with storage info.
curl -u 'username:password' \
http://YOUR_SERVER/api/v1/admin/usersGET /admin/users/{id}
Get detailed information about a user.
curl -u 'username:password' \
http://YOUR_SERVER/api/v1/admin/users/1POST /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/usersPATCH /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/2DELETE /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/2This 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.
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-passwordSet 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-passwordPOST /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/resetGET /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-statusGET /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/NinaGET /admin/stats
Get aggregated statistics for all users.
curl -u 'username:password' \
http://YOUR_SERVER/api/v1/admin/statsPOST /admin/repair
Scan and rebuild the master database registry.
curl -X POST -u 'username:password' \
http://YOUR_SERVER/api/v1/admin/repairGET /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-attachmentsResponse:
{
"success": true,
"users": [
{"user_id": 1, "total_files": 42, "orphans_found": 2, "orphans_deleted": 0, "files": ["a1b2c3.png", "d4e5f6.pdf"], "error": null}
]
}These endpoints manage interactive tasks on publicly shared notes. They use a token query parameter for authentication instead of HTTP Basic Auth.
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"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 /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"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.
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.
GET /api_health.php
Health check endpoint. No authentication required. Returns service status, name, and version.
curl http://YOUR_SERVER/api_health.phpResponse:
{
"status": "ok",
"service": "poznote",
"version": "x.x.x"
}| 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 |
| 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 |
| Method | Endpoint | Description |
|---|---|---|
GET |
/offline/manifest |
Notes kept offline, with their versions |
GET |
/offline/notes?ids= |
Full content of up to 50 notes |
| 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 |
| 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 |
| 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 |
| 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 |
| Method | Endpoint | Description |
|---|---|---|
GET |
/notes/{id}/backlinks |
Get backlinks |
GET |
/graph |
Get the note-link graph |
| 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 |
| 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 |
| Method | Endpoint | Description |
|---|---|---|
GET |
/trash |
List trash |
DELETE |
/trash |
Empty trash |
DELETE |
/trash/{id} |
Delete from trash |
| Method | Endpoint | Description |
|---|---|---|
GET |
/workspaces |
List workspaces |
POST |
/workspaces |
Create workspace |
PATCH |
/workspaces/{name} |
Rename workspace |
DELETE |
/workspaces/{name} |
Delete workspace |
| Method | Endpoint | Description |
|---|---|---|
GET |
/tags |
List tags |
PATCH |
/tags/{tag} |
Rename tag |
DELETE |
/tags/{tag} |
Delete tag |
| 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 |
| 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 |
| Method | Endpoint | Description |
|---|---|---|
GET |
/settings |
Get settings |
GET |
/settings/{key} |
Get setting |
PUT |
/settings/{key} |
Update setting |
| Method | Endpoint | Description |
|---|---|---|
GET |
/system/version |
Version info |
GET |
/system/updates |
Check updates |
GET |
/system/i18n |
Translations |
| 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 |
| 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 |
| 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 |
| 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 |