Lists

Lists

Lists are structured collections of typed rows. Every list has a schema (a small DSL describing the columns) and a stream of data rows matching that schema. Lists can be public, organised into folders, linked together via connections, watched by other users, or synchronised from a GitHub repository.

All list endpoints accept either a session cookie or a Bearer token, making them fully accessible from native iOS (and other non-browser) clients.

Endpoint table

MethodPathAuthDescription
GET/api/listsSession or BearerYour lists. Query: limit, offset, page.
POST/api/listsSession or BearerCreate a list. Body: title, description, schema (DSL), optional parentId, isPublic. Subscriber only.
GET/api/lists/:idSession or BearerList metadata and schema.
PUT/api/lists/:idSession or BearerUpdate list metadata.
DELETE/api/lists/:idSession or BearerDelete a list.
GET/api/lists/:id/schemaSession or BearerGet list schema (properties).
PUT/api/lists/:id/schemaSession or BearerUpdate list schema.
POST/api/lists/:id/refreshSession or BearerRefresh a GitHub-backed list from source.
GET/api/lists/:id/dataSession or BearerList rows. Query: limit, offset.
POST/api/lists/:id/dataSession or BearerAdd a row. Body: { "rowData": { "Field": "value", ... } }.
GET/api/lists/:id/data/:rowIdSession or BearerGet one row.
PUT/api/lists/:id/data/:rowIdSession or BearerUpdate a row.
DELETE/api/lists/:id/data/:rowIdSession or BearerDelete a row.
GET/api/lists/searchSession or BearerSearch your lists by title.
GET/api/lists/:id/watchersSession or BearerUsers watching this list.
POST/api/lists/:id/watchersSession or BearerAdd a watcher to this list.
GET/api/lists/:id/watchers/meSession or BearerWhether the current user is watching.
GET/api/lists/:id/watchers/usersSession or BearerUsers with access (watchers, collaborators, managers).
PUT/api/lists/:id/watchers/:userIdSession or BearerChange a user's watcher role.
DELETE/api/lists/:id/watchers/:userIdSession or BearerRemove a user from list access.
GET/api/lists/connectionsSession or BearerAll connections between your lists.
POST/api/lists/connectionsSession or BearerCreate a directed connection. Body: fromListId, toListId, optional label.
DELETE/api/lists/connections/:idSession or BearerRemove a connection.

Public access to lists you've marked isPublic: true is documented in Public Profiles.

Creating a list

POST /api/lists
Content-Type: application/json

{
  "title": "Books to Read",
  "description": "My reading backlog.",
  "isPublic": true,
  "schema": "Title:text, Author:text, Year:number, Read:boolean"
}

Response (201):

{
  "id": "lst_abc001",
  "title": "Books to Read",
  "isPublic": true,
  "schema": "Title:text, Author:text, Year:number, Read:boolean",
  "createdAt": "2025-06-11T08:30:00.000Z"
}

The schema DSL accepts column definitions in Name:type form, comma-separated. Supported types include text, number, boolean, date, url, markdown, and a few others. Validation errors include a details map identifying the offending column.

Adding and reading rows

POST /api/lists/lst_abc001/data
Content-Type: application/json

{
  "rowData": {
    "Title": "The Dream Machine",
    "Author": "M. Mitchell Waldrop",
    "Year": 2001,
    "Read": false
  }
}

Response (201):

{
  "id": "row_xyz001",
  "listId": "lst_abc001",
  "rowData": { "Title": "The Dream Machine", "Author": "M. Mitchell Waldrop", "Year": 2001, "Read": false },
  "createdAt": "2025-06-11T08:35:00.000Z"
}

Fetch rows with pagination:

GET /api/lists/lst_abc001/data?limit=50&offset=0

Updating the schema

PUT /api/lists/:id/schema accepts two body shapes on the same route (there is no separate /schema/structured route); the server dispatches by payload:

  • DSL rebuild (destructive): body { "schema": "Title\nName:text, Done:boolean", "parentId"?, "isPublic"? }. Wipes and recreates all properties.
  • Structured update (non-destructive): body { "properties": [ { "id"?, "propertyKey", "propertyName", "propertyType", "displayOrder"?, "isVisible"?, "isRequired"?, "defaultValue"?, "helpText"?, "placeholder"? } ] }. Items with an existing id are updated in place (row data preserved); items without id are created; existing properties omitted from the array are soft-deleted and their key is stripped from every row. displayOrder is authoritative by array order (renumbered 0..n-1). Allowed propertyType: text, number, boolean, date, url, email.

The structured form returns 400 for a duplicate propertyKey, an unknown propertyType, an unknown id, or an attempt to change propertyKey for an existing id (rename propertyName instead). If a to-be-deleted property still holds non-null row data, the request fails 409 with { error, propertiesWithData: [...] } unless ?force=true is passed. Success returns { "properties": [ ...rows ordered by displayOrder... ] }.

Watchers

A watcher's role is one of watcher, collaborator, or manager.

  • GET /api/lists/:id/watchers (owner only) → { watchers: [ { id, userId, role, createdAt, user } ] }.
  • POST /api/lists/:id/watchers → body { userId?, role? }. With userId, the owner adds that user (list must be public; role defaults to watcher). Without userId, the caller self-subscribes (list must be public and not their own). Idempotent — returns { watching: true } (201 when newly created, 200 if already watching).
  • PUT /api/lists/:id/watchers/:userId (owner only) → body { role }; invalid/missing role → 400. Returns { role }.
  • DELETE /api/lists/:id/watchers/:userId (owner only) → { removed: true }.
  • GET /api/lists/:id/watchers/users (owner only) — search users to add. Query: search, limit (50), offset (0), excludeWatchers (comma-separated ids; if omitted, current watchers are auto-excluded). Returns { users, total, pagination }.

Connections

Connections create labelled, directed relationships between two of your lists — useful for modelling related datasets, parent/child structures, or any graph of linked lists.

POST /api/lists/connections
Content-Type: application/json

{ "fromListId": "lst_abc001", "toListId": "lst_abc002", "label": "references" }

Response (201): the created connection object.

ErrorCondition
400Missing fromListId or toListId, or both IDs are the same.
403The current user does not own both lists.
409The connection already exists.

Fetch all connections owned by the current user:

GET /api/lists/connections
{
  "connections": [
    { "id": "conn_abc001", "fromListId": "lst_abc001", "toListId": "lst_abc002", "label": "references", "createdAt": "..." }
  ]
}

Searching

GET /api/lists/search?q=books&limit=20&offset=0

Returns lists owned by the current user whose title matches. Paginated.

GitHub-backed lists

A list created with source: "github" (and the right configuration) reflects issues from a GitHub repository. Trigger an on-demand sync via:

POST /api/lists/:id/refresh

A cron job (/api/cron/sync-github-lists) also refreshes these lists periodically — see Cron & Webhooks.