Krokanti Notes
REST API

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.

Base URL:https://notes.krokanti.com/api
Rate limit:100 req/min per token

Pro 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"
Note: Token management (create, list, revoke) is only available via the browser — you cannot manage tokens through the API itself.

Notes

GET/api/notes

List all notes for the authenticated user. Supports filtering, sorting, and pagination.

ParameterTypeDescription
searchstringFull-text search across title and content
folderIdstringFilter by folder ID
tagstringFilter by tag name (exact match)
trashedbooleanSet to "true" to list trashed notes instead of active ones
sortByupdatedAt | createdAt | titleSort field (default: "updatedAt")
sortOrderasc | descSort direction (default: "desc")
limitnumberResults per page — 1 to 100 (default: 50)
offsetnumberNumber 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)
POST/api/notes

Create 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
GET/api/notes/:id

Fetch a single note by ID.

ParameterTypeDescription
idrequiredstringNote UUID
  • Returns 404 if the note does not exist or belongs to another user
  • Secure note content is returned as an encrypted blob
PATCH/api/notes/:id

Update a note's title, content, tags, folder, or metadata. Supports conflict detection.

ParameterTypeDescription
idrequiredstringNote UUID (path parameter)
titlestringNew title
contentstringNew HTML content
tagsstring[]Replace the full tags array
folderIdstring | nullMove to folder (null to remove from folder)
isPinnedbooleanPin or unpin the note (owner only)
isPublicbooleanPublish or unpublish the note (owner only)
actiontrash | restoreMove to trash or restore from trash (owner only)
clientUpdatedAtstring (ISO 8601)Last known updatedAt — used for conflict detection
forcebooleanSkip 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
DELETE/api/notes/:id

Permanently delete a note. This is irreversible — use action: 'trash' via PATCH for soft delete.

ParameterTypeDescription
idrequiredstringNote UUID

Folders

GET/api/folders

List all folders for the authenticated user.

  • Response: { folders: Folder[] }
POST/api/folders

Create a new folder.

ParameterTypeDescription
namerequiredstringFolder name
  • Response status: 201 Created
PATCH/api/folders/:id

Rename a folder.

ParameterTypeDescription
idrequiredstringFolder UUID (path parameter)
namerequiredstringNew folder name
DELETE/api/folders/:id

Delete a folder. Notes inside the folder are not deleted — they become unfoldered.

ParameterTypeDescription
idrequiredstringFolder UUID

Pagination

The GET /api/notes endpoint uses offset-based pagination. Control it with two query parameters:

ParameterDefaultDescription
limit50Results per page (1–100)
offset0Number 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.

StatusMeaningCommon cause
400Bad RequestInvalid request body (e.g. making a secure note public)
401UnauthorizedMissing or invalid API token
402Payment RequiredFeature requires a Pro subscription (code: pro_required)
403ForbiddenNote belongs to another user, or editing a secure note via API
404Not FoundNote or folder does not exist
409ConflictclientUpdatedAt is stale — response includes serverNote for merging
429Too Many RequestsRate 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)