Skip to main content
Version: Next (main)

HTTP API

The web app talks to the server over JSON. That API is usable from scripts, but it is not yet a stable public API: it can change in any minor version. Pin the PeckList version if you script against it, and read the changelog when upgrading.

Swagger and OpenAPI​

Every server describes its own API: the OpenAPI 3.1 document is at /api/openapi/v1.json, and /api/docs is a Swagger UI page that reads it. There you can browse the routes by area and try them: press Authorize and paste an API key (below), or just use the page in a browser that is signed in to the server. Point other tools at the document: code generators, Postman or Bruno, an AI assistant's HTTP tool, a ChatGPT Custom GPT action.

Both are public, like the source code: they show what the API offers, never anyone's data. API_DOCS=off turns them off.

Signing in from a script​

The easy way is an API key: make one in Settings > API keys with a name, an expiry (or none) and only the permissions the script needs, copy it (it is shown once), and send it with every request:

curl -H 'Authorization: Bearer pl_...' https://tasks.example.com/api/sync \
-H 'Content-Type: application/json' -d '{"schemaVersion":16,"cursor":null,"changes":[]}'
PermissionLets the key
tasks:readSync without changes (everything you can see), /api/events, task history
tasks:writePush changes through /api/sync (comes with tasks:read)
files:readDownload attachments
files:writeUpload attachments (comes with files:read)
notificationsList notifications, mark them read, clear them
adminThe admin API, as far as your role allows (admins only)

A key works as you, so it never sees more than you do. It cannot do anything outside its permissions: no sign-in or password routes, no account settings, no friends, and it cannot make or revoke keys. Anything else answers 403 {"error":"scope"}; an unknown, revoked or expired key answers 401. The server keeps only a hash of the secret, and when each key was last used (to within a few minutes). Making and revoking keys go in the event log.

Route
GET /api/account/api-keysYour keys: id, name, scopes, createdAt, expiresAt, lastUsedAt, revokedAt
POST /api/account/api-keys{"name","scopes","expiresAt"} (expiresAt null: never); answers {"key","token"} once
DELETE /api/account/api-keys/{id}Revoke one at once

These three take a signed-in session, never a key. With AUTH_MODE=none there are no keys.

Sessions are cookies. Sign in, keep the cookie, send it back:

curl -c cookies.txt -H 'Content-Type: application/json' \
-d '{"username":"admin","password":"..."}' https://tasks.example.com/auth/login
curl -b cookies.txt https://tasks.example.com/api/admin/stats

An account that must change its password gets 403 from everything but /auth/password until it does. Every admin route needs the matching role.

Open routes​

Route
GET /healthz200 Healthy or 503
GET /api/infoName, version, schema version and time zone
GET /api/openapi/v1.json, GET /api/docsThe API's OpenAPI document and its Swagger UI page (unless API_DOCS=off)
GET /auth/providersThe single sign-on providers offered on the sign-in page

Signing in​

Route
POST /auth/login{"username","password"}; sets the session cookie
POST /auth/logoutEnds the session
GET /auth/meThe signed-in account
POST /auth/password{"current","next"}
GET /auth/oidc/{name}/loginStarts a single sign-on; the provider calls back to /auth/oidc/{name}/callback
GET /auth/link/{token}A one-time sign-in link
POST /auth/signup{"username","password","invite"}; makes the account and signs in (see below)
GET /auth/invites/{token}200 {"expires"} while an invite link works, else 404

/auth/providers also says who may sign up (registration: open, invite or closed), and localLogin whether passwords work. With LOCAL_LOGIN=disabled, /auth/login and /auth/password answer 403 local_login_disabled. A refused sign-up answers 403 (registration_closed, invite_required, local_login_disabled), 409 username_taken or 400 (invalid_username, too_short, invite_invalid). Sign-up shares the sign-in rate limit.

The app​

Route
POST /api/syncSend changes and receive others' since a cursor
GET /api/eventsServer-sent events: something changed, sync now
GET /api/account/sign-in, GET /api/account/statsYour sign-in methods, your stats
/api/friends/..., GET /api/users/lookupFriend links and requests, finding people
GET /api/notifications, POST .../read, .../clearYour notifications; mark read or clear ({"ids": [...]}, or null for all)
GET /api/tasks/{id}/historyA task's changes, newest first: at, actorId, field, before, after (JSON values); 404 for a task you cannot see
GET /api/push/key, POST /api/push/subscriptions, POST /api/push/unsubscribeWeb Push: the server's VAPID key, this browser's subscription

Admin (/api/admin)​

AreaRoutes
InvitesGET/POST /invites, DELETE /invites/{id}
UsersGET/POST /users, DELETE /users/{id}, POST /users/{id}/restore, POST /users/{id}/remove-now, PUT /users/{id}/disabled, PUT /users/{id}/role, POST /users/{id}/password, POST /users/{id}/login-link, POST /users/{id}/impersonate
Sign-inGET/POST/DELETE /users/{id}/identities..., GET /providers, POST /providers/{name}/unbind-all, PUT/DELETE /providers/{name}/migration-window, GET /pending-signins, POST /pending-signins/{provider}/{subject}/approve
SharingGET /users/{id}/friends, POST /friendships, DELETE /friendships/{a}/{b}, GET /users/{id}/shared, POST /users/{id}/hand-over
GroupsGET/POST /groups, PATCH/DELETE /groups/{id}, PUT/DELETE /groups/{id}/members/{accountId}
SettingsGET/PUT /settings, POST /restart, GET/PUT /setup
BackupsGET/POST /backups, GET /backups/{name}, GET /export, GET /users/{id}/export, POST /restore, POST /users/restore
LogsGET /logging, GET /debug-dump, GET /audit, GET /audit.csv
StatsGET /stats

The workspace data that /api/sync carries is described by the workspace schema. Each pushed change is checked on its own: one the server cannot take as it is (malformed, or stamped more than an hour ahead of its clock) is listed in the response's invalid with its index, id, code (invalid_change or clock_drift) and reason, and the rest of the push is stored. The response's serverTime lets a device tell when its own clock is off.