REST API

The JSON API the web app, desktop app and browser extension use. Everything they do, you can do with a token.

Requests

Send JSON to https://is.underwhelm.ing/api/v1 and read JSON back. Every request after sign-in carries a token:

curl https://is.underwhelm.ing/api/v1/tasks \
  -H "Authorization: Bearer <token>" \
  -H "X-Aquamarine-Account: <workspace id>"

Make a token in the app under Settings → Devices, or with POST /settings/devices. Native clients get one from the sign-in endpoints below. X-Aquamarine-Account (or an account_id parameter) picks the workspace; it can be left out when the person has one workspace or the token is scoped, since a scoped token only works in the workspace it was made in.

Clients that keep a local copy poll GET /changes?since=<cursor> to catch up with what changed elsewhere.

Scopes

A token made without a scope list can do everything its owner can. A token with a list is refused anything outside it. GET requests need the read scope of their family; other methods need the write scope.

ScopeCovers
read:notesRead pages, their bodies, tags and links
write:notesChange pages, their bodies, tags and links
read:tasksRead tasks, subtasks, captures and the parser
write:tasksChange tasks, subtasks, captures and the parser
read:projectsRead projects
write:projectsChange projects
read:routinesRead routines, their steps and runs
write:routinesChange routines, their steps and runs
read:tagsRead the workspace's tags
write:tagsChange the workspace's tags
read:calendarRead calendar events
write:calendarChange calendar events
read:focusRead focus sessions
write:focusChange focus sessions
read:notificationsRead notifications
write:notificationsChange notifications
read:workspaceRead the workspace's name, members' names, preferences, today, the week report and the change feed
write:workspaceChange the workspace's name, members' names, preferences, today, the week report and the change feed
read:settingsRead preferences, devices, passkeys, webhooks, members, invitations and connections
write:settingsChange preferences, devices, passkeys, webhooks, members, invitations and connections
read:extensionsRead the records every extension stores, such as recipes and people
write:extensionsChange the records every extension stores, such as recipes and people
publish:notesPublish and unpublish pages

Each endpoint below lists what a scoped token needs. "Full token" means scoped tokens are refused.

Errors

Errors answer { "error": "…" } with these statuses:

401No token, or one that has been signed out.
403The token lacks a scope (the body lists them under scopes), the profile is not set up, or the workspace is not the caller's.
404No such record in this workspace.
409The action needs confirmation, such as taking a published page down. The body says what would be affected; repeat the request with the flag it names.
422The record did not validate. error holds the messages.
429Too many sign-in attempts. Try again in a few minutes.

Server

GET /server Full token

Which sign-in methods this server offers (github, email) and its origin. No sign-in needed.

Sign-in

POST /session/code None

Emails a sign-in code to email_address. Answers 202. No sign-in needed.

POST /session/github/device None

Starts GitHub's device flow and answers GitHub's device code and verification URL. No sign-in needed.

POST /session/github/token None

Polls the device flow with device_code. Answers 202 until the person approves on GitHub, then the device token. No sign-in needed.

POST /session/github/web None

Starts GitHub sign-in in the system browser. Answers a code and the url to open. No sign-in needed.

POST /session/github/web/token None

Polls a browser sign-in with code. Answers 202 until the browser finishes, then the device token, once. No sign-in needed.

DELETE /session None

Signs out the token making the request.

POST /session None

Exchanges an emailed code for a device token. Takes an optional device_name. Answers the token, the user and their workspaces. No sign-in needed.

Onboarding

GET /onboarding read:workspace

Onboarding state: whether it is done, the current step, the workday and look preferences, and project names.

PUT, PATCH /onboarding write:settings

Saves one onboarding step. step is workday, look or first; first takes projects (up to five names) and starter, and marks the workspace onboarded. at records the step the person is on.

Profile

POST /profile Full token

Sets up a new identity's profile from name and timezone, which also makes their personal workspace. Answers 409 when the profile exists.

Workspaces

POST /accounts write:workspace

Creates a workspace named name with the caller as owner.

DELETE /accounts/:id write:workspace

Deletes a workspace. Owner only. Answers 409 with its live publications unless confirm_takedown is true.

Me

GET /me read:workspace

The signed-in user, the current workspace with every preference, the user's workspaces, the change feed cursor and tag colours.

PATCH /me write:settings

Writes any of the workspace preferences under preferences and answers the same shape as GET /me.

Change feed

GET /changes Any read: scope

Records changed since the since cursor, up to 500 at a time. Deleted records come back with record null. more is true when another page waits.

Tasks

POST /tasks/:id/complete write:tasks

Marks a task done.

POST /tasks/:id/reopen write:tasks

Reopens a done task.

