API

REST API reference

Endpoint contracts for authentication, tasks, Documents, Tables, roles, imports, webhooks, and automation graphs.

v0.3.0 referenceReviewed

On this page

Start with the API quick start for authentication and your first request. This reference describes the pinned v0.3.0 contract; your deployed instance serves its own /api/v1/openapi.json.

Documentation: reference table
ConventionValue
REST base path/api/v1 on your Hopya instance
Workspace prefix/workspaces/:wid; abbreviated paths below use the stated prefix
SuccessDirect JSON object or array
Error{ "error": "message" }
Resource IDsUUIDs; status IDs are bounded safe strings
Dates / timestampsYYYY-MM-DD / UTC ISO-8601
MutationsUnknown fields are rejected

See architecture for the full endpoint inventory and integrations for attachments, OIDC, AI, and MCP.

Authentication

Use Authorization: Bearer $HOPYA_API_TOKEN for programmatic requests. Create a token in Account settings after signing in.

Authentication: reference table
CredentialContract
Personal tokenRevealed once; hashed at rest; expires after 90 days; immediately revocable
Token accessInherits the account’s current workspace permissions; no independent per-token scopes
Browser sessionHttpOnly cookie; seven days; at most 20 sessions per account
Token limitAt most 50 active tokens per account
Password12–256 characters when set

Keep secrets private. Use a restricted automation account. Never expose tokens in URLs, client code, shell history, or logs. This example assumes privately supplied environment variables.

curl --fail-with-body "$HOPYA_URL/api/v1/workspaces" \
  -H "Authorization: Bearer $HOPYA_API_TOKEN"
Authentication: reference table
RequestMutation boundary
Unsafe browser request, including login/setupOrigin must exactly match APP_URL
Origin-less mutationValid bearer token and no cookies
Explicit foreign OriginRejected, even with a valid bearer
CORSNo permissive CORS in normal operation

Account endpoints

Paths below are relative to /api/v1.

Account endpoints: reference table
MethodPathInput / result
GET/configPublic feature booleans and setupRequired; no secrets
POST/auth/setup{name,email,password,setupToken}; first operator-authorized admin only
POST/auth/register{name,email,password}; only when enabled after setup
POST/auth/login{email,password}; session cookie plus public user
GET/auth/me{id,name,email,isAdmin}
POST/auth/logoutRevoke current browser session
POST/auth/forgot-password{email}; generic accepted response; delivery when configured
POST/auth/reset-password{token,password}; consume one-use link and revoke all credentials
PATCH/auth/profile{name,email?,password?,currentPassword?}; current password required for email/password changes; prior credentials revoked
GET/auth/tokensToken metadata only
POST/auth/tokens{name}; raw token once
DELETE/auth/tokens/:idRevoke caller-owned token
PUT/auth/profile/photo{contentType,data}; canonical base64 PNG/JPEG/WebP ≤512 KiB; self-only replacement; {photoUrl}
DELETE/auth/profile/photoRemove own photo; {photoUrl:null}
GET/users/:id/photo?v=:revisionCurrent private raster for self, current shared-workspace members, or site admins; not a capability URL
Authentication limits and recovery
Account endpoints: reference table
RuleLimit / behavior
Login10 requests per normalized account/IP and 100 per verified IP per 15 minutes, including invalid attempts
Reset requestsLimited by verified IP and normalized email; same response for existing and missing accounts
Reset tokenHashed; single-use; 30-minute lifetime
Limiter windowsLogin address windows: 1,000; other authentication categories: 10,000
Throttling429 includes Retry-After; see limiter boundaries
Credential changesRevalidate the current credential after hashing; expiry/revocation before commit returns 401
Dev-only CORS escape hatchALLOW_ANY_ORIGIN=true echoes any Origin with credentials, answers preflights, and logs a warning; never enable on a reachable instance

MCP transport

MCP paths below use /api/v1; payloads follow MCP, not the REST envelope. The pinned transport implementation confirms that prefix.

MCP transport: reference table
MethodPathContract
GET/mcp/sseAdministrator-enabled SSE stream; 404 while disabled
POST/mcp/messages?sessionId=...Client messages for the authenticated session
  • Both require a personal bearer token; cookie-only authentication is rejected.
  • Sessions are user-bound, expire after 30 minutes, and are capped at four per user / 32 per instance.

Workspaces And Hierarchy

Prefix: /api/v1. Every resource stays workspace-scoped.

Workspaces And Hierarchy: reference table
MethodPathInput / result
GET/workspacesMembership-scoped workspace array
POST/workspaces{name}; creates workspace and Owner, Member, Viewer roles
GET/workspaces/:widPermission-filtered bootstrap; response keys below
PATCH/workspaces/:wid{name}; workspace:manage
DELETE/workspaces/:widProtected Owner only; permanent deletion of workspace and relational contents

Hierarchy paths below use /api/v1/workspaces/:wid.

Workspaces And Hierarchy: reference table
MethodPathInput / result
GET/nodesHierarchy node array
POST/nodes{name,kind,parentId?,description?,icon?,color?}; project, folder, or list
PATCH/nodes/:id{name?,description?,parentId?,expectedParentId?,icon?,color?}; rename, style, or move
DELETE/nodes/:idEmpty nodes only; 409 if child nodes, documents, or tasks remain
GET/exportVersion 8 JSON: hierarchy, attributed Documents/pages, local Tables, safe live-source descriptors, tasks, comments, reactions, fields, and attachment metadata
Workspaces And Hierarchy: reference table
Structure ruleContract
RootsProjects and standalone lists may be workspace roots
ParentsFolders require a project/folder; nested lists may use either
ListsContain tasks, not hierarchy nodes
DepthAt most 32 path entries, root counted as level 1; includes Document/Table leaves
NamesAt most 120 characters
Mutation accessstructure:write

