Back to Super Productivity

APIs

docs/wiki/3.01-API.md

18.18.027.5 KB
Original Source

APIs

This reference is the entrypoint for working with Super Productivity's APIs. The app exposes four API systems: the Sync Server REST API (for data synchronization), the Local REST API (for controlling the desktop app from local scripts), the Plugin API (for extending the app), and URL Scheme Actions (for triggering a handful of task actions from outside the app, e.g. an iOS Shortcut). This reference summarizes all four; 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; URL Scheme Actions: electron/protocol-handler.ts and src/app/features/tasks/util/parse-app-uri-task-action.ts).

1. Sync Server REST API

The Sync Server is an operation-based synchronization service used by the built-in SuperSync provider. It is not WebDAV. See [[2.08-Choose-Sync-Backend]] and [[2.09-Configure-Sync-Backend]] for user-facing sync options.

Authentication and Authorization

Protected Sync Server endpoints use JWT Bearer authentication: Authorization: Bearer <token>. Production account creation and login use passkeys or emailed magic links; there are no password-based POST /api/register or POST /api/login routes. Email-only registration starts at POST /api/register/magic-link.

For the current endpoint purposes, token lifecycle, and security properties, use the authentication authority. The executable routes own exact request and response schemas.

Synchronization Endpoints

All sync routes require authentication. The stable surface includes operation upload/download, full-state upload, status, data deletion, restore-point listing, and restore. POST /api/sync/snapshot is an upload endpoint; there is no GET /api/sync/snapshot.

Use the Sync Server architecture for endpoint purposes and executable owners. Exact routes, limits, error codes, and wire schemas live in sync.routes.ts and the shared HTTP contract.


2. Plugin API

The Plugin API is exposed through a global PluginAPI object. Plugins are privileged extensions, not strongly sandboxed applications: host-side plugin.js code runs in the app renderer, and iframe plugins are same-origin with the host. See [[2.21-Manage-Plugins]] for the user trust model and [[3.05-Web-App-vs-Desktop]] for platform differences. Full types and the development guide live in packages/plugin-api/src/types.ts and docs/plugin-development.md.

Plugin API Categories

Data — Tasks:

  • getTasks(), getArchivedTasks(), getCurrentContextTasks() — Read tasks.
  • getSelectedTask() — Read the task selected in the task detail panel, or null.
  • getFocusedTask() — Read the currently focused task row, or null. Row focus is transient and is cleared when focus moves elsewhere, including into iframe side panels; use getSelectedTask() for persistent side-panel context.
  • 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) — Opens a plugin dialog and resolves to the clicked button label, or undefined if the dialog is dismissed. The host sanitizes htmlContent against an allowlist, keeping semantic HTML, form controls and layout styles while removing scripts, event handlers, unsafe URLs, inline <svg>, and styles containing url(.
  • showIndexHtmlAsView() — Shows 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).

Hooks (Events)

Plugins can register handlers for: taskCreated, taskComplete, taskUpdate, taskDelete, currentTaskChange, finishDay, languageChange, persistedDataChanged, action, anyTaskUpdate, projectListUpdate. Payload types are defined in plugin-api types (HookPayloadMap).

persistedDataChanged fires after the initial boot load on any change to this plugin's persisted data (local write, remote sync, bulk import). The payload is void; re-call loadSyncedData(key?) for fresh data. No replay-on-register, no ordering guarantee across rapid changes — handlers must be idempotent.

Plugin Data Types

Core types (Task, Project, Tag, ProjectFolder, etc.) and batch types (BatchUpdateRequest, BatchUpdateResult, BatchOperation, etc.) are in packages/plugin-api/src/types.ts.

Plugin Manifest

Plugins require manifest.json. Use PluginManifest as the current field contract rather than copying its required and optional fields into the wiki.

Plugin packages normally include plugin.js for host-side behavior. Iframe-only plugins may omit plugin.js when iFrame: true is set and index.html is included.

Plugin Permissions

  • nodeExecution: Required for executeNodeScript() (Electron only). Built-in and uploaded plugins may request it. The Electron main process shows a native consent dialog before granting access; uploaded plugins are labeled as unverified third-party code with full machine access.
  • Other permissions may gate specific API methods; see the plugin development guide.

