CLOUDMORROW

Resources / Developers / Developer handbook

The API

The HTTP API every client and quill speaks.

API

method path
POST /api/auth/login {username, password} → bearer token
GET /api/auth/me the current user
POST /api/auth/password change your own password
GET /api/records/{model}/{id}/history who did what to a record, newest first — which fields, never a value
GET/PUT /api/access/{username} one person's access, and their own rules replaced whole (administrators)
GET /api/quills/{id}/brought what a Quill brought, with how many records hold each (administrators)
DELETE /api/quills/{id}?drop= remove a Quill; what drop names goes with it, records and all
GET /api/secrets?env= the keys, without the values (&reveal=true for those)
GET /api/secrets/vaults the vaults that hold something, with counts
GET /api/secrets/environments environments that hold something, in this vault
GET/PUT/DELETE /api/secrets/item/{key}?env= one secret, value included
POST /api/secrets/import set many at once — what a .env becomes
GET /api/secrets/export?env= a .env file, or &format=json
DELETE /api/secrets/environment/{env} drop a whole environment
DELETE /api/secrets/vault/{vault} drop a whole vault
GET /api/types every kind of data there is: fields, scopes, what is sealed, who uses it
GET /api/quills the installed Quills, each with its screens and its datamodels in full
GET /api/quills/catalog the Quill Catalog, with what is installed
POST /api/quills/plan {id} or {source, ref} — what installing would add (administrators)
POST/DELETE /api/quills, /api/quills/{id} install, or remove; records are kept (administrators)
POST /api/quills/upload a .tar.gz of a Quill's folder, as a development Quill — cm quill dev
GET /api/datamodels every datamodel on the server, with its fields and the Quills that use it
GET/POST /api/records/{model} your records of a datamodel (?field=value filters on indexed fields, ?field__gte=… and __gt, __lte, __lt are ranges); make one (scope for a space)
GET/PATCH/DELETE /api/records/{model}/{id} one record; send rev with a change to get a 409 rather than overwrite
POST /api/records/{model}/{id}/move {fields, index} — another lane or group, and a place in it
POST /api/records/{model} with scope, members, unique a space — a channel, a calendar — made public or shared, with its people; unique finds the one with exactly those people instead of making another
POST /api/records/{model}/{id}/members {username} — put somebody in a shared space; they are told
DELETE /api/records/{model}/{id}/members/{username} take somebody out, or with your own name leave
POST /api/records/{model}/{id}/seen you have looked in a space: what is in it is not unread
POST /api/snapshots a frozen copy of a space's records, with a digest, sealed and locked (ATTESTATIONS.md)
GET /api/snapshots/{id}, …/bundle the snapshot with its attestations and whether its bundle is intact; the bundle's bytes
POST /api/snapshots/{id}/attestations bring somebody's signed verdict back; checked against the digest before it is kept
POST/GET/DELETE /api/snapshots/{id}/guests[/{n}] a link that opens the one snapshot to somebody outside; the links given out; take one back
GET/POST /api/guest/snapshots/{id}?token=, …/bundle, …/attestations what a guest link opens, with no account
GET/POST /api/identity, /api/attest this server's signing key; an attestation signed with it, in your name
GET /api/records/{model}?_last=50&_since=… the newest fifty, still in order; only what changed at or after a moment
GET /api/changes a server-sent events stream of the records you may see being made, changed or deleted; ?since=<seq> to catch up
GET /api/people everybody a space could be shared with, or written to
GET/POST /api/shares your fileshares and the ones shared with you; {name, members, path} makes one (path for administrators); a share carries its url, access, members and, for its owner, warnings
GET/PATCH/DELETE /api/shares/{name} one share; PATCH {description, path} (path for administrators); ?remove_files=true deletes a folder in Shares
PUT /api/shares/{name}/members {kind: user|circle|everyone, who, access: write|read} — share it, or change what they may do
DELETE /api/shares/{name}/members/{kind}/{who} stop sharing it; with your own username, leave it
GET /api/shares/candidates who a share could be shared with: people, circles, and whether everybody
GET /api/shares/check?path= administrators: whether a directory could be a share, and what is wrong with it
* /dav/{name}/… the share itself, as WebDAV — Basic auth with your password or token
GET/POST/DELETE /api/agents… your machines
POST /api/agents/{id}/jobs queue work
GET/POST /api/notifications what the machines have been doing
POST /api/notifications/read mark them read; omit ids for all of them
GET /api/push/key the VAPID public key, to subscribe a browser with
POST /api/push/subscribe a PushSubscription, as the browser gives it
POST /api/push/unsubscribe {endpoint} — forget this device
GET /api/push/devices the devices this account pushes to
GET /api/push/badge {messages, notifications, badge} — the number on the icon
POST /api/push/test push yourself, to find out whether this phone is wired up
POST /api/agent/enroll, /heartbeat, /jobs/claim the agent's own endpoints
POST /api/agent/notifications a machine saying what it just did
GET /api/server/features what this server offers
PATCH /api/server/features/{key} {enabled} — switch one for everybody, admin only
GET /api/me/features what you may switch, and your answer on each
PATCH /api/me/features/{key} {enabled} — switch one for yourself
GET/POST/PATCH/DELETE /api/users… admin only
GET /api/health no auth; carries the cloud's name and whether it still needs setup
GET /setup the first-boot page, while the server has no accounts; redirects to /app after
POST /api/setup {name, username, password} — name the cloud and make its administrator; 409 once anybody exists
GET/PATCH /api/server/settings {name} — what this cloud is called; PATCH is admin only
GET /api/client what cloudmorrow update should install, no auth
POST /api/server/update deploy from git and restart — admin only
GET /, /install.sh the install page and script, no auth
POST /mcp the MCP server, JSON-RPC over HTTP — an MCP token, or a user token
GET /.well-known/oauth-protected-resource, /.well-known/oauth-authorization-server how an MCP client finds the sign-in flow, no auth
POST /oauth/register, /oauth/token an MCP client registering itself, and trading its code for tokens
GET/POST /oauth/authorize the page where you sign in and let an assistant in
GET/DELETE /api/mcp/connections… the assistants you have let in; delete one to cut it off

Interactive docs at /docs.

Send X-Cloudmorrow-Vault: <vault> (or ?vault=<vault>) to say which vault a secrets call is about; without one it is the default vault. Nothing else reads it — everything else belongs to the account outright.

Secret values are handed out one at a time, or all at once by /export; a listing describes them instead. Those responses carry Cache-Control: no-store.

PUT takes the rev you last read; a stale rev gets a 409 carrying the current revision and content, which is what drives the conflict dialog.

Agent tokens are a separate credential from user tokens: an agent token cannot read notes, and a user token cannot claim jobs. MCP tokens are a third: handed out by /oauth/token after somebody said yes on the sign-in page, kept hashed, and good for /mcp alone.

From docs/MANUAL.md in the Cloudmorrow repository. Something wrong? A pull request there fixes it here.