Export is not a backup or import format. It excludes credentials, audit history, and file bytes. Workspace deletion also removes memberships, roles, fields, and automation records; export needed data first.

Move rules and status inheritance
  • Preserved: IDs, child relationships, tasks, attachments, and task statuses. Move/rename and audit commit together.
  • Rejected: moving a project under another node; self/descendant/list parents; foreign workspaces; subtree depth overflow.
  • Stale placement: send the original parentId as expectedParentId with a parentId mutation; mismatch returns 409. Rename-only PATCH preserves the current parent. Empty/unknown-field mutations are rejected.
  • Across projects: inherited descendant lists must be valid under destination statuses. Overrides retain their own statuses and do not block a move based on project defaults.
  • List to root: materializes its inherited statuses. Standalone list into project: keeps its statuses as an override. No move remaps task statuses.
Descriptions, icons, and colors
Workspaces And Hierarchy: reference table
FieldContract
descriptionReturned on nodes, default ""; only project POST/PATCH accepts it; ≤50,000 characters; "" clears it
iconNullable allowlisted name; null restores default
colorNullable six-digit #RRGGBB; responses normalize to lowercase; null restores default

Allowed icons:

diamond, briefcase, target, home, star, heart, globe, clock,
mapPin, settings, lock, users, user, folder, archive, bookmark,
list, checklist, calendar, flag, package, shoppingBag, fileText,
inbox, trash, pencil, eye, eyeOff, sparkles, code, link, comment, save

Legacy color names remain accepted:

Workspaces And Hierarchy: reference table
NameNormalized color
slate#64748b
orange#c45d0a
amber#9a7411
green#4d7a47
teal#17776f
blue#2563a6
violet#7652a8
rose#a5415b

Folder/list descriptions remain empty.

Workspace bootstrap shape and visibility
{
  "workspace": {}, "role": {}, "permissions": [],
  "members": [], "roles": [], "nodes": [], "documents": [],
  "tables": [], "documentPages": [], "fields": [],
  "projectFields": [], "listStatusConfigs": [], "listTagColorConfigs": []
}

The example shows response keys, not a complete instance response.

Workspaces And Hierarchy: reference table
DataRequired access
Workspace name, own role/permissionsAny active membership
Hierarchy metadataTask, Document, Table, or structure access
Document page linksBoth task and Document read access
Table metadata, without columns/recordsTable read, write, delete, or structure management
Field definitionsTask read or structure write
Member directoryTask read or member management
Full role catalogTask read, member management, or role management

Other arrays are empty; the role list retains the caller’s own role. Bootstrap metadata never grants task-body, Document-body, Table-record, or export access. Management-only roles can use Settings without reading tasks.

Documents

Prefix: /api/v1/workspaces/:wid. Documents sit at root or under a project/folder.

Documents: reference table
MethodPathInput / result
GET/documentsDocument metadata
POST/documents{title,body?,parentId?,icon?,color?}
GET/documents/:idOne Document
PATCH/documents/:idAt least one mutable field plus exact expectedUpdatedAt; documents:read + documents:write
DELETE/documents/:iddocuments:delete; removes nested document-page subtree and page links, preserves linked tasks/subtasks
Documents: reference table
FieldContract
title1–300 characters
bodyMarkdown, ≤50,000 characters
icon, colorNullable; same hierarchy allowlist/normalization; may change without editing body

Task-backed pages

Task-backed pages: reference table
MethodPathInput / result
GET/documents/:id/pages{items,total,truncated}; Document + task read; at most 500 rendered tasks, complete totals
POST/documents/:id/pages{itemId,position?}; link an active top-level task
DELETE/documents/:id/pages/:itemIdUnlink, not delete; Document write + task read
  • One task may belong to at most one Document; its subtasks appear as nested pages.
  • A linked root task cannot become a subtask until unlinked.

Document-backed pages

Document-backed pages: reference table
MethodPathInput / result
GET/documents/:id/subpages{rootId,documents,total,truncated}; ≤500 complete summaries including pagePlacement; valid from root or page
POST/documents/:id/subpages{title,placement?}; uses Document permissions and normal editor
Document-backed pages: reference table
PlacementMeaning
pageRoot Document only; top-level Pages-rail entry
subpage (default)Child of addressed Document/page; at most 31 levels

Tables

Prefix: /api/v1/workspaces/:wid. Tables are separate from task Lists: root/project/folder leaves, not containers for tasks.

Local Table Resources

Local Table Resources: reference table
MethodPathInput / result
GET/tablesTable metadata
POST/tables{name,parentId?,icon?,color?}
GET/tables/:idOne Table
PATCH/tables/:idRename/move/style; exact expectedUpdatedAt; tables:read + tables:write
DELETE/tables/:idtables:delete; permanently removes local columns/records
GET/tables/:id/columnsOrdered columns
POST/tables/:id/columns{name,type,options?}
PATCH/tables/:id/columns/:columnIdRename with exact expectedUpdatedAt; tables:read + tables:write
DELETE/tables/:id/columns/:columnIdtables:write + tables:delete; {success,touchedRecords}
GET/tables/:id/records{records,nextCursor}; limit 1–500 and opaque workspace/Table cursor; tables:read
POST/tables/:id/records{values?}; tables:write
GET/tables/:id/records/:recordIdOne record; tables:read
PATCH/tables/:id/records/:recordIdReplace complete values map with expectedUpdatedAt; tables:read + tables:write
DELETE/tables/:id/records/:recordIdtables:delete
Local Table Resources: reference table
DataValidation / limits
Table name / depth1–120 characters / at most 32 path entries; a containing project/folder is not empty
icon, colorNullable hierarchy allowlist/normalization; included in metadata and exports
ColumnsAt most 100; text, number, date, datetime, checkbox, select
Select options1–100 unique strings, ≤120 characters each; other column types reject options
Record valuesKeyed by column UUID; nullable and type-checked; text ≤10,000 characters
Complete values mapAt most 256 KiB / 100 keys
Record page2 MiB encoded budget; follow nextCursor until null; pages are not one frozen snapshot
Column deletionRemoves its key from every record transactionally and advances touched record revisions