POST /tasks/:id/pause write:tasks

Pauses a task.

POST /tasks/:id/resume write:tasks

Resumes a paused task.

POST /tasks/:id/unschedule write:tasks

Removes a task's scheduled time.

POST /tasks/reorder write:tasks

Saves the order of ids, optionally with a board_column. Moving into column 3 completes a task; moving out of it reopens one.

POST /tasks/autoplan write:tasks

Schedules unscheduled open tasks into the free time of date (default today) and answers how many were placed.

GET /tasks read:tasks

Top-level tasks, up to 500. Filter with status (inbox, planned, done) or date (YYYY-MM-DD, tasks scheduled that day).

POST /tasks write:tasks

Creates a task. Fields include title, notes, project_id, parent_id, page_id, priority, duration_minutes, due_on, scheduled_at, recurrence, board_column, location and tags.

GET /tasks/:id read:tasks

A task with its subtasks.

PUT, PATCH /tasks/:id write:tasks

Changes a task's fields. Takes the same fields as create.

DELETE /tasks/:id write:tasks

Deletes a task.

Projects

POST /projects/:id/archive write:projects

Archives a project.

POST /projects/:id/restore write:projects

Restores an archived project.

POST /projects/reorder write:projects

Saves the order of ids.

GET /projects read:projects

Active projects. archived=1 includes archived ones.

POST /projects write:projects

Creates a project from name, color and keyword.

GET /projects/:id read:projects

A project.

PUT, PATCH /projects/:id write:projects

Changes a project's name, color or keyword.

DELETE /projects/:id write:projects

Deletes a project. Its tasks stay, without a project.

Pages

GET /pages/daily read:notes

Today's daily note, made from the Daily note template when there is none yet. Answers 201 with created true when it was just made.

POST /pages/:id/archive write:notes

Moves a page to the trash. Answers 409 when the page is published unless confirm_takedown is true.

POST /pages/:id/restore write:notes

Brings a page back from the trash.

POST /pages/:id/favorite write:notes

Toggles a page's favourite flag.

POST /pages/:id/duplicate write:notes

Copies a page with its template placeholders filled in.

GET /pages read:notes

Active pages without bodies, newest first, with every folder and section. q searches titles and bodies; archived=1 includes archived pages.

POST /pages write:notes

Creates a page from title, body, icon, parent_id, position, template, favorite and tag_names. kind (page, folder or section) is fixed at creation.

GET /pages/:id read:notes

A page with its body.

PUT, PATCH /pages/:id write:notes

Changes a page. Takes the same fields as create, except kind.

Publishing

GET /pages/:page_id/publication/check read:notes

The pre-publish check: links, embeds and properties that will not work or will show for visitors.

GET /pages/:page_id/publication read:notes

The page's publication, including taken-down ones. 404 when the page was never published.

PATCH /pages/:page_id/publication publish:notes

Changes display, password, expires_at, slug or domain_id without taking a new snapshot. The old address redirects to the new one. 422 when the slug is taken.

PUT /pages/:page_id/publication publish:notes

Publishes or republishes the page as it is now. Takes display toggles, password, expires_at, slug and domain_id. Answers 409 with the check's items when links would go grey, unless confirm is true.

DELETE /pages/:page_id/publication publish:notes

Unpublishes the page. Answers 409 when other published pages link to it, unless confirm_takedown is true.

Custom domains

POST /publishing_domains/:id/check publish:notes

Looks the TXT record up now and answers with the domain's status.

GET /publishing_domains read:notes

The workspace's custom domains, with their status and the DNS records to add.

POST /publishing_domains publish:notes

Adds host. It is checked straight away, every 10 minutes for a day, then hourly for a week.

GET /publishing_domains/:id read:notes

One custom domain.

DELETE /publishing_domains/:id publish:notes

Removes the domain. Its pages move to the main address under the same slug, or a new one if it is taken there. Answers 409 while any of them is live, unless confirm is true.

Capture

POST /captures write:tasks

Turns text into tasks the way quick capture does, reading durations, priority, tags, projects and dates from the text.

Parse

POST /parse read:tasks

Parses one line of text like capture does, without creating anything.

Routines

POST /routines/:id/archive write:routines

Archives a routine.

POST /routines/:id/restore write:routines

Restores an archived routine.

GET /routines read:routines

Active routines with their steps. archived=1 includes archived ones.

POST /routines write:routines

Creates a routine from name, icon, color, finish_by, halfway_cue, sound, days and steps_attributes.

GET /routines/:id read:routines

A routine with its steps and the id of its running run, if any.

PUT, PATCH /routines/:id write:routines

Changes a routine. In steps_attributes a step with an id is updated, _destroy removes it, and one without an id is added.

