Cross-App API

Cross-App API#

Bearer-token-authenticated endpoints for external applications. All requests must carry an Authorization: Bearer <token> header containing an access token that was issued via Prism’s OAuth flow with the appropriate cross-app scope.

See Cross-App Integration for the full setup guide.

Authentication#

Every request must include a valid Prism access token:

http
Authorization: Bearer <access_token>

Glint verifies the token by calling Prism’s introspection endpoint (/api/oauth/introspect). The token must be:

  1. Active — not expired or revoked

  2. Scoped — must include app:<glint_client_id>:<required_scope>

After verifying the token, Glint resolves the user’s team membership from either a KV cache (populated on prior Glint login) or a live Prism fetch (if the token includes teams:read).

Scope → Endpoint Mapping#

Each endpoint requires a specific Prism scope and the corresponding Glint role permission. Both must be satisfied.

Registering these scopes in Prism

The Cross-App Integration guide ships a copy-pasteable JSON block of all scope definitions, ready for Prism’s Import dialog (Glint app → Permissions → Import).

Scope
Endpoints permitted
read_todosGET /api/cross-app/me
GET /api/cross-app/teams/:teamId/sets
GET /api/cross-app/teams/:teamId/sets/:setId/todos
GET /api/cross-app/teams/:teamId/todos/:todoId/comments
GET /api/cross-app/teams/:teamId/sets/:setId/export
GET /api/cross-app/teams/:teamId/sets/:setId/sse
* /api/cross-app/teams/:teamId/sets/:setId/ws (WebSocket upgrade)
create_todosPOST /api/cross-app/teams/:teamId/sets/:setId/todos
POST /api/cross-app/teams/:teamId/sets/:setId/import (append mode)
edit_todosPATCH /api/cross-app/teams/:teamId/todos/:todoId (title field only)
complete_todosPATCH /api/cross-app/teams/:teamId/todos/:todoId (completed field only)
delete_todosDELETE /api/cross-app/teams/:teamId/todos/:todoId
reorder_todosPOST /api/cross-app/teams/:teamId/todos/reorder
assign_todosPUT /api/cross-app/teams/:teamId/todos/:todoId/assignees
manage_setsPOST /api/cross-app/teams/:teamId/sets
PATCH /api/cross-app/teams/:teamId/sets/:setId (incl. auto-renew, timezone, split-completed)
DELETE /api/cross-app/teams/:teamId/sets/:setId
POST /api/cross-app/teams/:teamId/sets/reorder
POST /api/cross-app/teams/:teamId/sets/import (import-as-new-set)
POST /api/cross-app/teams/:teamId/sets/:setId/import with mode: "replace" (also requires create_todos)
commentPOST /api/cross-app/teams/:teamId/todos/:todoId/comments
delete_commentsDELETE /api/cross-app/teams/:teamId/todos/:todoId/comments/:commentId
read_settingsGET /api/cross-app/teams/:teamId/settings
manage_settingsPATCH /api/cross-app/teams/:teamId/settings
write_todosLegacy catch-all: accepted wherever create_todos, edit_todos, or complete_todos is required

The write_todos scope is kept for backward compatibility with existing integrations. New integrations should request the specific scopes they need.

Identity#

GET /api/cross-app/me#

Resolve the calling user from the bearer token. Useful for showing “logged in as X” in App B without making a separate Prism userinfo call.

Required scope: read_todos (re-used as the lowest-friction baseline)

Response 200:

json
{
  "user": {
    "id": "user_id",
    "username": "alice",
    "displayName": "Alice Liddell",
    "avatarUrl": "https://example.com/a.png",
    "teams": [
      { "id": "team_id", "name": "Tea Party", "role": "owner", "avatarUrl": null }
    ]
  },
  "grantedScopes": ["read_todos", "create_todos", "..."]
}

grantedScopes lists the inner scopes (without the app:<glint_id>: prefix) granted by this token, so App B can branch on them client-side instead of probing endpoints.

Sets#

GET /api/cross-app/teams/:teamId/sets#

List all todo sets in a team.

Required scope: read_todos

Response 200:

json
{
  "sets": [
    {
      "id": "uuid",
      "userId": "user_id_of_creator",
      "name": "Sprint 12",
      "sortOrder": 1,
      "autoRenew": false,
      "renewTime": "00:00",
      "timezone": "",
      "lastRenewedAt": null,
      "splitCompleted": false,
      "createdAt": "2026-04-17T09:00:00.000Z"
    }
  ]
}