Iframe API Surface and Trust Boundary

Iframe plugins receive a filtered window.PluginAPI object injected into index.html. That object is the supported interface for task/project/tag APIs, dialogs, notifications, navigation, persistence, registration, counters, and action dispatch. executeNodeScript is proxied through the host bridge when the desktop app grants nodeExecution.

The iframe uses allow-same-origin, which is required by the packaged desktop app. Same-origin iframe code can reach the parent directly, so the filtered postMessage bridge is a convenience and compatibility interface, not a security boundary. See docs/plugin-development.md for the current API surface and security considerations.


3. Local REST API

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. On a clean profile, set SP_FORCE_LOCAL_REST_API_TOKEN=<token> to choose the Bearer token explicitly; otherwise the app generates a temporary development token and prints it to the terminal (stdout, never the exportable app log).

Prerequisites

  • Electron desktop app only (not available on web or mobile)
  • Must be enabled in settings, unless using the development override
  • App must be running with renderer ready

Authentication

Every request must carry the access token as a Bearer token:

bash
# Keep the token out of the command line: arguments are visible to every process
# on the machine (`ps`, /proc). Put the header in a file only you can read.
(umask 077; printf 'Authorization: Bearer %s\n' "$SP_TOKEN" > ~/.sp-api-header)

# --noproxy is not optional: curl honours http_proxy/ALL_PROXY even for
# 127.0.0.1, so without it a configured proxy receives your token.
curl --noproxy 127.0.0.1 -H "@$HOME/.sp-api-header" http://127.0.0.1:3876/tasks

-H @file needs curl 7.55.0 or newer. On older versions put the same line in a 0600 config file as header = "Authorization: Bearer <token>" and pass it with --config. Either way the credential never reaches the process table.

The token is generated automatically when you enable the API, and is shown under Settings → Misc → Access Token, where you can also regenerate it. Regenerating invalidates the previous token immediately.

A missing, malformed, or wrong token returns 401 with the UNAUTHORIZED error code and a WWW-Authenticate: Bearer header. GET /health is the only exception and stays unauthenticated so tooling can probe liveness.

The examples below omit the Authorization header for brevity — every one of them needs it except /health.

What the Token Does and Does Not Protect

The token is stored per device in a 0600 file inside the [[3.06-User-Data]] folder and is never synced or exported. It closes the case where another account on the machine, or a sandboxed process that cannot read that file, talks to the API: before, every local process could read and modify all of your tasks.

Two limitations are deliberate, and worth knowing before you enable the API:

  • Any process running as you can read the token file, and therefore your tasks. On a single-user desktop that is most processes. File permissions are a boundary between accounts, not between programs you run.
  • The API does not authenticate itself to clients. The port is fixed and unprotected, so a process that binds 127.0.0.1:3876 while Super Productivity is not listening — before the app starts, or while the API is switched off — receives the Authorization header of the next client that connects, and can release the port and reuse that token afterwards. GET /health is unauthenticated, so an impostor can also answer a liveness probe convincingly. Closing this would require the server to prove its identity over a protected transport; retrying or moving the port does not help, because the impostor binds it while the app is down.

So: enable the API on a machine where you trust every account that can bind a loopback port, and treat the token as a credential — regenerate it if it may have been exposed.

Base URL

text
http://127.0.0.1:3876

Response Format

All responses are JSON with a consistent envelope:

typescript
// Success
{ "ok": true, "data": <response data> }

// Error
{ "ok": false, "error": { "code": "<ERROR_CODE>", "message": "<description>" } }

Health Check

MethodPathDescription
GET/healthCheck if server is running and renderer is ready

Response:

json
{ "ok": true, "data": { "server": "up", "rendererReady": true } }

Task Endpoints

MethodPathDescription
GET/tasksList tasks (with optional filters)
GET/tasks/:idGet task by ID
POST/tasksCreate task
PATCH/tasks/:idUpdate task
DELETE/tasks/:idDelete task
POST/tasks/:id/startStart task (set as current)
POST/tasks/:id/archiveArchive task
POST/tasks/:id/restoreRestore archived task