Query and calculations

Query and calculations: reference table
MethodPathInput / result
POST/tables/:id/queryRead-only; tables:read; {records,nextCursor,total,summary?}
GET/tables/:id/summarytables:read; {recordCount,columns:{[columnId]:{count,empty,...}}} across all records

Query input:

{
  "filters": [{"columnId": "<column UUID>", "operator": "not_empty"}],
  "sort": null,
  "limit": 100,
  "summary": false
}
Query and calculations: reference table
ParameterContract
filters?At most 20 {columnId,operator,value?} conditions combined with AND; columns must belong to this Table
sort?{columnId,direction:"asc"|"desc"} or null
limit?1–500; default 100; 2 MiB record budget
cursor?At most 8,192 characters; binds workspace/Table, column schema, filters, and sort; not access credentials
summary?Default false; true calculates across every matching row
Query and calculations: reference table
Column typeOperators / comparison
Allis, is_not, empty, not_empty; empty operators need no value
Text / selectAlso contains, not_contains; case-insensitive
Number / date / datetimeAlso gt, gte, lt, lte; numeric values / precise date instants
CheckboxJSON booleans; false sorts before true

Values must match column types: JSON number/boolean scalars, date strings, or offset date-time strings. Missing/null/empty text match only empty operators and sort last in both directions. Ties/no sort preserve local creation/ID order or live primary-key order.

Query and calculations: reference table
CalculationResult
All columnscount, empty; missing/null/empty string are empty, zero/false are present
Numbersum, average, min, max; empty: sum 0, other values null; overflow: null sum/average
Date / datetimeearliest, latest; precise instants including source fractional precision; empty bounds null
Checkboxchecked, unchecked

Full-dataset results, not loaded-grid totals. Queries scan a consistent local/live snapshot; total and requested calculations cover all matches. Exports remain independent of grid filters/order.

Query cursors, deadlines, and failure handling
  • Queries and calculations share four global / two per-account slots and a 30-second checkpoint deadline.
  • Processing uses bounded chunks; summaries use constant-sized accumulators. Authentication/permissions are rechecked between chunks and before returning.
  • A changed/missing/nonmatching query anchor returns 409: refresh. Pages are not a cross-request snapshot.
  • Incompatible compared source values return 422, not misleading ordering. Failed, expired, or cancelled work returns no partial totals/results; connections and slots are released.
  • HEAD on summary validates access without calculating.

Files And Live Connections

Files And Live Connections: reference table
MethodPathInput / result
POST/tables/importCreate a local Table; {format:"csv"|"json",data,name?,parentId?,columns?}; tables:read + tables:write
POST/tables/:id/importAppend to a local Table with the same body/access
GET/tables/:id/export?format=csv|jsonAll records, including unloaded/live rows; tables:read
GET/tables/:id/sourceLocal: null; live: {dialect,connectionName,schema,table}
GET/table-connectionsSafe metadata; credentials:manage
POST/table-connectionsTest/save encrypted connection; credentials:manage; fields below
GET/table-connections/:id/catalog{tables:[{schema,name}],more}; ≤500 names; credentials:manage
DELETE/table-connections/:idcredentials:manage; 409 if links still use it
POST/tables/connect{connectionId,schema,table,name,parentId?}; credentials:manage + tables:read + tables:write
Files And Live Connections: reference table
Transfer ruleContract
Import sizeAt most 500 records / 1,000,000 UTF-8 bytes; failure rolls back all rows, columns, and new Table
Import columnsExact destination names; empty Table creates definitions; optional {key,name,type,options?}
CSVNew columns default to text; typed destinations parse numbers and true/false/1/0; export needs unique headings and flattens null/empty strings
JSONBare arrays infer number/checkbox types; typed format below preserves values and remaps column keys on import
Export snapshotSeparate consistent snapshot; ≤30 seconds; eight global / two per-account slots; 64 KiB chunks
Export accessRechecked before chunks; interruption aborts; HEAD validates without a snapshot
Typed Table JSON example
{
  "format": "hopya-table",
  "version": 1,
  "name": "Inventory",
  "columns": [
    {"key":"qty","name":"Quantity","type":"number","options":[]},
    {"key":"active","name":"Active","type":"checkbox","options":[]}
  ],
  "records": [
    {"qty":12.5,"active":false},
    {"qty":null,"active":null}
  ]
}

Live writes change the source database. Reload before retrying an uncertain commit. Deleting a Hopya live Table removes its link, never source data; workspace export excludes remote rows.

Connection configuration and live write-back

Connection POST accepts {name,dialect,host?,port?,database?,username?,password?,tls?,filename?}.

