REST API
Mill serves JSON under/api, remote MCP at /mcp, and OAuth endpoints on the same origin configured by MILL_BASE_URL. REST and MCP share domain validation, transactions, versions, retry behavior, and current-role checks. Personal REST keys use the human owner’s accessible boards; MCP OAuth additionally enforces scopes and optional approved boards.
Start with REST and MCP clients for connections, accounts and team access for human routes, or access security for authorization boundaries.
Authentication and limits
Browser requests use themill_session cookie; mutations require the configured public Origin. REST and MCP clients can use Authorization: Bearer mill_… from a personal or team API key. The key’s stored grant limits the action; personal keys are also bounded by the owner’s current active membership and role. OAuth tokens belong to the person approving the connection and authorize /mcp only, not public REST.
Authenticated requests are limited to 240 per minute per person or credential; anonymous requests to 120 per minute per client address. MCP dispatch also uses the REST limit. 429 includes Retry-After: 60. Ordinary request bodies are bounded to 2 MiB, credential/OAuth bodies to 16 KiB, and MCP requests/tool responses to 1 MiB.
Use Idempotency-Key for POST/PATCH/DELETE under /api outside human authentication. Keys are 8–128 letters, digits, or . _ : - without spaces. A key binds the person/credential, method, path/query, and exact JSON bytes for 24 hours. Exact retries replay saved status/body with Idempotency-Replayed: true; different content with the same key returns 409. Saved responses are encrypted, including one-time tokens.
Permanent deletion clears cached response content referencing removed work while retaining retry identities. Those retries return 410 with code:"retry_invalidated" and cannot recreate the work; delete acknowledgments remain retryable. Upgrading to the fixed-status schema also invalidates completed old responses while retaining their keys because their shapes are obsolete. Authentication has separate one-time challenge/token behavior.
Responses and pagination
Collections return{items}; paginated collections also return {hasMore,nextCursor}. Treat cursors as opaque and continue with the same filters. Tasks, comments, activity, and notifications default to 50 items and accept limit=1..100; boards default to 100 with the same maximum. Credentials default/max 200 and invitations default/max 100. The team directory returns at most 1,000 members per page and includes hasMore and nextCursor for continuation. A changed member directory returns 409 member_list_changed; restart without a cursor.
Boards use case-insensitive name order, then the exact name and UUID to resolve ties: lower(name),name,id. directory=true supports the same complete accessible directory for navigation. A board cursor binds the person, accessible board set, directory mode, and list revision. A changed board list returns 409 with code:"board_list_changed"; restart without a cursor. Invalid/foreign cursors return 400. Task continuation also binds a list revision: a concurrent edit, status change, insertion, or deletion returns 409 with code:"task_list_changed". Restart the current filtered/sorted task list without a cursor; do not continue old pages. Credential/invitation cursors are owner/workspace-scoped UUID anchors.
Single mutations return {board}, {task}, or {comment}. Domain deletes return {ok:true}. Fields are camelCase. Resource IDs are UUIDs; a task’s stable human identifier such as OPS-17 is a label, not the API path ID.
Generic API failures return {error:{code,message,requestId}}. Use error.code to choose recovery behavior and error.message for readable feedback. The same correlation ID appears in X-Request-Id on successful and failed requests. A supplied request ID must be at most 100 characters containing only letters, digits or ._:-; invalid values are replaced with a generated UUID. IDs do not authorize an action. Server failures omit database details and credentials.
An expired identity-confirmation window returns 403 REAUTHENTICATION_REQUIRED; complete real identity verification before retrying the initiating operation, at most once automatically. Other 403 failures do not request reauthentication. Unknown fields in authentication and domain mutation bodies are rejected. OAuth protocol failures retain {error,error_description} rather than the generic envelope.
Permissions
Viewers read work, comments, and task history. Members also create/change boards and tasks, comment, and permanently delete tasks. Admin browser sessions can also permanently delete boards and manage membership/workspace settings. An Administrative personal or team API key can perform permitted domain administration, including board deletion, but cannot manage people or security. Every active member can access workspace boards. Personal API keys are bounded by current access and their stored grant. Team keys use their stored team grant. OAuth connections can narrow access to approved boards and read or read/write scope. Personal credentials require the owner’s active membership and current role. Team keys use a stored team policy independent of the creator’s later membership. Identity, membership, account security, credential management, and consent require a browser session. API keys cannot call any/api/auth route, including the team directory. Board-restricted OAuth connections cannot create boards or list members; unscoped OAuth can resolve basic member metadata through MCP. Notifications stay within the owner’s account and, for scoped OAuth, approved boards. Authorization is rechecked before a mutation commits.
Boards
Names are 1–100 characters and descriptions at most 10,000. Custom prefixes match
^[A-Z][A-Z0-9]{1,9}$ and stay fixed after creation. There is no manual board order.
Board list items include backlogCount, todoCount, inProgressCount and activeCount for all tasks in each board. The first three count only their corresponding statuses. The activeCount aggregate counts todo, in_progress and in_review, excluding Backlog and terminal states. Cards show Backlog, To Do and In Progress. These counts are independent of task pagination. Individual board, create and update responses keep their existing fields.
Board deletion permanently removes its tasks, comments, notifications, and task activity. Explicit OAuth board scopes lose that board and are revoked when no approved boards remain. There is no archive, trash, or restore API.
Fixed task statuses
Every board uses these same values; there are no status resources or custom-status routes.Tasks
PATCH can change status and other fields atomically. It requires the current row version, which increments on mutation. An actual status change records
task.moved with {fromStatus,status}; edited non-status fields record task.updated. Existing historical activity details remain intact after migration and may contain earlier field names.
Tasks have one optional human assignee. Task creation, updates and filters reject agentId; responses do not contain Agent fields.
Start and due dates are optional calendar dates without a time or time zone. Responses include both fields; each can be set or cleared independently through creation or PATCH.
Task deletion removes only that task and its discussion, notifications, and activity. Former subtasks are independent tasks after migration and survive deletion of their former parent. Repeating a delete without its retry key returns 404. Task numbers are not reused within an existing board.
Task detail returns the complete requested task. commentLimit and activityLimit accept 0–100 and default to 100 for REST. Each preview has {hasMore,nextCursor}; use the corresponding comments/activity endpoint to continue. A zero-sized preview has a null cursor; start that endpoint without a cursor.
Task responses include type, either task or bug. Creation defaults to task; updates can change type with the current version and record it in activity. Type edits preserve the task identifier and status-change clock.
Without a status filter, Done and Won’t Do tasks remain visible for 24 hours after entering that status, then leave the default list. Search and other filters retain this rule. Set status=done or status=wont_do to include all tasks in that status, including older ones. Tasks remain accessible by their direct links. Task responses include read-only statusChangedAt; unrelated edits do not restart the window. Counts and pagination cover the visible results, and expiry invalidates default-list pagination revisions.
Creation/update sorts are newest first. Title is case-insensitive ascending; due date is ascending with undated tasks last; priority runs urgent through none; status follows Backlog, Todo, In Progress, In Review, Done, Won’t Do. UUIDs break ties. Page requests clamp to the last available page and return the actual
page with total and revision. Send that revision on subsequent page requests to reject a changed result with 409 task_list_changed. Cursor continuation remains available for clients.
Comments, mentions, and activity
Comment Markdown is nonempty and at most 10,000 characters. Mentions use
@their-email, user:UUID links, or active UUIDs in mentionIds. Assignment/mention notifications respect in-app preferences. Comments cannot be edited after creation. The author or an Admin can delete a comment. An OAuth client can delete its owner’s comments within approved boards and write scope, but cannot moderate others. Activity retains actorId, actorName, and actorKind (human, oauth, or team) so readers can distinguish a person from a team key.
New activity records also include detail.connection with type (session, api-key, or oauth) and the originating requestId. This distinguishes browser, REST and MCP changes; team-key actions carry a team actor. It contains no tokens or credential values and survives revocation. Historical records without this metadata remain unattributed to a connection type.
Notifications and settings
Read-state changes default to
read:true; false marks unread. An OAuth client’s reads/writes remain in its owner’s account and board scope. A human can PATCH /api/auth/profile with notificationPreferences:{assignments,mentions}. Task notifications are in-app only; email task delivery is excluded.
Earlier-client compatibility
Columns/custom statuses, task moves/manual order, task parents/subtasks, labels, and portable export/import are removed. Their routes return404; old structural payload fields and list queries such as columnId, label, or parentId return 400. subtaskLimit is rejected, and sort=position is unsupported. Archive/deleted-state query parameters also return 400. Change task status through PATCH with a fixed status value and current version.
A retained completed retry key from before migration returns terminal 410, including on a removed route, rather than replaying an obsolete shape or repeating the mutation. See upgrades before changing an existing installation.
Credentials
All four creation fields are required.
name is trimmed, nonempty, and at most 120 characters; access is read or edit; includeAdmin is a boolean. The three UI choices map to {access:"read",includeAdmin:false} (Read-only), {access:"edit",includeAdmin:false} (Edit), and {access:"edit",includeAdmin:true} (Administrative permissions). expiresAt is a future timestamp or null for Never; Never requires an Admin issuer, with any of the three permission choices. The UI offers 30 days, 90 days, 1 year, and Never, with no preselected value. expiresInDays, agentId, scopes, and boardIds are rejected. The token is revealed once; later responses contain metadata without tokens or hashes.
Personal keys belong to their owner, require current active membership, and apply the stored grant within the current role. Demotion permanently narrows the stored grant; promotion does not restore it. Team keys have userId:null and a stored team grant independent of the creator’s future membership. Team-key creation/list/revocation is Admin-only. Both key types can use REST and MCP, and neither can use browser-only identity, membership, security, or credential-management routes. The personal listing also includes the owner’s OAuth connections with their scopes and approved-board metadata. Revoked credentials are excluded before pagination; expired credentials remain listed until revoked. Neither credential type has Agent fields.
Team-key comments identify a team author in responses, with authorKind:"team", a team-key name, and authorId:null; the creator’s ID is retained only as a database reference. Edit team keys may remove team-authored comments, but cannot remove a person’s comment through creator ownership. Team-key assignments and mentions notify the creator if they are a recipient. Activity attributes the action to the team key.
Health and backups
GET /health/live checks service liveness. GET /health/ready checks PostgreSQL and returns the application version; neither needs a session or exposes account data. Full PostgreSQL backup and restore preserve work and identity. There is no portable export/import endpoint.
OAuth and MCP
The base protected-resource discovery path is also supported. Registration accepts
client_name, redirect_uris, token_endpoint_auth_method (none, client_secret_basic, client_secret_post), grant_types:["authorization_code"], and response_types:["code"]. Confidential clients receive a one-time secret; public clients use PKCE without a secret.
Consent details return clientName, clientId, clientTrust, redirectUri, scope, expiresIn, user:{name,role}, and canApprove. A Viewer cannot approve requested write access. Approval creates a connection owned by that person. Omit boardIds to approve all accessible boards or provide 1–100 unique existing board UUIDs to restrict the connection. The server rechecks current membership and role during approval, token exchange and authenticated requests. There is no identity-selection step; agentId is rejected.
Authorization needs response_type=code, client_id, exact redirect_uri, resource, scope=read or scope=read write, code_challenge, and code_challenge_method=S256. state is returned unchanged, and iss identifies Mill. Token exchange needs grant_type=authorization_code, code, code_verifier, the same redirect_uri and resource, and the client’s declared authentication method. Consent codes expire after two minutes and can be consumed once. Tokens expire after 30 days; refresh-token grants are not supported.
OAuth errors use {error,error_description} with OAuth error names. Unauthenticated MCP responses include the protected-resource metadata URL in WWW-Authenticate. A tool outside the credential’s access returns insufficient_scope. Tool operation failures appear as MCP isError:true results and preserve the REST error message. See client connection steps for the supported workflow and revocation behavior.