docs/wiki/3.01-API.md
This reference is the entrypoint for working with Super Productivity's APIs. The app exposes three API systems: the Sync Server REST API (for data synchronization), the Local REST API (for controlling the desktop app from local scripts), and the Plugin API (for extending the app). This reference summarizes all three; full request/response schemas and examples live in the repository (Sync Server: packages/super-sync-server/; Plugin API: packages/plugin-api/ and docs/plugin-development.md; Local REST API: src/app/core/electron/local-rest-api-handler.service.ts).
The Sync Server is a custom, operation-based synchronization service (event-sourcing style). It is not WebDAV. It is intended for use with the built-in SuperSync sync provider. SuperSync is very new and is still in beta. See [[2.08-Choose-Sync-Backend]] and [[2.09-Configure-Sync-Backend]] for user-facing sync options.
All sync endpoints require JWT Bearer authentication: Authorization: Bearer <token>.
JWT_SECRET (minimum 32 characters).userId, email, tokenVersion (for invalidation).Traditional:
POST /api/register — User registration (email verification required).POST /api/login — Password login.POST /api/verify-email — Email verification.POST /api/replace-token — Replace compromised JWT (requires current auth).Passkey (WebAuthn):
POST /api/register/passkey/options — Registration options.POST /api/register/passkey/verify — Verify registration.POST /api/login/passkey/options — Authentication options.POST /api/login/passkey/verify — Verify authentication.POST /api/recover/passkey — Request passkey recovery (e.g. magic link).POST /api/recover/passkey/options — Recovery registration options.POST /api/recover/passkey/complete — Complete recovery.Magic link:
POST /api/login/magic-link — Request magic link email.POST /api/login/magic-link/verify — Verify magic link token.Account:
DELETE /api/account — Delete user and all data (requires auth).All require authentication.
Operations:
POST /api/sync/ops — Upload operations (incremental). Request: ops[], clientId, optional lastKnownServerSeq, optional requestId (deduplication).GET /api/sync/ops?sinceSeq={seq}&limit={limit}&excludeClient={clientId} — Download operations. Query: sinceSeq (required), limit (default 500, max 1000), excludeClient (optional).Snapshots:
GET /api/sync/snapshot — Get full state snapshot.POST /api/sync/snapshot — Upload full state (backup/repair/migration). Body limit 30 MB (compressed).Status and maintenance:
GET /api/sync/status — Sync status and storage info.DELETE /api/sync/data — Delete all sync data for the user (e.g. encryption password change).GET /api/sync/restore-points?limit={limit} — List restore points (limit 1–100).GET /api/sync/restore/{serverSeq} — Get state snapshot at a specific sequence.Health:
GET /health — Database connectivity check (no auth). Returns 200 with { status: 'ok', db: 'connected' } or 503 on failure.id, clientId, actionType, opType, entityType, entityId/entityIds, payload, vectorClock, timestamp, schemaVersion, optional isPayloadEncrypted. Response: results[] (per-op accepted/rejected + serverSeq), optional newOps, latestSeq, optional hasMorePiggyback, optional deduplicated.ops, hasMore, latestSeq, optional gapDetected, optional latestSnapshotSeq, optional snapshotVectorClock, serverTime.state, serverSeq, generatedAt. Upload snapshot body: state, clientId, reason, vectorClock, schemaVersion, optional isPayloadEncrypted.latestSeq, devicesOnline, optional snapshotAge, storageUsedBytes, storageQuotaBytes.Exact schemas (Zod on server, TypeScript types in repo) are in packages/super-sync-server/src/sync/sync.types.ts and sync.routes.ts.
Structured error codes (for client handling) include: VALIDATION_FAILED, INVALID_OP_ID, INVALID_OP_TYPE, INVALID_ENTITY_TYPE, INVALID_ENTITY_ID, INVALID_PAYLOAD, PAYLOAD_TOO_LARGE, INVALID_VECTOR_CLOCK, INVALID_CLIENT_ID, CONFLICT_CONCURRENT, CONFLICT_SUPERSEDED, DUPLICATE_OPERATION, RATE_LIMITED, STORAGE_QUOTA_EXCEEDED, ENCRYPTED_OPS_NOT_SUPPORTED, SYNC_IMPORT_EXISTS, INTERNAL_ERROR. Defined in sync.types.ts (SYNC_ERROR_CODES).
Content-Encoding: gzip. Android can send base64-encoded gzip with Content-Transfer-Encoding: base64.Configurable via environment/config. Typical allowed headers include Authorization, Content-Type, Content-Encoding, X-Expected-Rev, X-Force-Overwrite, X-Requested-With. Credentials supported.
Operations, devices, and related validation data are retained for 45 days. Older data is purged.
Upload ops accepts optional requestId. Repeated requests with the same requestId receive cached results (and fresh piggybacked ops if applicable), so clients can retry safely without duplicating operations.
Payloads can be encrypted client-side; server stores them as opaque blobs. Restore-at-sequence is not supported when operations are encrypted (ENCRYPTED_OPS_NOT_SUPPORTED).
The Plugin API is exposed to plugins via a global PluginAPI object. Plugins run in a sandboxed environment (VM or iframe). See [[3.05-Web-App-vs-Desktop]] for web limitations (e.g. Node-only plugins disabled, iframe API restrictions). Full types and the development guide: packages/plugin-api/src/types.ts, docs/plugin-development.md.
Data — Tasks:
getTasks(), getArchivedTasks(), getCurrentContextTasks() — Read tasks.addTask(taskData), updateTask(taskId, updates), deleteTask(taskId) — Create/update/delete.batchUpdateForProject(request) — Batch create/update/delete/reorder for a project.reorderTasks(taskIds, contextId, contextType) — Reorder tasks.Data — Projects:
getAllProjects(), addProject(projectData), updateProject(projectId, updates).Data — Tags:
getAllTags(), addTag(tagData), updateTag(tagId, updates).Data — Simple counters:
setCounter(id, value), getCounter(id), incrementCounter(id, incrementBy), decrementCounter(id, decrementBy), deleteCounter(id), getAllCounters().UI:
showSnack(snackCfg), notify(notifyCfg) — Notifications.openDialog(dialogCfg), showIndexHtmlAsView() — Dialogs and plugin UI.Registration (main plugin context only; not in iframe):
registerHeaderButton(config), registerMenuEntry(config), registerShortcut(config), registerSidePanelButton(config), registerHook(hook, handler).Persistence:
persistDataSynced(dataStr), loadSyncedData(), getConfig() — Plugin-specific storage and config.Advanced:
executeNodeScript(request) — Run Node.js scripts (Electron only, nodeExecution permission and user consent).dispatchAction(action) — Dispatch NgRx actions (allowed subset).downloadFile(filename, data), isWindowFocused(), onWindowFocusChange(handler).Plugins can register handlers for: taskCreated, taskComplete, taskUpdate, taskDelete, currentTaskChange, finishDay, languageChange, persistedDataUpdate, action, anyTaskUpdate, projectListUpdate. Payload types are defined in plugin-api types (HookPayloadMap).
Core types (Task, Project, Tag, ProjectFolder, etc.) and batch types (BatchUpdateRequest, BatchUpdateResult, BatchOperation, etc.) are in packages/plugin-api/src/types.ts.
Plugins require manifest.json: name, id, manifestVersion, version, minSupVersion, optional description, hooks, permissions, optional iFrame, isSkipMenuEntry, type, assets, icon, nodeScriptConfig, sidePanel, jsonSchemaCfg. See PluginManifest in the repo.
executeNodeScript() (Electron only; user consent). See [[3.05-Web-App-vs-Desktop]] and [[3.06-User-Data]] for file-system and desktop-only behavior.When a plugin uses an iframe UI, the following are not available: registerHeaderButton, registerMenuEntry, registerSidePanelButton, registerShortcut, registerHook, execNodeScript. Iframe content is subject to CSP (e.g. no external scripts; same-origin or inlined only).
The Local REST API allows external scripts and tools to interact with a running Super Productivity desktop app. It runs on http://127.0.0.1:3876 and is disabled by default. Enable it in Settings → Misc → Enable local REST API.
For development scripts, SP_FORCE_LOCAL_REST_API=1 npm start starts the local REST API without changing the persisted user setting. This override only works in NODE_ENV=DEV.
No authentication required. The API only accepts connections from localhost (127.0.0.1).
http://127.0.0.1:3876
All responses are JSON with a consistent envelope:
// Success
{ "ok": true, "data": <response data> }
// Error
{ "ok": false, "error": { "code": "<ERROR_CODE>", "message": "<description>" } }
| Method | Path | Description |
|---|---|---|
| GET | /health | Check if server is running and renderer is ready |
Response:
{ "ok": true, "data": { "server": "up", "rendererReady": true } }
| Method | Path | Description |
|---|---|---|
| GET | /tasks | List tasks (with optional filters) |
| GET | /tasks/:id | Get task by ID |
| POST | /tasks | Create task |
| PATCH | /tasks/:id | Update task |
| DELETE | /tasks/:id | Delete task |
| POST | /tasks/:id/start | Start task (set as current) |
| POST | /tasks/:id/archive | Archive task |
| POST | /tasks/:id/restore | Restore archived task |
GET /tasks Query Parameters:
| Parameter | Type | Description |
|---|---|---|
query | string | Filter by title (case-insensitive, contains) |
projectId | string | Filter by project ID |
tagId | string | Filter by tag ID |
includeDone | boolean | Include completed tasks (default: false) |
source | string | "active" | "archived" | "all" (default: "active") |
Examples:
# List all active tasks
curl http://127.0.0.1:3876/tasks
# List archived tasks
curl "http://127.0.0.1:3876/tasks?source=archived"
# Search tasks containing "meeting"
curl "http://127.0.0.1:3876/tasks?query=meeting"
# Create a task
curl -X POST http://127.0.0.1:3876/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Buy groceries", "projectId": "INBOX_PROJECT"}'
# Create a subtask (parent must be a top-level task; the subtask inherits
# the parent's projectId and cannot have its own tags)
curl -X POST http://127.0.0.1:3876/tasks \
-H "Content-Type: application/json" \
-d '{"title": "milk", "parentId": "PARENT_TASK_ID"}'
# Update a task
curl -X PATCH http://127.0.0.1:3876/tasks/task-id \
-H "Content-Type: application/json" \
-d '{"title": "Updated title"}'
# Archive a task
curl -X POST http://127.0.0.1:3876/tasks/task-id/archive
POST /tasks body fields:
| Field | Notes |
|---|---|
title (required) | Non-empty string |
parentId | Create the task as a subtask of this top-level task. Returns 404 if parent missing, 400 if parent is itself a subtask. The new subtask inherits the parent's projectId and never has its own tags — supplying projectId or tagIds together with parentId returns 400 UNSUPPORTED_FIELD. |
subTaskIds | Not supported on create — returns 400 UNSUPPORTED_FIELD. Create the parent first, then create each child with parentId. |
| other allowed fields | notes, isDone, timeEstimate, timeSpent, projectId, tagIds, dueDay, dueWithTime, plannedAt |
PATCH /tasks/:id — restricted fields:
parentId and subTaskIds cannot be set via PATCH (returns 400 UNSUPPORTED_FIELD). Re-parenting an existing task is not supported by this API; delete and recreate the task instead.
| Method | Path | Description |
|---|---|---|
| GET | /status | Get current task and task count |
| GET | /task-control/current | Get current task |
| POST | /task-control/current | Set current task |
| POST | /task-control/stop | Stop current task |
POST /task-control/current Body:
{ "taskId": "task-id" }
// or to clear:
{ "taskId": null }
Examples:
# Get current task
curl http://127.0.0.1:3876/task-control/current
# Set current task
curl -X POST http://127.0.0.1:3876/task-control/current \
-H "Content-Type: application/json" \
-d '{"taskId": "task-id"}'
# Stop current task
curl -X POST http://127.0.0.1:3876/task-control/stop
| Method | Path | Description |
|---|---|---|
| GET | /projects | List projects |
GET /projects Query Parameters:
| Parameter | Type | Description |
|---|---|---|
query | string | Filter by title (case-insensitive, contains) |
Example:
# List all projects
curl http://127.0.0.1:3876/projects
| Method | Path | Description |
|---|---|---|
| GET | /tags | List tags |
GET /tags Query Parameters:
| Parameter | Type | Description |
|---|---|---|
query | string | Filter by title (case-insensitive, contains) |
Example:
# List all tags
curl http://127.0.0.1:3876/tags
| Code | HTTP Status | Description |
|---|---|---|
TASK_NOT_FOUND | 404 | Task does not exist |
INVALID_INPUT | 400 | Invalid request body |
NOT_FOUND | 404 | Route not found |
INTERNAL_ERROR | 500 | Internal server error |
3876; not configurable in v1.packages/shared-schema).schemaVersion; server validates and may reject unsupported versions.Sync uses vector clocks for conflict resolution. Server validates/sanitizes clocks: max 50 entries; keys max 255 chars; values 0–10,000,000. Invalid entries are stripped.
validatePayload in sync.types.ts.packages/super-sync-server/; Plugin API and examples in packages/plugin-api/, docs/plugin-development.md, and example plugins under packages/plugin-dev/; Local REST API types in electron/shared-with-frontend/local-rest-api.model.ts.