Errors:

Status
Cause
401Missing, malformed, or expired bearer token
403Token lacks read_todos scope
403User is not a member of teamId
403Team membership unavailable — include teams:read in scope or have the user log in to Glint once

POST /api/cross-app/teams/:teamId/sets#

Create a new todo set. Requires the manage_sets Glint permission.

Required scope: manage_sets

Request body:

json
{ "name": "Q3 Goals" }
Field
Type
Required
Description
namestringYesSet name. Whitespace is trimmed.

Response 201:

json
{
  "set": {
    "id": "uuid",
    "name": "Q3 Goals",
    "sortOrder": 3,
    "createdAt": "2026-04-17T09:00:00.000Z"
  }
}

Errors:

Status
Cause
400name is missing or empty
401Token missing or inactive
403Token lacks manage_sets scope
403User’s manage_sets permission denied

PATCH /api/cross-app/teams/:teamId/sets/:setId#

Rename or reconfigure a set. Requires the manage_sets Glint permission, or the requesting user must own the set.

Required scope: manage_sets

Path parameters:

Parameter
Description
teamIdPrism team ID
setIdTodo set ID

Request body (all fields optional, send only the ones you want to change):

json
{
  "name": "Renamed Set",
  "autoRenew": true,
  "renewTime": "06:00",
  "timezone": "America/Los_Angeles",
  "splitCompleted": true
}
Field
Type
Description
namestringNew name (whitespace trimmed, must not be empty if sent)
autoRenewbooleanEnable daily reset of completed todos
renewTimestringHH:MM time of day for auto-renew
timezonestringIANA timezone (empty = team default)
splitCompletedbooleanShow completed root todos in their own section

Response 200:

json
{ "ok": true }

Errors:

Status
Cause
400No fields provided, or name is empty
401Token missing or inactive
403Token lacks manage_sets scope
403User lacks manage_sets permission and does not own the set
404Set not found or does not belong to teamId

DELETE /api/cross-app/teams/:teamId/sets/:setId#

Delete a set and all its todos. Requires manage_sets permission, or the requesting user must own the set.

Required scope: manage_sets

Response 200:

json
{ "ok": true }

Errors:

Status
Cause
401Token missing or inactive
403Token lacks manage_sets scope
403User lacks manage_sets permission and does not own the set
404Set not found or does not belong to teamId

POST /api/cross-app/teams/:teamId/sets/reorder#

Persist a new sort order for sets after a drag-and-drop reorder. Send only the sets whose sortOrder changed.

Required scope: manage_sets

Request body:

json
{
  "items": [
    { "id": "set-uuid-1", "sortOrder": 1 },
    { "id": "set-uuid-2", "sortOrder": 2 }
  ]
}

Response 200: { "ok": true }

Errors: 400 empty items; 401 token; 403 scope or permission.

GET /api/cross-app/teams/:teamId/sets/:setId/export#

Export a set as Markdown, JSON, or YAML. Mirrors the native export endpoint.

Required scope: read_todos

Query params:

Param
Default
Description
formatmdOne of md, json, yaml
includeComments0Set to 1 to include comments in the export

Response 200:

json
{
  "format": "md",
  "fileName": "Sprint_12.md",
  "content": "- [x] First todo\n- [ ] Second todo"
}

Errors: 403 scope or view_todos permission; 404 set not found.

POST /api/cross-app/teams/:teamId/sets/:setId/import#

Import todos into an existing set. mode: "replace" wipes existing todos first and additionally requires the manage_sets scope and permission.

Required scope: create_todos or write_todos (replace mode also requires manage_sets)

Request body:

json
{
  "format": "md",
  "content": "- [ ] Todo A\n- [ ] Todo B",
  "mode": "append",
  "includeComments": false,
  "insertAt": "bottom"
}
Field
Type
Default
Description
formatstring"md"md, json, or yaml
contentstringThe serialized payload to import
modestring"append""append" or "replace" (replace requires manage_sets)
includeCommentsbooleanfalseImport comments embedded in the payload
insertAtstring"bottom""top" or "bottom"

Response 200: { "ok": true, "imported": 7 }

Errors: 400 parse failure or empty content; 403 missing scope/permission (replace mode without manage_sets).

POST /api/cross-app/teams/:teamId/sets/import#

Import a payload as a brand-new set in the team.

Required scope: manage_sets

Request body:

json
{
  "format": "yaml",
  "content": "version: 1\nset:\n  id: my-set-id\n  name: Imported\ntodos: []",
  "includeComments": false,
  "setName": "Optional override",
  "setId": "optional-override-uuid"
}

For markdown imports the set ID is always generated; setName defaults to "Imported Set" if not supplied.

Response 200:

json
{
  "ok": true,
  "imported": 0,
  "set": {
    "id": "uuid",
    "userId": "user_id_of_creator",
    "name": "Imported",
    "sortOrder": 4,
    "autoRenew": false,
    "renewTime": "00:00",
    "timezone": "",
    "lastRenewedAt": null,
    "splitCompleted": false
  }
}

Errors: 400 empty content / missing name / parse failure; 409 setId already exists in this team.

Todos#

GET /api/cross-app/teams/:teamId/sets/:setId/todos#

List all todos in a set. Respects the user’s view_todos permission.

Required scope: read_todos

Response 200:

json
{
  "todos": [
    {
      "id": "uuid",
      "userId": "user_id_of_creator",
      "parentId": null,
      "title": "Write unit tests",
      "completed": false,
      "sortOrder": 1,
      "commentCount": 2,
      "assignees": [
        {
          "userId": "user_id",
          "name": "Display Name",
          "username": "handle",
          "avatarUrl": "https://example.com/a.png"
        }
      ],
      "createdAt": "2026-04-01T10:00:00.000Z",
      "updatedAt": "2026-04-01T10:00:00.000Z"
    },
    {
      "id": "uuid-2",
      "userId": "user_id_of_creator",
      "parentId": "uuid",
      "title": "Cover edge cases",
      "completed": true,
      "sortOrder": 1,
      "commentCount": 0,
      "assignees": [],
      "createdAt": "2026-04-01T10:05:00.000Z",
      "updatedAt": "2026-04-02T08:00:00.000Z"
    }
  ]
}

Todos are returned flat. Sub-todos have a non-null parentId referencing their parent. Assignee name / username / avatarUrl are resolved from Prism for team spaces; for personal spaces only the calling user is resolved.

Errors:

Status
Cause
401Token missing or inactive
403Insufficient scope or team membership
403User’s view_todos permission denied for this set
404Set not found or does not belong to teamId

POST /api/cross-app/teams/:teamId/sets/:setId/todos#

Create a new todo. When parentId is provided, the add_subtodos Glint permission is required instead of create_todos.

Required scope: create_todos or write_todos

Request body:

json
{
  "title": "Write documentation",
  "parentId": "optional-parent-uuid"
}
Field
Type
Required
Description
titlestringYesThe todo title. Whitespace is trimmed.
parentIdstringNoParent todo ID. Requires add_subtodos permission.

Response 201:

json
{
  "todo": {
    "id": "uuid",
    "parentId": null,
    "title": "Write documentation",
    "completed": false,
    "sortOrder": 5,
    "createdAt": "2026-04-17T09:00:00.000Z",
    "updatedAt": "2026-04-17T09:00:00.000Z"
  }
}

Errors:

Status
Cause
400title is missing or empty
401Token missing or inactive
403Token lacks create_todos or write_todos scope
403User’s create_todos permission denied (or add_subtodos when using parentId)
404Set not found, or parentId does not exist in this set

PATCH /api/cross-app/teams/:teamId/todos/:todoId#

Update a todo’s title or completion state. Each field requires its own scope and Glint permission:

Field
Required scope
Required Glint permission
titleedit_todos or write_todosedit_own_todos (own) / edit_any_todo (others’)
completedcomplete_todos or write_todosNone for own todos; complete_any_todo for others’

Both fields may be updated in a single request. Each is checked independently.

Path parameters:

Parameter
Description
teamIdPrism team ID
todoIdTodo ID

Request body (all fields optional, at least one required):

json
{
  "title": "Updated title",
  "completed": true
}

Response 200:

json
{ "ok": true }

Errors:

Status
Cause
401Token missing or inactive
403Token lacks the required scope for the requested field (edit_todos/complete_todos or write_todos)
403User’s edit_any_todo or complete_any_todo permission denied
404Todo not found or does not belong to teamId

DELETE /api/cross-app/teams/:teamId/todos/:todoId#

Delete a todo. Requires delete_own_todos for own todos, delete_any_todo for others’.

Required scope: delete_todos

Response 200:

json
{ "ok": true }

Errors:

Status
Cause
401Token missing or inactive
403Token lacks delete_todos scope
403User’s delete_own_todos or delete_any_todo permission denied
404Todo not found or does not belong to teamId