Files And Live Connections: reference table
SettingContract
dialectpg, mysql, or sqlite
Network sourceHost, database, username required; verified TLS by default; operator SQL_ALLOWED_HOSTS
SQLite sourceFilename required; operator SQL_SQLITE_ROOT
Returned metadata{id,workspaceId,name,dialect,createdAt} only; encrypted configuration never returned
Linked sourceText/numeric primary key required; composite keys supported; explicit schema/table linking is not limited by catalog truncation
Files And Live Connections: reference table
Live record behaviorContract
Shared routesColumn/record GET and record PATCH
Column metadataAdds sourceType, readOnly, primaryKey
IDs / cursorsOpaque primary-key tokens ≤4,096 characters
RevisionupdatedAt is a SHA-256 value revision, not a timestamp; createdAt is empty
PATCHSend exact revision; changed fields apply under a source row lock; omitted fields survive
ConflictsStale row/changed schema returns 409; schema changes require reconnecting
Read-only fieldsPrimary keys, generated fields, unsupported types
Source operationsInsert/delete rows and change columns through database tools, not Hopya
AuditDurable intent before source transaction, applied audit after commit; no distributed transaction

An uncertain source commit can leave an unresolved intent. Workspace export v8 includes local Table data, appearance, committed-image metadata, and safe tableSources descriptors—not connection secrets, file bytes, or remote rows. Use per-Table export for remote data.

Tasks

Prefix: /api/v1/workspaces/:wid.

Tasks: reference table
MethodPathInput / result
GET/items/pagePreferred paged read; {items,nextCursor}; query parameters below
GET/itemsComplete JSON-array snapshot stream; same node/search/status/archive filters
POST/itemstitle and list nodeId required; example below
GET/items/:idOne workspace-scoped task
PATCH/items/:idPreserve omitted fields; items:read + items:write; use expectedUpdatedAt
DELETE/items/:iditems:delete
POST/items/bulkAtomic archive/delete of 1–100 tasks with exact revisions

Pagination and bulk reads

Pagination and bulk reads: reference table
ParameterContract
nodeId?Hierarchy scope
search?Literal title/description substring
status?Exact configured status ID
archived?exclude (default), include, or only
limit?Integer 1–500; default 200; may change between cursor requests
cursor?Pass nextCursor unchanged with the same workspace/filters; restart if filters change
curl --fail-with-body --get \
  "$HOPYA_URL/api/v1/workspaces/$WORKSPACE_ID/items/page" \
  -H "Authorization: Bearer $HOPYA_API_TOKEN" \
  --data-urlencode "limit=200"

For subsequent pages, add --data-urlencode "cursor=$NEXT_CURSOR" using the previous non-null value.

Finish only when nextCursor is null. The 2 MiB encoded budget can return a short page. Cursors are workspace/filter-bound positions, not credentials; every request checks current permissions. Multiple pages are not one frozen snapshot.

Pagination and bulk reads: reference table
Bulk read ruleContract
/items / /exportConsistent SQLite/PostgreSQL snapshot; no silent row cap; workspace export always includes archives
CapacityEight global / two per-account reads; excess: 429, Retry-After: 1
Lifetime30 seconds; released on cancellation, finish, or error
AccessRechecked before every chunk; HEAD checks access without a stream slot
Before headersSanitized JSON error; bulk deadline: 504
After headersRevocation, timeout, or corruption aborts the connection; delivered bytes cannot be recalled

An interrupted export is a failed download, never a complete/importable partial file. Settings downloads stream through the browser without JavaScript buffering/reserialization.

Task page sizing and seek behavior
  • A schema-valid item may exceed the soft 2 MiB budget so it remains whole; an encoded stored record above 8 MiB fails rather than truncates.
  • The (createdAt,id) seek avoids offset drift when an earlier item is deleted. Use a bulk stream for one consistent dataset snapshot.

Create and update fields

Create and update fields: reference table
FieldContract
titleRequired on create; 1–300 characters
nodeIdRequired list ID on create
descriptionAt most 50,000 characters; "" clears
statusEffective destination-list ID on create/update/move; omitted create defaults to first status
prioritynone, low, medium, high, urgent
startDate, dueDateNullable calendar dates; start cannot follow due
tagsAt most 30 distinct strings, 1–60 characters each
assigneeIdActive workspace member; null clears
checklistAt most 100 entries; server-normalized IDs; text 1–200 characters
parentIdSame-workspace task; no cycles/subtask-depth overflow; null clears
customFieldsTyped values keyed by field UUID; PATCH replaces the complete map
Server fieldsResponses add id, workspaceId, archivedAt, createdAt, updatedAt; ordinary writes cannot overwrite identity/timestamps/archive state

List overrides govern status validation; otherwise the root project’s configuration applies. Standalone lists always have explicit statuses. All updates preserve omitted fields.

Create-task example
{
  "nodeId": "11111111-1111-4111-8111-111111111111",
  "title": "Rehearse backup recovery",
  "description": "Verify restored attachments and membership.",
  "status": "todo",
  "priority": "high",
  "startDate": "2026-09-07",
  "dueDate": "2026-09-10",
  "tags": ["operations"],
  "assigneeId": null,
  "checklist": [{"text":"Verify task data","done":false}],
  "parentId": null,
  "customFields": {}
}

Replace illustrative IDs with real resources from your instance.

Concurrent Editing

Send the exact last-read updatedAt as expectedUpdatedAt on PATCH. Preserve unrelated entries when replacing customFields.

{
  "status": "done",
  "expectedUpdatedAt": "2026-09-06T12:00:00.000Z"
}
Concurrent Editing: reference table
SituationResult
Task changed409; no modification or mutation audit; refresh and reconcile the human’s intent before retry
Browser editSends the condition and only changed fields
REST/MCP omits conditionExplicitly accepts last-write-wins
CreateexpectedUpdatedAt is not valid

Archive and delete in bulk