GET /tasks Query Parameters:

ParameterTypeDescription
querystringFilter by title (case-insensitive, contains)
projectIdstringFilter by project ID
tagIdstringFilter by tag ID. Use TODAY for tasks scheduled for today
includeDonebooleanInclude completed tasks (default: false)
sourcestring"active" | "archived" | "all" (default: "active")

Examples:

bash
# 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"

# List tasks scheduled for today
curl "http://127.0.0.1:3876/tasks?tagId=TODAY"

# 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:

FieldNotes
title (required)Non-empty string
parentIdCreate 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.
subTaskIdsNot supported on create — returns 400 UNSUPPORTED_FIELD. Create the parent first, then create each child with parentId.
other allowed fieldsnotes, 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.

Changing projectId on a top-level task moves the task and all of its subtasks atomically. The destination must be an existing active project; missing or archived destinations return 404 PROJECT_NOT_FOUND. A subtask's projectId is inherited from its parent and cannot be changed directly (400 UNSUPPORTED_FIELD); echoing its unchanged inherited value is accepted for GET-to-PATCH round trips. A project move can be combined with other writable fields in the same PATCH request.


Task Control Endpoints

MethodPathDescription
GET/statusGet current task and task count
GET/task-control/currentGet current task
POST/task-control/currentSet current task
POST/task-control/stopStop current task

POST /task-control/current Body:

json
{ "taskId": "task-id" }
// or to clear:
{ "taskId": null }

Examples:

bash
# 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

Project Endpoints

MethodPathDescription
GET/projectsList projects

GET /projects Query Parameters:

ParameterTypeDescription
querystringFilter by title (case-insensitive, contains)

Example:

bash
# List all projects
curl http://127.0.0.1:3876/projects

Tag Endpoints

MethodPathDescription
GET/tagsList tags

GET /tags Query Parameters:

ParameterTypeDescription
querystringFilter by title (case-insensitive, contains)

Example:

bash
# List all tags
curl http://127.0.0.1:3876/tags

Local REST API Error Codes

CodeHTTP StatusDescription
UNAUTHORIZED401Missing, malformed, or wrong access token
TASK_NOT_FOUND404Task does not exist
PROJECT_NOT_FOUND404Target project does not exist or is archived
INVALID_INPUT400Invalid request body, or a field has the wrong value type (see error.details)
UNSUPPORTED_FIELD400Field cannot be changed through this endpoint
NOT_FOUND404Route not found
INTERNAL_ERROR500Internal server error

Local REST API Notes

  • Electron only: The Local REST API is only available in the desktop app.
  • Auth: Bearer access token on every request except /health, on top of localhost-only binding.
  • Port: Fixed at 3876; not configurable in v1.
  • Timeout: 15 seconds for renderer responses.

4. URL Scheme Actions

Two task actions are reachable through the app's existing custom URL scheme, without opening the app UI first: add a task and complete a task (matched by title). This is intended for external automation — most notably an iOS Shortcut's "Open URLs" action, since Apple Shortcuts has no other lightweight way to reach a non-App-Intents app — but works anywhere a URL can be opened.