POST /api/cross-app/teams/:teamId/todos/reorder#

Persist new sort orders for one or more todos after a drag-and-drop reorder. Sub-todos and root todos can be sent in the same batch.

Required scope: reorder_todos

Request body:

json
{
  "items": [
    { "id": "todo-uuid-1", "sortOrder": 1 },
    { "id": "todo-uuid-2", "sortOrder": 2 }
  ]
}

Response 200: { "ok": true }

Errors: 400 empty items; 401 token; 403 missing scope or reorder_todos permission.

PUT /api/cross-app/teams/:teamId/todos/:todoId/assignees#

Replace the full set of assignees on a todo. Send the complete desired list of user IDs; IDs that are not members of the team are ignored. Pass [] to unassign everyone. (Replaces the legacy single-user “claim” endpoint.)

Required scope: assign_todos

Request body:

json
{ "userIds": ["user-uuid-1", "user-uuid-2"] }

Response 200:

json
{
  "ok": true,
  "assignees": [
    {
      "userId": "user-uuid-1",
      "name": "Display Name",
      "username": "handle",
      "avatarUrl": "https://example.com/a.png"
    }
  ]
}

Errors:

Status
Cause
401Token missing or inactive
403Token lacks assign_todos scope or user lacks assign_todos permission
404Todo not found or does not belong to teamId
503Database missing the todo_assignees table (run migrations)

GET /api/cross-app/teams/:teamId/members#

List the members that todos in this workspace can be assigned to.

Required scope: read_todos

Response 200: { "members": [{ "userId", "name", "username", "avatarUrl" }] }

GET /api/cross-app/teams/:teamId/assigned-to-me#

Incomplete todos assigned to the caller in this workspace, grouped by list. Identical shape to the native GET /api/teams/:teamId/assigned-to-me.

Required scope: read_todos

GET /api/cross-app/assigned-to-me#

Every incomplete todo assigned to the caller across all their workspaces, grouped by workspace. This is the endpoint derived apps use to build a cross-workspace “my work” view; it partitions results by workspace so an app can lay them out per team.

Required scope: read_todos (no team path parameter — the token’s user is resolved and all of their memberships are queried).

Response 200:

json
{
  "workspaces": [
    {
      "teamId": "team-uuid",
      "name": "Acme Engineering",
      "kind": "team",
      "todos": [
        {
          "id": "todo-uuid",
          "setId": "set-uuid",
          "setName": "Sprint 12",
          "parentId": null,
          "title": "Set up CI pipeline",
          "updatedAt": "2026-04-01T10:00:00.000Z"
        }
      ]
    },
    {
      "teamId": "personal:user-uuid",
      "name": "Ada Lovelace",
      "kind": "personal",
      "todos": []
    }
  ]
}
Field
Type
Description
workspaces[].teamIdstringWorkspace (team) ID, or personal:<userId> for the personal space.
workspaces[].namestringWorkspace display name.
workspaces[].kind"team" | "personal"Whether this is a team or the caller’s personal space.
workspaces[].todosarrayIncomplete todos assigned to the caller in that workspace.

Completed todos are omitted (the assignment is retained but hidden).

Comments#

GET /api/cross-app/teams/:teamId/todos/:todoId/comments#

List all comments on a todo. Requires view_todos permission on the todo’s set.

Required scope: read_todos

Response 200:

json
{
  "comments": [
    {
      "id": "uuid",
      "userId": "user-uuid",
      "username": "alice",
      "body": "Looks good!",
      "createdAt": "2026-04-17T10:00:00.000Z"
    }
  ]
}

Errors:

Status
Cause
401Token missing or inactive
403Token lacks read_todos scope
403User’s view_todos permission denied for this set
404Todo not found or does not belong to teamId

POST /api/cross-app/teams/:teamId/todos/:todoId/comments#

Post a comment on a todo. Requires the comment Glint permission.

Required scope: comment

Request body:

json
{ "body": "Looks good to me!" }
Field
Type
Required
Description
bodystringYesComment text. Whitespace is trimmed.

Response 201:

json
{
  "comment": {
    "id": "uuid",
    "userId": "user-uuid",
    "username": "alice",
    "body": "Looks good to me!",
    "createdAt": "2026-04-17T10:00:00.000Z"
  }
}

Errors:

Status
Cause
400body is missing or empty
401Token missing or inactive
403Token lacks comment scope
403User’s comment permission denied for this set
404Todo not found or does not belong to teamId