{
  "action": "archive",
  "items": [{
    "id": "11111111-1111-4111-8111-111111111111",
    "expectedUpdatedAt": "2026-09-08T10:00:00.000Z"
  }]
}
  • Archive requires items:read + items:write; permanent delete requires items:delete.
  • Select every descendant before archiving/deleting a parent.
  • Missing, foreign, stale, duplicate, or incomplete selections reject the entire mutation.

Discussion And Notifications

Task-comment paths below use /api/v1/workspaces/:wid/items/:id. Document discussion uses the equivalent /documents/:id/comments routes with documents:read instead of items:read.

Discussion And Notifications: reference table
MethodPathInput / result
GET/commentsComplete creation-ordered discussion; parentId, optional anchor, grouped {emoji,count,reactedByMe}; resource read access
POST/comments{body,parentId?,anchor?}; resource read + comments:create
DELETE/comments/:commentIdFirst tombstones body; author or comments:manage; repeat DELETE by moderator permanently removes tombstone
PATCH/comments/:commentId/reaction{emoji,active}; resource read + comments:create; one of each reaction per member
Discussion And Notifications: reference table
Discussion fieldContract
bodyMarkdown, 1–10,000 characters
RepliesSame resource; depth ≤32; inherit root range and cannot supply a separate anchor
Root anchor{revision,start,end,exact,prefix,suffix}; UTF-16 saved-body/description offsets; stale/nonmatching selections rejected
Edited quoteUnambiguous range relocates; deleted/ambiguous text retains quote with orphaned anchor
Tombstone removalDirect replies survive, reattached to removed entry’s parent
Reactions👍, ❤️, 😂, 🎉, 😕, 👀

Notification prefix: /api/v1/workspaces/:wid.

Discussion And Notifications: reference table
MethodPathInput / result
GET/notificationsUp to 200 newest assignment/user-mention notifications owned by current member
GET/notifications/unread-count{unread}
PATCH/notifications/:id{read:boolean}
DELETE/notifications/:idRemove caller-owned row

User mentions are editor-generated same-origin Markdown links to active same-workspace members with items:read; at most 20 per body/comment. Unchanged task mentions do not notify twice; self-notifications are skipped. @@ task links and @@@ structure links navigate without notifying.

Fields And Access

Prefix: /api/v1/workspaces/:wid.

Custom field definitions

Custom field definitions: reference table
MethodPathInput / result
GET/fieldsWorkspace field catalog
POST/fields{name,type,options?,settings?,projectId?}; optional project assignment is atomic
PATCH/fields/:id{name?,options?,settings?}; at least one property; structure:write; last-write-wins
DELETE/fields/:idDestructive deletion of definition, assignments, and saved task values

Definitions return {id,workspaceId,name,type,options,settings?}. Omitted projectId creates a reusable catalog entry; type/projectId cannot be patched.

Custom field definitions: reference table
TypeValue / configuration
text, number, checkboxTyped value; options must be empty
dateCalendar-valid YYYY-MM-DD; optional dateFormat
datetimeValid ISO timestamp ≤64 characters with Z/explicit offset; preserved as supplied; optional dateFormat
selectDropdown; 1–100 unique trimmed nonempty options, ≤120 characters each
checklistSame option constraints; value is unique selected-option array, including []
ratingInteger 1–maxRating; default 5, configured maximum 1–10
formulaNew shared formula defined at creation; calculated task values read-only; legacy behavior below

All types permit null. Task customFields replaces the complete map on PATCH; preserve unrelated values and send expectedUpdatedAt. Unknown fields/wrong types are rejected.

Field settings, formulas, and mutation integrity
Custom field definitions: reference table
SettingContract
dateFormatDate/datetime only: yyyy-MM-dd, MMM d, yyyy, MMMM d, yyyy, dd/MM/yyyy; presentation only
maxRatingRating only; integer 1–10
formulaFormula only; nonempty expression ≤200 characters; trimmed {{Field name}} references must resolve in workspace
ReplacementSupplied options/settings replace that property; omitted ones survive; {settings:{}} restores defaults
  • Field PATCH returns 409 if it would invalidate any saved value, including unassigned values. Validation scans one task map at a time. Safe audit and field update commit atomically; task values/versions and assignment revisions stay unchanged.
  • Field deletion uses indexed single-row value/version reads, removes assignments, and advances affected task/configuration versions. field.delete records exact touched-task count; audit failure rolls everything back.
  • New formula fields require a shared expression (=0 is the browser starter). Referenced fields cannot be renamed/deleted until shared formulas are updated. Legacy definitions without settings.formula keep per-task expressions until deliberately configured.
  • Raw formula strings allow ≤200 characters / 20 references. New/changed references must name exact sibling fields and cannot self-reference; quoted reference-like text is literal. Removing an input does not block unrelated edits to an unchanged formula.
  • Display evaluation supports arithmetic, comparisons, quoted strings, and SUM, AVERAGE, MIN, MAX, ROUND, ABS, IF, CONCAT, with bounded nesting and no JavaScript execution. Referenced formula strings are not recursively evaluated. Raw strings survive REST/export; missing references, malformed expressions, and non-finite results display the raw expression.

Field ownership and statuses

