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.
| Scope | Covers |
|---|---|
read:notes | Read pages, their bodies, tags and links |
write:notes | Change pages, their bodies, tags and links |
read:tasks | Read tasks, subtasks, captures and the parser |
write:tasks | Change tasks, subtasks, captures and the parser |
read:projects | Read projects |
write:projects | Change projects |
read:routines | Read routines, their steps and runs |
write:routines | Change routines, their steps and runs |
read:tags | Read the workspace's tags |
write:tags | Change the workspace's tags |
read:calendar | Read calendar events |
write:calendar | Change calendar events |
read:focus | Read focus sessions |
write:focus | Change focus sessions |
read:notifications | Read notifications |
write:notifications | Change notifications |
read:workspace | Read the workspace's name, members' names, preferences, today, the week report and the change feed |
write:workspace | Change the workspace's name, members' names, preferences, today, the week report and the change feed |
read:settings | Read preferences, devices, passkeys, webhooks, members, invitations and connections |
write:settings | Change preferences, devices, passkeys, webhooks, members, invitations and connections |
read:extensions | Read the records every extension stores, such as recipes and people |
write:extensions | Change the records every extension stores, such as recipes and people |
publish:notes | Publish 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:
401 | No token, or one that has been signed out. |
403 | The token lacks a scope (the body lists them under scopes), the profile is not set up, or the workspace is not the caller's. |
404 | No such record in this workspace. |
409 | The 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. |
422 | The record did not validate. error holds the messages. |
429 | Too 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.
Search
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.