Developer API Reference
Use the Krokanti Notes REST API to read and write notes programmatically, automate workflows, or build integrations. All endpoints accept and return JSON.
https://notes.krokanti.com/api100 req/min per tokenPro subscription required
API token access requires an active Pro subscription. Generate your token in Settings → Connections after upgrading.
Upgrade to Pro →Authentication
All API requests must include a personal API token in the Authorizationheader. Generate tokens in Settings → Connections (Pro plan required). Tokens start with kn_ and are shown only once at creation — store them securely.
curl https://notes.krokanti.com/api/notes \
-H "Authorization: Bearer kn_your_token_here" \
-H "Content-Type: application/json"Notes
/api/notesList all notes for the authenticated user. Supports filtering, sorting, and pagination.
| Parameter | Type | Description |
|---|---|---|
search | string | Full-text search across title and content |
folderId | string | Filter by folder ID |
tag | string | Filter by tag name (exact match) |
trashed | boolean | Set to "true" to list trashed notes instead of active ones |
sortBy | updatedAt | createdAt | title | Sort field (default: "updatedAt") |
sortOrder | asc | desc | Sort direction (default: "desc") |
limit | number | Results per page — 1 to 100 (default: 50) |
offset | number | Number of results to skip (default: 0) |
- →Response: { notes: Note[], hasMore: boolean }
- →Pinned notes are always returned first, then sorted by the chosen field
- →Secure note content is returned as an encrypted blob (JSON string starting with {"v":1)
/api/notesCreate a new empty note. Returns the created note object.
- →The new note has an empty title and content — update it immediately with PATCH
- →Response status: 201 Created
/api/notes/:idFetch a single note by ID.
| Parameter | Type | Description |
|---|---|---|
idrequired | string | Note UUID |
- →Returns 404 if the note does not exist or belongs to another user
- →Secure note content is returned as an encrypted blob
/api/notes/:idUpdate a note's title, content, tags, folder, or metadata. Supports conflict detection.
| Parameter | Type | Description |
|---|---|---|
idrequired | string | Note UUID (path parameter) |
title | string | New title |
content | string | New HTML content |
tags | string[] | Replace the full tags array |
folderId | string | null | Move to folder (null to remove from folder) |
isPinned | boolean | Pin or unpin the note (owner only) |
isPublic | boolean | Publish or unpublish the note (owner only) |
action | trash | restore | Move to trash or restore from trash (owner only) |
clientUpdatedAt | string (ISO 8601) | Last known updatedAt — used for conflict detection |
force | boolean | Skip conflict detection and overwrite server state |
- →Returns 409 with { conflict: true, serverNote } if clientUpdatedAt is older than the server version
- →Secure notes cannot be edited via the API (returns 403)
- →Collaborators with edit permission can update title, content only — tags and folder are owner-only
/api/notes/:idPermanently delete a note. This is irreversible — use action: 'trash' via PATCH for soft delete.
| Parameter | Type | Description |
|---|---|---|
idrequired | string | Note UUID |
Folders
/api/foldersList all folders for the authenticated user.
- →Response: { folders: Folder[] }
/api/foldersCreate a new folder.
| Parameter | Type | Description |
|---|---|---|
namerequired | string | Folder name |
- →Response status: 201 Created
/api/folders/:idRename a folder.
| Parameter | Type | Description |
|---|---|---|
idrequired | string | Folder UUID (path parameter) |
namerequired | string | New folder name |
/api/folders/:idDelete a folder. Notes inside the folder are not deleted — they become unfoldered.
| Parameter | Type | Description |
|---|---|---|
idrequired | string | Folder UUID |
Pagination
The GET /api/notes endpoint uses offset-based pagination. Control it with two query parameters:
| Parameter | Default | Description |
|---|---|---|
limit | 50 | Results per page (1–100) |
offset | 0 | Number of results to skip |
The response includes a hasMore boolean. Fetch the next page by incrementing offset:
// Fetch all notes (auto-pagination)
async function fetchAllNotes(token) {
const notes = [];
let offset = 0;
while (true) {
const res = await fetch(
`https://notes.krokanti.com/api/notes?limit=100&offset=${offset}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
const { notes: page, hasMore } = await res.json();
notes.push(...page);
if (!hasMore) break;
offset += 100;
}
return notes;
}Conflict Detection
The API uses optimistic concurrency control to prevent lost updates when multiple clients edit the same note simultaneously.
Include the clientUpdatedAt field in every PATCH request — set it to the updatedAt value of the note you last fetched. If another client has saved a newer version in the meantime, the API returns 409 Conflict with the server's current version so you can merge:
// PATCH with conflict detection
const res = await fetch(`https://notes.krokanti.com/api/notes/${id}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Updated title",
content: "<p>New content</p>",
clientUpdatedAt: note.updatedAt, // ISO 8601 string
}),
});
if (res.status === 409) {
const { serverNote } = await res.json();
// Merge your changes with serverNote, then retry with force: true
await fetch(`https://notes.krokanti.com/api/notes/${id}`, {
method: "PATCH",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({ content: merged, force: true }),
});
}Pass force: true to skip conflict detection and overwrite the server state unconditionally.
Secure Notes
Secure notes are encrypted client-side with AES-256-GCM before being stored. The API returns the raw encrypted blob — it cannot decrypt it for you (the PIN never leaves the browser).
- →GET /api/notes/:id — returns content as an encrypted JSON blob starting with {"v":1,"alg":"AES-256-GCM",...}
- →PATCH /api/notes/:id — blocked for secure notes (returns 403). Encryption can only be done through the web app.
- →Secure notes cannot be made public (returns 400 if isPublic: true is sent).
Error Codes
All error responses include a JSON body with an error string, and sometimes a code field for machine-readable subcodes.
| Status | Meaning | Common cause |
|---|---|---|
400 | Bad Request | Invalid request body (e.g. making a secure note public) |
401 | Unauthorized | Missing or invalid API token |
402 | Payment Required | Feature requires a Pro subscription (code: pro_required) |
403 | Forbidden | Note belongs to another user, or editing a secure note via API |
404 | Not Found | Note or folder does not exist |
409 | Conflict | clientUpdatedAt is stale — response includes serverNote for merging |
429 | Too Many Requests | Rate limit exceeded — check the Retry-After response header |
// Error response body
{ "error": "Rate limit exceeded" }
// With machine-readable subcode
{ "error": "Secure notes require Pro", "code": "pro_required" }
// Conflict response
{ "conflict": true, "serverNote": { "id": "...", "title": "...", "updatedAt": "..." } }Rate Limits
API token requests are rate-limited to 100 requests per minute per token (fixed window). Session-based requests (browser) are not rate-limited.
When the limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait before retrying.
# 429 response headers
HTTP/1.1 429 Too Many Requests
Retry-After: 42
Content-Type: application/json
{ "error": "Rate limit exceeded" }Code Examples
Replace kn_your_token_here with your actual API token from Settings → Connections.
cURL
# List your 10 most recently updated notes
curl "https://notes.krokanti.com/api/notes?limit=10" \
-H "Authorization: Bearer kn_your_token_here"
# Create a new note
curl -X POST "https://notes.krokanti.com/api/notes" \
-H "Authorization: Bearer kn_your_token_here"
# Update a note's title and content
curl -X PATCH "https://notes.krokanti.com/api/notes/NOTE_ID" \
-H "Authorization: Bearer kn_your_token_here" \
-H "Content-Type: application/json" \
-d '{ "title": "My Note", "content": "<p>Hello world</p>", "clientUpdatedAt": "2026-01-01T00:00:00.000Z" }'
# Search notes
curl "https://notes.krokanti.com/api/notes?search=meeting&limit=20" \
-H "Authorization: Bearer kn_your_token_here"
# Move a note to trash
curl -X PATCH "https://notes.krokanti.com/api/notes/NOTE_ID" \
-H "Authorization: Bearer kn_your_token_here" \
-H "Content-Type: application/json" \
-d '{ "action": "trash" }'JavaScript (fetch)
const BASE = "https://notes.krokanti.com/api";
const TOKEN = "kn_your_token_here";
const headers = {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
};
// List notes
const { notes, hasMore } = await fetch(`${BASE}/notes?limit=50`, { headers }).then(r => r.json());
// Create a note
const note = await fetch(`${BASE}/notes`, { method: "POST", headers }).then(r => r.json());
// Update title + content
await fetch(`${BASE}/notes/${note.id}`, {
method: "PATCH",
headers,
body: JSON.stringify({
title: "Shopping list",
content: "<ul><li>Milk</li><li>Eggs</li></ul>",
clientUpdatedAt: note.updatedAt,
}),
});
// Add a tag
await fetch(`${BASE}/notes/${note.id}`, {
method: "PATCH",
headers,
body: JSON.stringify({ tags: ["shopping", "weekly"] }),
});
// Pin the note
await fetch(`${BASE}/notes/${note.id}`, {
method: "PATCH",
headers,
body: JSON.stringify({ isPinned: true }),
});
// List folders
const { folders } = await fetch(`${BASE}/folders`, { headers }).then(r => r.json());
// Create a folder and move the note to it
const folder = await fetch(`${BASE}/folders`, {
method: "POST",
headers,
body: JSON.stringify({ name: "Groceries" }),
}).then(r => r.json());
await fetch(`${BASE}/notes/${note.id}`, {
method: "PATCH",
headers,
body: JSON.stringify({ folderId: folder.id }),
});Python (requests)
import requests
BASE = "https://notes.krokanti.com/api"
TOKEN = "kn_your_token_here"
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
# List notes
resp = requests.get(f"{BASE}/notes", headers=HEADERS, params={"limit": 50})
data = resp.json()
notes, has_more = data["notes"], data["hasMore"]
# Create a note
note = requests.post(f"{BASE}/notes", headers=HEADERS).json()
# Update it
requests.patch(
f"{BASE}/notes/{note['id']}",
headers=HEADERS,
json={
"title": "Meeting notes",
"content": "<p>Action items:</p><ul><li>Follow up with Alice</li></ul>",
"clientUpdatedAt": note["updatedAt"],
},
)
# Search and print titles
resp = requests.get(f"{BASE}/notes", headers=HEADERS, params={"search": "meeting"})
for n in resp.json()["notes"]:
print(n["title"])
# Delete a note permanently
requests.delete(f"{BASE}/notes/{note['id']}", headers=HEADERS)