DELETE /api/cross-app/teams/:teamId/todos/:todoId/comments/:commentId#

Delete a comment. Requires delete_own_comments for own comments, delete_any_comment for others’.

Required scope: delete_comments

Response 200:

json
{ "ok": true }

Errors:

Status
Cause
401Token missing or inactive
403Token lacks delete_comments scope
403User’s delete_own_comments or delete_any_comment permission denied
404Todo or comment not found

Settings#

GET /api/cross-app/teams/:teamId/settings#

Read team settings. Available to any team member.

Required scope: read_settings

Response 200:

json
{
  "settings": {
    "site_name": "Acme Todos",
    "site_logo_url": "",
    "accent_color": "#6366f1",
    "default_set_name": "Not Grouped",
    "allow_member_create_sets": false,
    "default_timezone": "UTC"
  }
}

Errors:

Status
Cause
401Token missing or inactive
403Token lacks read_settings scope
403User is not a member of teamId

PATCH /api/cross-app/teams/:teamId/settings#

Update team settings. Requires the manage_settings Glint permission (owner or explicitly granted admin).

Required scope: manage_settings

Request body (all fields optional):

json
{
  "site_name": "Acme Todos",
  "accent_color": "#6366f1",
  "default_set_name": "Inbox",
  "allow_member_create_sets": true,
  "default_timezone": "America/New_York"
}

Only the fields listed below are accepted; any other keys are ignored:

Field
Type
Description
site_namestringDisplay name for the workspace
site_logo_urlstringURL for the workspace logo
accent_colorstringCSS color value for the accent color
default_set_namestringDefault name for the ungrouped set
allow_member_create_setsbooleanWhether members can create new sets
default_timezonestringIANA timezone identifier

Response 200:

json
{
  "settings": { "...": "full updated settings object" }
}

Errors:

Status
Cause
401Token missing or inactive
403Token lacks manage_settings scope
403User’s manage_settings permission denied

Realtime#

Subscribe to live updates for a single set. Both endpoints require read_todos and team membership; events are pushed for any todo, comment, or assignment change in that set, regardless of which client made the change (native UI, cross-app API, or share link).

WebSocket clients must include Authorization: Bearer <token> on the upgrade request — browser WebSocket doesn’t support custom headers, so most cross-app integrations will run from a server or a non-browser client.

GET /api/cross-app/teams/:teamId/sets/:setId/sse#

Server-Sent Events stream. Re-uses the same Durable Object as the native UI so events are identical.

Required scope: read_todos

Response: text/event-stream with events such as:

event: todo:created
data: {"setId":"uuid","todo":{...}}

event: todo:updated
data: {"setId":"uuid","todo":{"id":"uuid","completed":true}}

event: todo:assigned
data: {"setId":"uuid","id":"uuid","assignees":[{"userId":"user_id","name":"Alice","username":"alice","avatarUrl":null}]}

* /api/cross-app/teams/:teamId/sets/:setId/ws#

WebSocket upgrade endpoint. Same event payloads as SSE.

Required scope: read_todos

Status
Cause
200HEAD probe — confirms the endpoint is reachable
426GET without Upgrade: websocket header
503Durable Object binding TODO_SYNC is missing on the worker

Common Error Shape#

All errors return JSON:

json
{ "error": "Human-readable description" }
Message fragment
Meaning
"Token inactive or expired"Introspection returned active: false
"Missing required scope. Token must include one of: …"Token exists but lacks any of the required cross-app scopes
"Not a member of this team"User is not in the requested team
"Team membership unavailable"KV cache miss and no teams:read in token scope
"No permission to view todos"User’s view_todos permission is off for this set
"No permission: create_todos"User’s create_todos permission denied
"No permission: add_subtodos"User’s add_subtodos permission denied
"No permission to edit this todo"Missing edit_own_todos or edit_any_todo
"No permission to toggle completion"Missing complete_any_todo for another user’s todo
"No permission to delete this todo"Missing delete_own_todos or delete_any_todo
"No permission to manage sets"User’s manage_sets permission denied
"No permission to comment"User’s comment permission denied for this set
"No permission to delete this comment"Missing delete_own_comments or delete_any_comment
"No permission to manage settings"User’s manage_settings permission denied
"Missing required scope: edit_todos or write_todos"Tried to update title without the right scope
"Missing required scope: complete_todos or write_todos"Tried to update completed without the right scope