Currently iOS and desktop only. Android support is not yet implemented (the app doesn't declare the intent-filters these actions need) and is tracked as a follow-up.

Platforms and Schemes

PlatformSchemeExample
iOS (Capacitor)com.super-productivity.app://com.super-productivity.app://create-task?title=Buy%20milk
Desktop (Electron)superproductivity://superproductivity://create-task?title=Buy%20milk

Both schemes are already registered for other purposes (OAuth callbacks on mobile; global-shortcut and window-visibility actions on desktop) — these are two more recognized actions on the same schemes, not a new registration.

Actions

Add a task — action create-task on both platforms (desktop also accepts the title as a path segment, create-task/<title>, matching the pre-existing desktop action shipped since v14.2.4 — whose title handling this changes, see the Short Syntax note below). Not add-task: desktop's add-task protocol action already exists and does something unrelated (opens the quick-add-task input bar).

Query parameterRequiredDescription
titleYesThe new task's title. Surrounding whitespace is trimmed; whitespace-only is rejected (no task is added). Max 300 characters (after trimming).
notesNoTask notes. Max 100,000 characters.
projectIdNoTarget project ID. The action fails with an error (no task added) if it doesn't match an existing project.

Short Syntax is not parsed (see [[3.04-Short-Syntax]]): tokens like +Project, #tag, or @date in the title are stored literally rather than creating/assigning a project, tags, or a due date (surrounding whitespace is still trimmed). A URL is untrusted external content, so — as with the email-drop import — it can't silently create tags/projects or override an explicit projectId. Use the projectId parameter to set the project.

Behavior change: the pre-existing desktop create-task action (shipped since v14.2.4) previously parsed Short Syntax on the title, like a manually typed task. As of this change the title is stored literally on both platforms. Pass projectId for the project; +/#/@ tokens are no longer interpreted.

Without an explicit projectId, the task is added to whichever project/tag/context is currently active in the app — which on a cold launch is whatever was last open, not necessarily the Inbox. Pass projectId for deterministic placement.

Complete a task — action complete-task on both platforms.

Query parameterRequiredDescription
titleYesMatched (trimmed, case-insensitive) against non-subtask, non-done tasks in active (non-archived) projects. Only a lookup needle, never stored, so it is not subject to the create-task length cap.

An exact case-insensitive title match is preferred; if there is none, a case-insensitive substring match is used, but only when exactly one task matches. If no task matches, or more than one substring match is equally plausible, the action fails with an error rather than guessing.

Examples:

text
com.super-productivity.app://create-task?title=Buy%20milk&notes=2%25%20fat&projectId=proj-1
com.super-productivity.app://complete-task?title=Buy%20milk

superproductivity://create-task?title=Buy%20milk
superproductivity://complete-task?title=Buy%20milk

Behavior Notes

  • Both platforms show a snack notification on success (task added/completed) or failure (no matching/ambiguous task, unknown project) — there is no other feedback channel for a URL triggered from outside the app.
  • An action received during a cold launch (app not already running) is queued until the app's own data has finished loading, so it never races initial hydration.
  • Not available in the web (browser) build, or on Android yet — see [[3.05-Web-App-vs-Desktop]].

5. Versioning and Compatibility

Schema Versioning (Sync)

Operations and snapshots carry schemaVersion. The current and minimum supported versions, together with the compatibility policy, are defined in packages/shared-schema/src/schema-version.ts. Newer schemas are blocked rather than interpreted using a copied "version skip" allowance.

Vector Clocks

Sync uses bounded vector clocks for conflict resolution. The canonical size and pruning rules are in packages/sync-core/src/vector-clock.ts; server-side input sanitization is in packages/super-sync-server/src/sync/sync.types.ts. The server may temporarily accept a larger clock during conflict resolution before pruning it, so an input-validation limit is not the stored clock size.

API Validation (Sync Server)

  • Operation IDs: 1–255 characters. Client IDs: alphanumeric, underscore, hyphen; max 255.
  • Entity types: 1–255 characters. Schema version: 1–100.
  • Payload validation by op type (CRT, UPD, DEL, MOV, BATCH, SYNC_IMPORT, etc.); see validatePayload in sync.types.ts.

General Notes

  • Sync Server: Production-oriented (JWT, rate limiting, CORS, Helmet). Multi-instance deployment has limitations (e.g. passkey challenge storage in memory; snapshot generation locks); single-instance is the typical deployment.
  • Plugin API: Privileged and not strongly sandboxed; iframe, web, and Electron have different capabilities. See [[2.21-Manage-Plugins]], [[3.05-Web-App-vs-Desktop]], and the repository plugin guide.
  • Local REST API: Desktop-only, localhost-bound, and protected by a Bearer access token on every request except /health. For automation and scripting. See [[3.05-Web-App-vs-Desktop]] for platform differences.
  • URL Scheme Actions: iOS (Capacitor) and desktop (Electron) only, not the web build or Android. No authentication — any process able to open a URL on the device can trigger it, same trust model as the existing OAuth-callback/global-shortcut actions on the same schemes.
  • Further documentation: Sync Server README and auth docs in 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.