DELETE /routines/:id write:routines

Deletes a routine and its runs.

Routine runs

POST /routines/:routine_id/runs/:id/advance write:routines

Completes the current step with outcome (default done) and moves to the next.

POST /routines/:routine_id/runs/:id/defer write:routines

Moves the current step to the end of the run.

POST /routines/:routine_id/runs/:id/abandon write:routines

Stops a running run.

POST /routines/:routine_id/runs write:routines

Starts a run, or answers the one already running. Takes an optional finish_by time.

GET /routines/:routine_id/runs/:id read:routines

A run with its step runs.

Today

GET /today read:workspace read:tasks read:calendar read:routines

The day's scheduled, due and overdue tasks, events, routines, inbox count and next task. date defaults to today.

Calendar

GET /calendar read:tasks read:calendar

Events and tasks per day from from to to (YYYY-MM-DD, at most 31 days; default the current week), with the work hours.

Calendar events

GET /calendar_events read:calendar

Events carrying tag, newest first, up to 200. tag is required.

POST /calendar_events write:calendar

Creates an event from title, starts_at, ends_at, all_day, location, meeting_url and tags.

PUT, PATCH /calendar_events/:id write:calendar

Changes an event. Takes the same fields as create.

DELETE /calendar_events/:id write:calendar

Deletes an event.

Focus sessions

POST /focus_sessions/:id/finish write:focus

Ends a session with outcome. then set to done completes its task, pause pauses it.

GET /focus_sessions read:focus

The last 50 focus sessions, newest first.

POST /focus_sessions write:focus

Starts a focus session, optionally for task_id and with planned_minutes. Answers the running one if a session is already going. A client that ran it offline sends started_at.

GET /focus_sessions/:id read:focus

A focus session.

PUT, PATCH /focus_sessions/:id write:focus

Extends or shortens the session by delta minutes.

Notifications

POST /notifications/read_all write:notifications

Marks every notification read and answers the list.

GET /notifications read:notifications

The 50 latest notifications, unread first.

PUT, PATCH /notifications/:id write:notifications

Marks a notification read, or unread with read false.

GET /search Any read: scope

Searches pages, tasks, projects and routines for q. A family the token cannot read comes back empty.

Week report

GET /report read:workspace read:tasks read:focus read:routines

The week's figures for the week holding date (default today): focus minutes, tasks done, routine runs, by day and by project.

Ask

POST /ask read:notes

Answers question from the workspace's pages with the sources used. Needs Ask set up in settings.

Extensions

GET /extensions read:settings

The extensions registered in the workspace, with the collections and tools each declares.

PUT /extensions/:extension_id write:settings

Registers an extension from name, version, collections and tools, or replaces its registration. The app sends this when the extension is enabled.

DELETE /extensions/:extension_id write:settings

Removes an extension's registration. Its records are kept, so registering it again brings them back.

Extension records

GET /extensions/:extension_id/records/:collection read:extensions

Records in one of the extension's collections. Search with query, match fields with filter (a JSON object) and cap with limit. Needs read:extensions, unless the token is the extension's own.

POST /extensions/:extension_id/records/:collection write:extensions

Creates a record from the data object and answers 201. Needs write:extensions, unless the token is the extension's own.

GET /extensions/:extension_id/records/:collection/:id read:extensions

One record.

PATCH /extensions/:extension_id/records/:collection/:id write:extensions

Merges the fields in data into the record. A field sent as null is removed.

DELETE /extensions/:extension_id/records/:collection/:id write:extensions

Deletes a record.

Ai: Settings

GET /ai/settings Full token

The signed-in user's AI settings: whether AI is on, whether it is ready, their providers and their MCP servers.

PUT, PATCH /ai/settings Full token

Turns AI on or off with ai_enabled and answers the same shape as GET /ai/settings.

Ai: Providers

POST /ai/providers/:id/test Full token

Sends one short request with the saved settings. Answers ok and the reply, or 422 with the provider's error.

GET /ai/providers Full token

The signed-in user's AI providers. Keys are never returned, only whether one is set.

POST /ai/providers Full token

Adds a provider from name, kind, base_url, model and api_key. The first provider is selected. Answers 201.

PUT, PATCH /ai/providers/:id Full token

Changes a provider. A blank api_key keeps the saved key; clear_key removes it. selected makes it the one used.

DELETE /ai/providers/:id Full token

Removes a provider. When it was selected, the next one is selected.

Ai: Web searches

POST /ai/web_search/test Full token

PUT, PATCH /ai/web_search Full token

DELETE /ai/web_search Full token

Ai: Servers

POST /ai/servers/:id/refresh Full token

Fetches the server's tool list again. Answers 422 with the error when the server can't be reached.