Field ownership and statuses: reference table
MethodPathInput / result
GET/projects/:projectId/fieldsRoot-project or standalone-root-list configuration; membership + items:read or structure:write
PATCH/projects/:projectId/fields{fieldIds?,builtInFields?,statuses?,dateFormat?,expectedUpdatedAt?}; at least one config property; structure:write
GET/lists/:listId/statusesStored {listId,statuses?,updatedAt,inheritedProjectUpdatedAt}; omitted statuses means inheritance
PATCH/lists/:listId/statuses{statuses,expectedUpdatedAt?,expectedProjectUpdatedAt?}; array overrides, null inherits; structure:write
GET/lists/:listId/tag-colors{listId,colors,updatedAt}; structure visibility
PATCH/lists/:listId/tag-colors{colors,expectedUpdatedAt}; complete replacement; structure:write
Field ownership and statuses: reference table
ConfigurationContract
Field-owner response{projectId,fieldIds,builtInFields,statuses,dateFormat?,updatedAt}; historical route/response key retained
Field IDsUnique workspace fields; at most 100
Optional built-inspriority, startDate, tags, description, nodeId, createdAt, updatedAt
Date presentationSame date-format enum; absence means ISO display; null restores default
Assignment removalNot field deletion; task values/versions survive; display assignment does not restrict REST/MCP permissions
Standalone statusesMutate through /lists/:listId/statuses, not the project-fields route
Tag colorsAt most 30 exact nonempty tag names ≤60 characters → six-digit hex; required revision; stale write 409; does not change task tags

Status schema:

Field ownership and statuses: reference table
FieldContract
ArrayOrdered, 1–50 strict objects
idUnique, 1–64 characters; ^[A-Za-z0-9][A-Za-z0-9_-]*$
nameTrimmed, 1–120 characters
colorSix-digit #RRGGBB
completedExplicit boolean, never inferred from ID

Status changes never rewrite tasks. Removing a status still in use returns 409; renaming an ID is removal plus addition. To enable an override on an inherited list, send both the list revision and its owning project’s inheritedProjectUpdatedAt as expectedProjectUpdatedAt.

Status concurrency, inheritance, and legacy defaults
  • Project PATCH replaces statuses, allowing add/reorder/rename/color/completion/removal. Its removal check considers only currently inheriting lists; overridden lists do not block it. Existing request/response/concurrency shape stays intact.
  • List GET returns stored configuration, not a materialized effective copy. inheritedProjectUpdatedAt is always present, even for an overridden list. List GET requires membership + items:read or structure:write.
  • Supplied expectedUpdatedAt must match the list revision or return 409. When enabling an override from inheritance, missing expectedProjectUpdatedAt returns 400; mismatch returns 409. The owner is resolved inside the mutation transaction.
  • Replacing an existing override/restoring inheritance needs only the list revision. A supplied project revision adds no conflict condition in those transitions. Every list has a stored row from creation/migration; first-write semantics match later writes.
  • Invalidating tasks in the list returns 409. Mutation plus safe list.statuses.update audit is atomic. Workspace detail and exports carry all field-owner configurations in projectFields; standalone statuses appear in GET/detail/export.
  • Legacy projects retain todo, backlog, in_progress, review, done. Migration 006/new projects use that order, with todo first and only done initially completed. Custom IDs work in page/bulk/MCP filters with unchanged cursor binding and authorization.

List-view settings

List-view settings: reference table
MethodPathInput / result
GET/views/list/settingsOptional UUID projectId; omit for all projects; caller’s current settings
PATCH/views/list/settings{projectId?:string|null,columnOrder,hiddenColumns,sort,expectedUpdatedAt}; strict full replacement
List-view settings: reference table
RuleContract
Response{view:"list",projectId:string|null,columnOrder:string[],hiddenColumns:string[],sort:{column,direction}|null,updatedAt:string|null}
AccessMembership + items:read; viewers manage only their own settings; write-only members/outsiders cannot read/write
First saveexpectedUpdatedAt:null; then exact timestamp; stale writes return 409
ColumnsUnique; always available: title, status, assigneeId, dueDate; optional built-ins and custom:<UUID>
ScopeWorkspace accepts any configured field; root-project/standalone-list scope only assigned custom fields; project ID must identify a root project
Visibility / orderTitle must stay ordered and visible; hidden columns must be ordered; sort names a visible ordered column, direction asc/desc
OwnershipAuthenticated account only; no client user ID; membership removal deletes settings
PersistenceMutation and count-only audit commit together; unsaved defaults have updatedAt:null

Roles and membership

Roles and membership: reference table
MethodPathInput / result
GET, POST/rolesList or create {name,permissions}
PATCH, DELETE/roles/:idUpdate or delete; assigned roles cannot be deleted
POST/members{email,roleId}; existing active account
PATCH/members/:userId{roleId}
DELETE/members/:userIdRemove member and clear task assignments
Roles and membership: reference table
Permission groupValues
Tasksitems:read, items:write, items:delete
Documentsdocuments:read, documents:write, documents:delete
Tablestables:read, tables:write, tables:delete
Discussioncomments:create, comments:manage
Workspacestructure:write, members:manage, roles:manage, workspace:manage
Integrationsautomations:manage, credentials:manage, agent:use
  • Owner is immutable and receives all permissions. Role managers cannot delegate/manage privileges above their own.
  • Normal membership changes cannot remove the last active Owner. Site-admin security suspension is exempt and retains ownership for recovery.
  • Migration 0007 derives existing roles’ Table permissions from task permissions; review delegation after upgrading.

Site Administration