POST /ai/servers/:id/sign_in Full token

The browser url that starts the server's OAuth sign-in. Answers 422 for a server that doesn't use OAuth.

GET /ai/servers Full token

The signed-in user's MCP servers. Secrets are never returned, only whether they are set.

POST /ai/servers Full token

Adds an MCP server from name, url, enabled, auth_kind, header or OAuth client settings and allowed_tools. Answers 201.

PUT, PATCH /ai/servers/:id Full token

Changes an MCP server. A blank secret keeps the saved one.

DELETE /ai/servers/:id Full token

Removes an MCP server.

Ai: Runs

GET /ai/runs/:id/conversation Full token

Every run of the conversation this run belongs to, oldest first.

GET /ai/runs Full token

The 50 latest AI conversations the signed-in user started in this workspace, each as its first run with runs, the number of runs in it.

POST /ai/runs Full token

Asks the AI prompt, with optional context (selection, page_id, screen, extension_id, task_ids, event_ids, page_ids). With conversation_id it replies in that conversation, keeping its context and answering any question; 409 while its last run is still running. Runs in the background and answers 202 with the run.

GET /ai/runs/:id Full token

One AI request, its proposed actions, and question when the model asked one.

DELETE /ai/runs/:id Full token

Deletes the conversation this run belongs to. Changes already applied stay.

Ai: Actions

POST /ai/actions/:id/apply Full token

Applies a proposed action. Answers 409 when it can no longer be applied.

POST /ai/actions/:id/reject Full token

Rejects a proposed action. Answers 409 when it was already applied or rejected.

POST /ai/actions/:id/undo Full token

Undoes an applied action. Answers 409 when the record has changed since.

Settings: Passkeys

POST /settings/passkeys/options write:settings

WebAuthn creation options plus a signed challenge_token, valid for five minutes.

GET /settings/passkeys read:settings

The signed-in identity's passkeys.

POST /settings/passkeys write:settings

Registers a passkey from the browser's credential and the challenge_token. Takes an optional name.

PUT, PATCH /settings/passkeys/:id write:settings

Renames a passkey.

DELETE /settings/passkeys/:id write:settings

Removes a passkey.

Settings: Devices

GET /settings/devices read:settings

Every session of the signed-in identity: devices, extensions and browsers.

POST /settings/devices write:settings

Makes a token from name, scopes, kind (device or extension) and extension_id. The token is in this response only. A scoped token cannot grant scopes it lacks.

DELETE /settings/devices/:id write:settings

Signs a session out. The session making the request uses DELETE /session instead.

Settings: Webhooks

GET /settings/webhooks read:settings

The workspace's webhooks.

POST /settings/webhooks write:settings

Creates a webhook from url, enabled and events. The signing secret is in this response only.

PUT, PATCH /settings/webhooks/:id write:settings

Changes a webhook's url, enabled or events.

DELETE /settings/webhooks/:id write:settings

Deletes a webhook.

Settings: Members

GET /settings/members read:settings

Members and pending invitations.

DELETE /settings/members/:id write:settings

Removes a member. Admins only; the owner cannot be removed.

Settings: Invitations

POST /settings/invitations write:settings

Invites email_address. Someone with a profile joins at once; anyone else gets an email. Admins only.

DELETE /settings/invitations/:id write:settings

Withdraws a pending invitation. Admins only.

Settings: Exports

GET /settings/export read:notes read:tasks read:projects read:routines read:tags read:calendar read:focus read:notifications read:workspace read:settings read:extensions

The whole workspace as JSON, or with format=zip the pages as a folder of Markdown files.

Settings: Imports

POST /settings/import write:notes

Imports Markdown, as a multipart vault zip or notes[] files.

Settings: Asks

GET /settings/ask read:settings

Whether Ask is on, the model and the last four characters of the API key.

PUT, PATCH /settings/ask write:settings

Changes ai_enabled, ai_model or ai_api_key. A blank key keeps the saved one; clear_key removes it.

Settings: Tags

GET /settings/tags read:tags

Every tag with how many records use it.

PUT, PATCH /settings/tags/:id write:tags

Renames or recolours a tag.

DELETE /settings/tags/:id write:tags

Deletes a tag.

Settings: Connections

POST /settings/connections/:provider/sync write:settings

Queues a sync of the connection. Answers 202.

GET /settings/connections read:settings

Each integration provider, whether the server has it set up, and the workspace's connection and settings.

POST /settings/connections write:settings

Answers the url to open in a browser to connect provider with OAuth.

PUT, PATCH /settings/connections/:provider write:settings

Changes a connection's settings.

DELETE /settings/connections/:provider write:settings

Disconnects a provider and revokes its token.