Prefix: /api/v1. Every /admin/* endpoint requires isAdmin, not a workspace role. Site administration does not grant workspace-data access.

Site Administration: reference table
MethodPathInput / result
GET/admin/usersPublic user metadata, including disabled state
POST/admin/users{name,email,password,isAdmin?}
PATCH/admin/users/:id{isAdmin?,disabled?}; disabling revokes credentials and clears assignments, even for sole workspace owners; last active admin protected
GET/admin/auditNewest first; optional workspaceId, limit 1–200 (default 50), offset default 0; no secret hashes/keys, bodies, or full AI prompts
GET/admin/statusCounts, migrations, selected integration booleans, storage mode, pending cleanup; no provider credentials
GET/admin/oidc-identitiesOptional UUID userId; {id,userId,issuer,subject,createdAt}; unknown/malformed queries rejected
POST/admin/oidc-identitiesExplicit {userId,issuer,subject} link; see SSO
DELETE/admin/oidc-identities/:idUnlink and atomically revoke target sessions/tokens; safe audit counts; recovery rules below
OIDC unlink and recovery safeguards
  • Passwordless unlink returns 409 unless another identity remains for the exact configured issuer and SSO has a configured client. Inactive-issuer links are not recovery.
  • Configuration must exactly match discovery issuer, not a normalized equivalent URL.
  • Password-backed admins may unlink themselves, but are signed out. Matching in-flight callbacks cannot restore the removed identity/session.
  • Unlink is not a permanent IdP ban: restrict JIT and/or disable the upstream identity when quarantining access.

Integrations And Automations

Prefix: /api/v1/workspaces/:wid. Every ID remains workspace-scoped.

Integrations And Automations: reference table
OperationRequired permission
Webhook administrationworkspace:manage
All automation routes, including legacy CRUD and runsautomations:manage + items:read
Credential mutation / OAuthcredentials:manage
Credential metadataautomations:manage or credentials:manage; no extra items:read

Events: item.created, item.updated, item.deleted, node.created, node.updated, node.deleted, field.changed. Matching durable runs enqueue atomically with the event-producing mutation.

Workspace Webhooks

Workspace Webhooks: reference table
MethodPathInput / result
GET/webhooksSafe metadata
POST/webhooks{name,url,events,enabled?}; maximum 20; raw new secret once; signingVersion:2
PATCH, DELETE/webhooks/:idUpdate name/URL/events/enabled or delete
POST/webhooks/:id/rotateReplacement secret once; upgrades to version 2
Workspace Webhooks: reference table
Signing versionVerification of exact JSON body
2x-hopya-signature: sha256=<hex HMAC-SHA256(secret, body)>
1 (pre-migration)Historical sha256(secret + "." + body) until rotation; not HMAC

Configure the final destination directly. All 3xx responses are rejected for both signing versions and legacy/graph transports. Rotation is not needed for redirect rejection; redirect-dependent destinations are intentionally incompatible. Consumers must choose verification by signingVersion.

Versioned Automation Graphs

Versioned Automation Graphs: reference table
MethodPathInput / result
GET, POST/automationsLegacy list/create; {name,event,steps,enabled?} or singular action; maximum 50 automations / 1–20 steps
PATCH, DELETE/automations/:idCompatibility updates/delete; graph restrictions below
GET/automations/catalogTyped trigger outputs, node input/output/config manifests, events, limits
GET/automations/:id/draft{revision,graph,updatedAt}; revision 0 initialized from published version if unsaved
PUT/automations/:id/draft{expectedRevision,graph}; saves structurally invalid drafts with validation; stale 409
POST/automations/:id/validateOptional {graph}, otherwise saved draft; includes credential/destination checks
POST/automations/:id/publish{expectedRevision}; validate/publish immutable version, record publisher/credentials, update trigger, retain draft and increment revision
GET/automations/:id/versionsNewest-first {version,format,publisherId,publishedAt}
GET/automations/:id/versions/:version{version,format,graph,publisherId,publishedAt}
POST/automations/:id/preview{graph?,event?,mockOutputs?} → {valid,errors,path,uncertain}, effect:"none"; unresolved output branches marked uncertain
POST/automations/:id/testQueue real published-version run → {ok,event,runId,status:"pending"}
GET/automations/runsNewest durable runs; limit 1–100, optional automation UUID and pending/running/delivered/failed
GET/automations/runs/:runIdBounded sanitized steps/graph nodes; raw event/causation omitted

Preview has no effects; test does. Preview sends no HTTP/email/log/task changes. Test rejects any update_item node with 400 before enqueueing because its synthetic event has no triggering task.

Versioned Automation Graphs: reference table
Graph propertyContract
Shape{nodes,edges} with exactly one trigger
Actionshttp, webhook, email, log, update_item
Controlscondition, switch
Conditionequals, not_equals, exists, contains; true/false edges required
Switch1–20 scalar cases and default branch
TopologyAcyclic, fully reachable, exclusively branched; ordinary nodes ≤1 outgoing edge; one selected path, no parallel/join semantics
Limits50 nodes / 75 edges / 256 KiB graph / 32 KiB per node config / 50 visited execution nodes
TemplatesEvent: {{event.<path>}}; upstream: {{nodes.<nodeId>.output.<path>}}; controls: nodes.<nodeId>.output.<path>
PublicationReject unknown/self/downstream/branch-optional references not guaranteed upstream
TransportMethods, recipients, headers, bodies, outputs, logs, execution time, retention bounded; no automatic redirects
Version lifecycle, execution security, and legacy compatibility
  • Draft revisions rise across saves/publications, never reset on publication. Re-read the retained draft before the next save/publish. Stale requests return 409; queued/active runs keep their immutable version.
  • Before execution and each node—even read/send-only nodes—the publisher must still exist, belong to the workspace, and have items:read. Failure is closed.
  • update_item acts only on triggering itemId as the publisher, not event actor/test caller. It rechecks items:read + items:write immediately before mutation; ordinary task validation applies.
  • Nested causation is limited to depth five; an automation cannot re-enter its own causation chain.
  • Legacy create produces a versioned linear graph and preserves {{steps.N.output}}; existing linear versions keep ordered execution under no-redirect transport. Lists mark graph versions with graph:true.
  • Legacy action/steps PATCH cannot replace a graph version; graph trigger changes require draft publication. Name/enabled remain patchable; linear trigger PATCH creates a new immutable linear version.
  • Migration 0002_automation_linear_compatibility preserves linear representations larger than the graph-only 256 KiB limit and repairs HTTP-body {{event}} to {{nodes.<triggerId>.output.event}} in derived linear forms. Original steps, queued runs, published graphs, credential bindings, and edited drafts survive. Graph publication retains its own limits.

Automation Credentials

Secrets are write-only: encrypted by the API, never returned/exported/included in runs. Responses contain only {id,name,type,origin,pathPrefix,version,status,createdAt,updatedAt}.

Automation Credentials: reference table
MethodPathInput / result
GET/automations/credentialsSafe metadata; either management permission
GET/automations/credentials/:idOne safe metadata record
POST/automations/credentials{name,type,origin,pathPrefix?,secret}; version 1
PUT/automations/credentials/:id{expectedVersion,name?,secret}; complete secret replacement; stale version rejected
DELETE/automations/credentials/:idRevoke; published uses then fail
POST/automations/credentials/:id/oauth/start{authorizationUrl,expiresAt}; ten-minute authorization-code PKCE S256 flow
GET/automations/credentials/:id/oauth/callbackAuthenticated state/code exchange; encrypted tokens versioned; redirect to /integrations?oauth=connected on instance
Automation Credentials: reference table
Credential typeWrite-only secret shape
bearer{token}
api_key{name,value}; header only
basic{username,password}
custom_headers{headers}
oauth2{authorizationUrl,tokenUrl,clientId,clientSecret?,scopes[],accessToken?,refreshToken?,expiresAt?}

Destination binding always applies. Public credential destinations require HTTPS. A missing/invalid AUTOMATION_KEYRING returns explicit 503 for credential-dependent work; ordinary task APIs remain operational.

OAuth storage and network restrictions
  • OAuth state is hash-only; verifier encrypted; refresh advances credential version. This is a generic authorization-code/PKCE client, not verified-provider certification.
  • origin is exact scheme/host/optional port. pathPrefix permits only its exact path or descendants. DNS/address checks run at validation and dispatch.
  • Private RFC1918/IPv6 ULA is allowed. Unspecified, loopback, link-local, IPv4 multicast/reserved (224.0.0.0/4 and above), IPv6 multicast, and mapped loopback/link-local forms are blocked.
  • AUTOMATION_NETWORK_EXCEPTIONS bypass address/HTTPS blocks only for exact origins, never credential origin/path binding or redirect rejection.

Task Import And Export

Prefix: /api/v1/workspaces/:wid. Use the migration guide for a reviewed transfer workflow.

Task Import And Export: reference table
MethodPathInput / result
POST/items/import{nodeId,format,data}; items:write; success 201 {imported,ids}
GET/items/exportformat=json|csv; items:read; attachment response
Task Import And Export: reference table
Import ruleContract
TargetOne list; strict request; format is json or csv
SizeAt most 500 rows / 1,000,000 data characters
JSONArray of row objects
CSVHeader row with title
ValidationFully checked before write; 400 Row N: reason; all-or-nothing transaction with one count-only audit
Task Import And Export: reference table
ColumnMapping / validation
titleRequired; 1–300 characters
descriptionTask description
statusDestination-list ID or exact status name
priorityTask priority enum
startDate, dueDateYYYY-MM-DD; start not after due
tagsSemicolon-separated; trimmed/deduplicated; ≤30 tags of ≤60 characters
assigneeEmail of active workspace member
custom:<ExactFieldName>Existing typed field; JSON may instead use customFields object

CSV coercion is strict: finite numbers; true/false/yes/no/1/0 checkboxes; in-range integer ratings; semicolon-split checklists.

Task Import And Export: reference table
Export optionContract
formatjson or csv
nodeId?Any node, expanded to descendant lists
status?, search?Exact status ID / literal title-description substring
limit?Default 1,000; maximum 5,000; creation order
JSONAPI-shape items
CSVStandard columns below, plus union of ≤50 custom:<FieldName> columns; semicolon-joined lists, empty nulls
HeadersContent-Disposition: attachment; text/csv or application/json
Standard task CSV export columns
id, title, description, status, priority, startDate, dueDate,
tags, assigneeEmail, nodeId, nodePath, createdAt, updatedAt

Site Settings

Prefix: /api/v1. Settings mutations require site-administrator access and commit with audit.

Site Settings: reference table
MethodPathInput / result
GET/site/settingslandingDisabled, mcpSseEnabled, showSidebarAttribution; also reports operator LANDING_ENABLED=true
PATCH/site/settingsToggle those booleans; site admin
PUT/site/logo{contentType,data}; base64 PNG/JPEG/WebP/SVG ≤300 KB; site admin
GET/site/logoPublic bytes; 300-second cache
DELETE/site/logoRemove; site admin
  • showSidebarAttribution defaults true and controls the instance-wide By WNZN watermark; also exposed safely by /config.
  • Public landingEnabled defaults false; stays false when operator/admin disables it. The application’s landing template is apps/web/src/landing.json, separate from this docs site.
  • /config exposes the current logo URL when set.

Errors

Errors: reference table
StatusMeaning
400Validation failure
401Missing/expired credentials or login failure
403Permission or Origin denial
404Missing scoped resource
409State/version conflict
413Payload too large
429Rate/concurrency limit
502Invalid/failed AI response
503Disabled/unavailable integration
504Bulk deadline before headers

Storage/OIDC errors are sanitized, never raw upstream bodies. Post-header stream failures abort the connection rather than return a successful partial export.