Skip to main content
Koru
Get started
API reference sections

HTTP API reference

Workspaces

Workspaces, their manifests, listings, pins and lifecycle.

34 operations. The contract has no example values, so example bodies are generated from each schema: every field is shown with a placeholder of its type.

Commit only the synced workspace pin and project its listing

PUT/v1/workspaces/{workspace}/pinned

committed workspace with applied or pending listing

Authorization
Bearer token with manage_workspaces

Parameters

NameInTypeDescription
workspacerequiredpathstring

Request body (required)

application/json WorkspacePin

FieldTypeDescription
pinnedrequiredboolean
Example request body
{
  "pinned": false
}

Responses

StatusDescriptionBody
200See above.PinnedWorkspace
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "workspace": {
    "id": "00000000-0000-4000-8000-000000000000",
    "owner": {},
    "name": "string",
    "created_at": 0,
    "last_opened_at": 0,
    "icon": "string",
    "color": "string",
    "pinned": false,
    "archived": false,
    "deleted_at": 0,
    "position": 0
  },
  "listing": {
    "state": "applied"
  }
}
Example request
curl -X PUT "http://127.0.0.1:$PORT/v1/workspaces/$WORKSPACE/pinned" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "pinned": false
}'

Repair the listing from verified durable workspace authority

POST/v1/workspaces/{workspace}/reconcile-listing

reconciled workspace; uncertainty refuses repair

Authorization
Bearer token with manage_workspaces

Parameters

NameInTypeDescription
workspacerequiredpathstring

Responses

StatusDescriptionBody
200See above.Workspace
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "id": "00000000-0000-4000-8000-000000000000",
  "owner": {},
  "name": "string",
  "created_at": 0,
  "last_opened_at": 0,
  "icon": "string",
  "color": "string",
  "pinned": false,
  "archived": false,
  "deleted_at": 0,
  "position": 0
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$WORKSPACE/reconcile-listing" \
  -H "Authorization: Bearer $KORU_TOKEN"

Live workspaces reachable from no active Profile, with the reason

GET/v1/workspaces/unassigned

each live workspace placed under nobody, under a deleted or absent Profile, or under a legacy holder, with its former Profile; moving one is the ordinary move, restoring its Profile is the batch restore

Authorization
Bearer token with manage_workspaces

Responses

StatusDescriptionBody
200See above.array of UnassignedWorkspace
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
[
  {
    "workspace": {
      "id": "00000000-0000-4000-8000-000000000000",
      "owner": {},
      "name": "string",
      "created_at": 0,
      "last_opened_at": 0,
      "icon": "string",
      "color": "string",
      "pinned": false,
      "archived": false,
      "deleted_at": 0,
      "position": 0
    },
    "former_profile": "00000000-0000-4000-8000-000000000000",
    "reason": "explicitly_unassigned"
  }
]
Example request
curl "http://127.0.0.1:$PORT/v1/workspaces/unassigned" \
  -H "Authorization: Bearer $KORU_TOKEN"

Every workspace

GET/v1/workspaces

an array of workspaces

Authorization
Bearer token with read_documents

Responses

StatusDescriptionBody
200See above.array of Workspace
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
[
  {
    "id": "00000000-0000-4000-8000-000000000000",
    "owner": {},
    "name": "string",
    "created_at": 0,
    "last_opened_at": 0,
    "icon": "string",
    "color": "string",
    "pinned": false,
    "archived": false,
    "deleted_at": 0,
    "position": 0
  }
]
Example request
curl "http://127.0.0.1:$PORT/v1/workspaces" \
  -H "Authorization: Bearer $KORU_TOKEN"

Create a workspace and its manifest

POST/v1/workspaces

the workspace, with its new immutable id

Authorization
Bearer token with manage_workspaces

Request body (required)

application/json NewWorkspace

FieldTypeDescription
ownerrequiredProfileId
namerequiredstring
Example request body
{
  "owner": "00000000-0000-4000-8000-000000000000",
  "name": "string"
}

Responses

StatusDescriptionBody
200See above.Workspace
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "id": "00000000-0000-4000-8000-000000000000",
  "owner": {},
  "name": "string",
  "created_at": 0,
  "last_opened_at": 0,
  "icon": "string",
  "color": "string",
  "pinned": false,
  "archived": false,
  "deleted_at": 0,
  "position": 0
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "owner": "00000000-0000-4000-8000-000000000000",
  "name": "string"
}'

One workspace

GET/v1/workspaces/{id}

a workspace

Authorization
Bearer token with read_documents

Parameters

NameInTypeDescription
idrequiredpathstring

Responses

StatusDescriptionBody
200See above.Workspace
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "id": "00000000-0000-4000-8000-000000000000",
  "owner": {},
  "name": "string",
  "created_at": 0,
  "last_opened_at": 0,
  "icon": "string",
  "color": "string",
  "pinned": false,
  "archived": false,
  "deleted_at": 0,
  "position": 0
}
Example request
curl "http://127.0.0.1:$PORT/v1/workspaces/$ID" \
  -H "Authorization: Bearer $KORU_TOKEN"

Rename a workspace, in the catalog and in its manifest

PATCH/v1/workspaces/{id}

the workspace, renamed

Authorization
Bearer token with manage_workspaces

Parameters

NameInTypeDescription
idrequiredpathstring

Request body (required)

application/json RenameWorkspace

FieldTypeDescription
namerequiredstring
Example request body
{
  "name": "string"
}

Responses

StatusDescriptionBody
200See above.Workspace
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "id": "00000000-0000-4000-8000-000000000000",
  "owner": {},
  "name": "string",
  "created_at": 0,
  "last_opened_at": 0,
  "icon": "string",
  "color": "string",
  "pinned": false,
  "archived": false,
  "deleted_at": 0,
  "position": 0
}
Example request
curl -X PATCH "http://127.0.0.1:$PORT/v1/workspaces/$ID" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "string"
}'

Soft-delete a workspace; its database and blobs remain recoverable

DELETE/v1/workspaces/{id}

the workspace with deleted_at set. Takes destroy_workspaces, which no ordinary token holds: removing a workspace closes its terminals, and the Gateway's lifecycle operation is what does that first

Authorization
Bearer token with destroy_workspaces

Parameters

NameInTypeDescription
idrequiredpathstring

Responses

StatusDescriptionBody
200See above.Workspace
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "id": "00000000-0000-4000-8000-000000000000",
  "owner": {},
  "name": "string",
  "created_at": 0,
  "last_opened_at": 0,
  "icon": "string",
  "color": "string",
  "pinned": false,
  "archived": false,
  "deleted_at": 0,
  "position": 0
}
Example request
curl -X DELETE "http://127.0.0.1:$PORT/v1/workspaces/$ID" \
  -H "Authorization: Bearer $KORU_TOKEN"

Archive a workspace, preparing its required replacement first

POST/v1/workspaces/{id}/archive-replacing

authority, listing, and replacement facts; HTTP 409 listing_pending retains committed authority

Authorization
Bearer token with destroy_workspaces

Parameters

NameInTypeDescription
idrequiredpathstring

Request body (required)

application/json RemoveWorkspaceReplacing

FieldTypeDescription
namerequiredstring

Localized name supplied by the caller for a required replacement.

forceboolean

Default false

removal_attemptRemovalAttemptId or null
Example request body
{
  "name": "string",
  "force": false,
  "removal_attempt": "00000000-0000-4000-8000-000000000000"
}

Responses

StatusDescriptionBody
200See above.RemovedWorkspace
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "workspace": {
    "id": "00000000-0000-4000-8000-000000000000",
    "owner": {},
    "name": "string",
    "created_at": 0,
    "last_opened_at": 0,
    "icon": "string",
    "color": "string",
    "pinned": false,
    "archived": false,
    "deleted_at": 0,
    "position": 0
  },
  "authority_committed": false,
  "listing": {
    "state": "applied"
  },
  "replacement": {
    "state": "created",
    "workspace": {
      "id": "00000000-0000-4000-8000-000000000000",
      "owner": {},
      "name": "string",
      "created_at": 0,
      "last_opened_at": 0,
      "icon": "string",
      "color": "string",
      "pinned": false,
      "archived": false,
      "deleted_at": 0,
      "position": 0
    }
  }
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/archive-replacing" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "string",
  "force": false,
  "removal_attempt": "00000000-0000-4000-8000-000000000000"
}'

Soft-delete a workspace, preparing its required replacement first

POST/v1/workspaces/{id}/delete-replacing

authority, listing, and replacement facts; HTTP 409 listing_pending retains committed authority

Authorization
Bearer token with destroy_workspaces

Parameters

NameInTypeDescription
idrequiredpathstring

Request body (required)

application/json RemoveWorkspaceReplacing

FieldTypeDescription
namerequiredstring

Localized name supplied by the caller for a required replacement.

forceboolean

Default false

removal_attemptRemovalAttemptId or null
Example request body
{
  "name": "string",
  "force": false,
  "removal_attempt": "00000000-0000-4000-8000-000000000000"
}

Responses

StatusDescriptionBody
200See above.RemovedWorkspace
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "workspace": {
    "id": "00000000-0000-4000-8000-000000000000",
    "owner": {},
    "name": "string",
    "created_at": 0,
    "last_opened_at": 0,
    "icon": "string",
    "color": "string",
    "pinned": false,
    "archived": false,
    "deleted_at": 0,
    "position": 0
  },
  "authority_committed": false,
  "listing": {
    "state": "applied"
  },
  "replacement": {
    "state": "created",
    "workspace": {
      "id": "00000000-0000-4000-8000-000000000000",
      "owner": {},
      "name": "string",
      "created_at": 0,
      "last_opened_at": 0,
      "icon": "string",
      "color": "string",
      "pinned": false,
      "archived": false,
      "deleted_at": 0,
      "position": 0
    }
  }
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/delete-replacing" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "string",
  "force": false,
  "removal_attempt": "00000000-0000-4000-8000-000000000000"
}'

Move a workspace and atomically publish its required source replacement

POST/v1/workspaces/{id}/move-replacing

the moved workspace, previous owner, and replacement facts

Authorization
Bearer token with migrate_workspaces

Parameters

NameInTypeDescription
idrequiredpathstring

Request body (required)

application/json MoveWorkspaceReplacing

FieldTypeDescription
ownerrequiredProfileId
namerequiredstring
Example request body
{
  "owner": "00000000-0000-4000-8000-000000000000",
  "name": "string"
}

Responses

StatusDescriptionBody
200See above.MovedWorkspace
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "workspace": {
    "id": "00000000-0000-4000-8000-000000000000",
    "owner": {},
    "name": "string",
    "created_at": 0,
    "last_opened_at": 0,
    "icon": "string",
    "color": "string",
    "pinned": false,
    "archived": false,
    "deleted_at": 0,
    "position": 0
  },
  "from": "00000000-0000-4000-8000-000000000000",
  "replacement": {
    "state": "created",
    "workspace": {
      "id": "00000000-0000-4000-8000-000000000000",
      "owner": {},
      "name": "string",
      "created_at": 0,
      "last_opened_at": 0,
      "icon": "string",
      "color": "string",
      "pinned": false,
      "archived": false,
      "deleted_at": 0,
      "position": 0
    }
  }
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/move-replacing" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "owner": "00000000-0000-4000-8000-000000000000",
  "name": "string"
}'

Restore a soft-deleted workspace

POST/v1/workspaces/{id}/restore

the workspace with deleted_at cleared. The same scope the delete takes: whoever decides what the trash contains decides both ways

Authorization
Bearer token with destroy_workspaces

Parameters

NameInTypeDescription
idrequiredpathstring

Responses

StatusDescriptionBody
200See above.Workspace
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "id": "00000000-0000-4000-8000-000000000000",
  "owner": {},
  "name": "string",
  "created_at": 0,
  "last_opened_at": 0,
  "icon": "string",
  "color": "string",
  "pinned": false,
  "archived": false,
  "deleted_at": 0,
  "position": 0
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/restore" \
  -H "Authorization: Bearer $KORU_TOKEN"

Give a workspace to another profile

POST/v1/workspaces/{id}/move

the workspace with its new owner and its position at the end of that profile's order. Its id, its documents and its history are untouched. Takes migrate_workspaces, which no ordinary token holds: a move closes every terminal the workspace owns, and the Gateway's migration operation is what does that first

Authorization
Bearer token with migrate_workspaces

Parameters

NameInTypeDescription
idrequiredpathstring

Request body (required)

application/json MoveWorkspace

FieldTypeDescription
ownerrequiredProfileId
Example request body
{
  "owner": "00000000-0000-4000-8000-000000000000"
}

Responses

StatusDescriptionBody
200See above.Workspace
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "id": "00000000-0000-4000-8000-000000000000",
  "owner": {},
  "name": "string",
  "created_at": 0,
  "last_opened_at": 0,
  "icon": "string",
  "color": "string",
  "pinned": false,
  "archived": false,
  "deleted_at": 0,
  "position": 0
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/move" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "owner": "00000000-0000-4000-8000-000000000000"
}'

How a workspace appears in a switcher: icon, colour, pinned, archived

PATCH/v1/workspaces/{id}/appearance

the workspace. All four are sent every time - there is no absent-versus-null. Changing archived additionally takes destroy_workspaces: archiving closes the workspace's terminals and hides its projects, and it only travels here because these four fields are edited together

Authorization
Bearer token with manage_workspaces

Parameters

NameInTypeDescription
idrequiredpathstring

Request body (required)

application/json WorkspaceAppearance

FieldTypeDescription
iconstring or null

Default null

colorstring or null

Default null

pinnedboolean

Default false

archivedboolean

Default false

Example request body
{
  "icon": "string",
  "color": "string",
  "pinned": false,
  "archived": false
}

Responses

StatusDescriptionBody
200See above.Workspace
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "id": "00000000-0000-4000-8000-000000000000",
  "owner": {},
  "name": "string",
  "created_at": 0,
  "last_opened_at": 0,
  "icon": "string",
  "color": "string",
  "pinned": false,
  "archived": false,
  "deleted_at": 0,
  "position": 0
}
Example request
curl -X PATCH "http://127.0.0.1:$PORT/v1/workspaces/$ID/appearance" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "icon": "string",
  "color": "string",
  "pinned": false,
  "archived": false
}'

The workspace's documents, without opening any of them

GET/v1/workspaces/{id}/documents

an array of document entries in tree order

Authorization
Bearer token with read_documents

Parameters

NameInTypeDescription
idrequiredpathstring

Responses

StatusDescriptionBody
200See above.array of DocumentEntry
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
[
  {
    "id": "00000000-0000-4000-8000-000000000000",
    "parent": "00000000-0000-4000-8000-000000000000",
    "ordinal": 0,
    "title": "string",
    "kind": "string",
    "mime": "string",
    "note": "00000000-0000-4000-8000-000000000000",
    "trashed_at": 0,
    "trashed_by": "string"
  }
]
Example request
curl "http://127.0.0.1:$PORT/v1/workspaces/$ID/documents" \
  -H "Authorization: Bearer $KORU_TOKEN"

Register a new document in the workspace

POST/v1/workspaces/{id}/documents

the new document id

Authorization
Bearer token with edit_documents

Parameters

NameInTypeDescription
idrequiredpathstring

Request body (required)

application/json NewDocument

FieldTypeDescription
idDocumentId or null

Optional client-minted identity. Importers use this so snapshots can contain routed references to other entries in the same batch.

Default null

titlerequiredstring
kindrequiredstring

The application's own kind - page, task-list. A stable token, and the store never interprets it.

mimestring or null

MIME type for file entries. Optional for ordinary documents and older clients.

parentDocumentId or null

Absent means the workspace root.

Default null

indexinteger (uint, >= 0) or null

Where among parent's children it goes. Absent means the end, which is what every caller with no opinion sends and what an older client's body decodes as.

The same coordinate MoveDocument uses - an index among siblings, counting the trashed ones, because that is what the manifest tree holds. Past the end is clamped rather than refused: a new page below the last one is an ordinary request, not an arithmetic mistake.

Default null

trashed_atTimestamp or null

Create the manifest row already in Trash at this source timestamp. Absent preserves the pre-capability behavior for older clients.

snapshotarray of integer (uint8, >= 0, <= 255) or null

Optional complete initial state. Used by importers so registering the manifest row and committing content is one Store operation rather than a briefly visible empty page followed by a client update.

Example request body
{
  "id": "00000000-0000-4000-8000-000000000000",
  "title": "string",
  "kind": "string",
  "mime": "string",
  "parent": "00000000-0000-4000-8000-000000000000",
  "index": 0,
  "trashed_at": 0,
  "snapshot": [
    0
  ]
}

Responses

StatusDescriptionBody
200See above.Created
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "id": "00000000-0000-4000-8000-000000000000"
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/documents" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "id": "00000000-0000-4000-8000-000000000000",
  "title": "string",
  "kind": "string",
  "mime": "string",
  "parent": "00000000-0000-4000-8000-000000000000",
  "index": 0,
  "trashed_at": 0,
  "snapshot": [
    0
  ]
}'

The one note a file carries, creating it when it has none

POST/v1/workspaces/{id}/documents/{document}/note

the note's document id - the existing one when the file already has a note, so the call is the answer to both Add note and Open note - and whether this call created it

Authorization
Bearer token with edit_documents

Parameters

NameInTypeDescription
idrequiredpathstring
documentrequiredpathstring

Responses

StatusDescriptionBody
200See above.EnsuredFileNote
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "id": {},
  "created": false
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/documents/$DOCUMENT/note" \
  -H "Authorization: Bearer $KORU_TOKEN"

Move a document and its explore subtree into another workspace

POST/v1/workspaces/{id}/transfer

the document ids that moved, root first. Ids are preserved.

Authorization
Bearer token with edit_documents

Parameters

NameInTypeDescription
idrequiredpathstring

Request body (required)

application/json TransferDocuments

FieldTypeDescription
targetrequiredWorkspaceId
rootrequiredDocumentId

DocumentId - a client-minted v4 UUID, hyphenated and lowercase. Immutable, and never derived from a name or a path.

parentDocumentId or null

Default null

indexinteger (uint, >= 0)

Entry index among siblings under parent. Past the end means append.

Default 0

Example request body
{
  "target": "00000000-0000-4000-8000-000000000000",
  "root": "00000000-0000-4000-8000-000000000000",
  "parent": "00000000-0000-4000-8000-000000000000",
  "index": 0
}

Responses

StatusDescriptionBody
200See above.Transferred
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "ids": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/transfer" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "target": "00000000-0000-4000-8000-000000000000",
  "root": "00000000-0000-4000-8000-000000000000",
  "parent": "00000000-0000-4000-8000-000000000000",
  "index": 0
}'

The document id a client joins to see the workspace's contents

GET/v1/workspaces/{id}/manifest

a document id

Authorization
Bearer token with read_documents

Parameters

NameInTypeDescription
idrequiredpathstring

Responses

StatusDescriptionBody
200See above.ManifestRef
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "document": "00000000-0000-4000-8000-000000000000"
}
Example request
curl "http://127.0.0.1:$PORT/v1/workspaces/$ID/manifest" \
  -H "Authorization: Bearer $KORU_TOKEN"

A document's whole state, for a client that has none

POST/v1/workspaces/{id}/snapshot/{document}

application/octet-stream - a Loro snapshot, ready to import

Authorization
Bearer token with read_documents

Parameters

NameInTypeDescription
idrequiredpathstring
documentrequiredpathstring

Responses

StatusDescriptionBody
200See above.
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/snapshot/$DOCUMENT" \
  -H "Authorization: Bearer $KORU_TOKEN"

A document's durable history markers, newest first

GET/v1/workspaces/{id}/documents/{document}/history

a cursor-paginated history page

Authorization
Bearer token with read_documents

Parameters

NameInTypeDescription
idrequiredpathstring
documentrequiredpathstring

Responses

StatusDescriptionBody
200See above.HistoryPage
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "entries": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "document": "00000000-0000-4000-8000-000000000000",
      "version": "00000000-0000-4000-8000-000000000000",
      "reason": "string",
      "recorded_at": 0,
      "label": "string",
      "message": "string",
      "boundary": "string",
      "pinned": false
    }
  ],
  "next_before_at": 0,
  "next_before_id": "00000000-0000-4000-8000-000000000000",
  "stale": false
}
Example request
curl "http://127.0.0.1:$PORT/v1/workspaces/$ID/documents/$DOCUMENT/history" \
  -H "Authorization: Bearer $KORU_TOKEN"

Capture a durable document version and append a history marker

POST/v1/workspaces/{id}/documents/{document}/history

the new history marker

Authorization
Bearer token with edit_documents

Parameters

NameInTypeDescription
idrequiredpathstring
documentrequiredpathstring

Request body (required)

application/json CaptureHistory

FieldTypeDescription
reasonrequiredstring

One of the HistoryReason tokens. Public captures normally use manual; MCP and restore use their paired boundary reasons.

frontiersarray of integer (uint8, >= 0, <= 255) or null

Default null

labelstring or null

Default null

messagestring or null

Default null

boundarystring or null

Default null

Example request body
{
  "reason": "string",
  "frontiers": [
    0
  ],
  "label": "string",
  "message": "string",
  "boundary": "string"
}

Responses

StatusDescriptionBody
200See above.HistoryEntry
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "id": "00000000-0000-4000-8000-000000000000",
  "document": "00000000-0000-4000-8000-000000000000",
  "version": "00000000-0000-4000-8000-000000000000",
  "reason": "string",
  "recorded_at": 0,
  "label": "string",
  "message": "string",
  "boundary": "string",
  "pinned": false
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/documents/$DOCUMENT/history" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "string",
  "frontiers": [
    0
  ],
  "label": "string",
  "message": "string",
  "boundary": "string"
}'

Rename a history marker; its immutable state is untouched

POST/v1/workspaces/{id}/history/{entry}/label

the marker with its new label

Authorization
Bearer token with edit_documents

Parameters

NameInTypeDescription
idrequiredpathstring
entryrequiredpathstring

Request body (required)

application/json RelabelHistory

FieldTypeDescription
labelstring or null

Default null

Example request body
{
  "label": "string"
}

Responses

StatusDescriptionBody
200See above.HistoryEntry
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "id": "00000000-0000-4000-8000-000000000000",
  "document": "00000000-0000-4000-8000-000000000000",
  "version": "00000000-0000-4000-8000-000000000000",
  "reason": "string",
  "recorded_at": 0,
  "label": "string",
  "message": "string",
  "boundary": "string",
  "pinned": false
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/history/$ENTRY/label" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "label": "string"
}'

The checksum-verified CRDT snapshot named by a history marker

POST/v1/workspaces/{id}/history/{entry}/snapshot

application/octet-stream - an immutable Loro snapshot

Authorization
Bearer token with read_documents

Parameters

NameInTypeDescription
idrequiredpathstring
entryrequiredpathstring

Responses

StatusDescriptionBody
200See above.
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/history/$ENTRY/snapshot" \
  -H "Authorization: Bearer $KORU_TOKEN"

The checksum-verified CRDT snapshot a document version id names

GET/v1/workspaces/{id}/versions/{version}/snapshot

application/octet-stream - an immutable Loro snapshot, with its document id, checksum and capture time on X-Koru-Document-Id, X-Koru-Checksum and X-Koru-Captured-At; document_version_not_found when the workspace holds no such version

Authorization
Bearer token with read_documents

Parameters

NameInTypeDescription
idrequiredpathstring
versionrequiredpathstring

Responses

StatusDescriptionBody
200See above.
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example request
curl "http://127.0.0.1:$PORT/v1/workspaces/$ID/versions/$VERSION/snapshot" \
  -H "Authorization: Bearer $KORU_TOKEN"

Every blob this workspace holds

GET/v1/workspaces/{id}/blobs

an array of blobs - hash, size, mime, filename, created_at

Authorization
Bearer token with read_documents

Parameters

NameInTypeDescription
idrequiredpathstring

Responses

StatusDescriptionBody
200See above.array of Blob
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
[
  {
    "hash": "string",
    "size": 0,
    "mime": "string",
    "filename": "string",
    "created_at": 0,
    "attributes": {
      "key": {
        "type": "int",
        "value": 0
      }
    }
  }
]
Example request
curl "http://127.0.0.1:$PORT/v1/workspaces/$ID/blobs" \
  -H "Authorization: Bearer $KORU_TOKEN"

Store bytes and get back the address they are filed under

PUT/v1/workspaces/{id}/blobs

a blob. The caller does not choose the hash: the store hashes what it receives, so storing the same bytes twice makes no second copy. The filename travels percent-encoded in X-Koru-Filename, because a filename is arbitrary Unicode and a header value is not

Authorization
Bearer token with edit_documents

Parameters

NameInTypeDescription
idrequiredpathstring

Responses

StatusDescriptionBody
200See above.Blob
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "hash": "string",
  "size": 0,
  "mime": "string",
  "filename": "string",
  "created_at": 0,
  "attributes": {
    "key": {
      "type": "int",
      "value": 0
    }
  }
}
Example request
curl -X PUT "http://127.0.0.1:$PORT/v1/workspaces/$ID/blobs" \
  -H "Authorization: Bearer $KORU_TOKEN"

The local or on-demand hydrated bytes behind a hash

GET/v1/workspaces/{id}/blobs/{hash}

application/octet-stream, fetched from an authorized connected peer when absent locally, then verified and persisted against the address

Authorization
Bearer token with read_documents

Parameters

NameInTypeDescription
idrequiredpathstring
hashrequiredpathstring

Responses

StatusDescriptionBody
200See above.
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example request
curl "http://127.0.0.1:$PORT/v1/workspaces/$ID/blobs/$HASH" \
  -H "Authorization: Bearer $KORU_TOKEN"

Associate another workspace of the same Profile with one blob

POST/v1/workspaces/{id}/blobs/{hash}

the target workspace's durable blob record. No bytes are copied: one object serves every associated workspace, and the record is written before the caller publishes a CRDT reference to it

Authorization
Bearer token with edit_documents

Parameters

NameInTypeDescription
idrequiredpathstring
hashrequiredpathstring

Request body (required)

application/json AssociateBlob

FieldTypeDescription
targetrequiredWorkspaceId
Example request body
{
  "target": "00000000-0000-4000-8000-000000000000"
}

Responses

StatusDescriptionBody
200See above.Blob
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example 200 response
{
  "hash": "string",
  "size": 0,
  "mime": "string",
  "filename": "string",
  "created_at": 0,
  "attributes": {
    "key": {
      "type": "int",
      "value": 0
    }
  }
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/blobs/$HASH" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "target": "00000000-0000-4000-8000-000000000000"
}'

A whole workspace as one .koru bundle

POST/v1/workspaces/{id}/export

application/octet-stream - manifest, a snapshot per document, then the blobs. A POST because it is work; it changes nothing

Authorization
Bearer token with read_documents

Parameters

NameInTypeDescription
idrequiredpathstring

Responses

StatusDescriptionBody
200See above.
401

no bearer token, or one that is not recognised

Failure
403

the credential does not hold the required scope

Failure
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/export" \
  -H "Authorization: Bearer $KORU_TOKEN"

Compute what an action would close, and issue the token that proves it was shown

POST/v1/workspaces/{id}/lifecycle/confirmation

The consequence - the terminals this action would close, named - and a single-use token bound to this workspace and this action. The caller presents the consequence, and sends the token back in the lifecycle request if the person accepts. The same scope as the operation itself: computing what would be destroyed is not a smaller permission than destroying it.

Authorization
Bearer token with workspace.destroy
Operation id
workspace_lifecycle_confirmation

Parameters

NameInTypeDescription
idrequiredpathstring (uuid)

Request body (required)

application/json WorkspaceConfirmationRequest

FieldTypeDescription
actionrequiredWorkspaceLifecycleAction

What POST /v1/workspaces/{id}/lifecycle is being asked to do.

Three verbs, and no permanent delete. There is no purge route anywhere in Koru - the retention sweep is the only thing that removes bytes - so naming one here would be describing an operation that does not exist.

Example request body
{
  "action": "archive"
}

Responses

StatusDescriptionBody
200See above.WorkspaceConfirmation
400

The body is not a confirmation request

401

The gateway bearer is absent or invalid

403

The session does not hold workspace.destroy

404

There is no such workspace, deleted ones included

503

The catalog could not be read, so no consequence could be computed

Example 200 response
{
  "confirmation": "string",
  "workspace": "string",
  "action": "archive",
  "sessions": [
    {
      "id": "string",
      "display_name": "string",
      "working_directory": "string"
    }
  ],
  "expires_at": 0
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/lifecycle/confirmation" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "action": "archive"
}'

Archive, soft-delete or restore a workspace, closing its terminals first

POST/v1/workspaces/{id}/lifecycle

What the operation did: the sessions it closed, whether they all closed, and whether the catalog mutation committed. A refusal carrying workspace_lifecycle_incomplete with sessions_closed: true and mutation_applied: false is a partial failure - the terminals are gone and the catalog is unchanged - and the same request again is the retry, which needs a fresh confirmation because the first one was spent. Asking for a state the workspace is already in succeeds and closes nothing.

Authorization
Bearer token with workspace.destroy
Operation id
workspace_lifecycle

Parameters

NameInTypeDescription
idrequiredpathstring (uuid)

Request body (required)

application/json WorkspaceLifecycleRequest

FieldTypeDescription
actionrequiredWorkspaceLifecycleAction

What POST /v1/workspaces/{id}/lifecycle is being asked to do.

Three verbs, and no permanent delete. There is no purge route anywhere in Koru - the retention sweep is the only thing that removes bytes - so naming one here would be describing an operation that does not exist.

confirmationrequiredstring
replacement_namestring

Localized default for a required replacement. Older callers use the protocol's English fallback; this is not a second creation operation.

Default "My Workspace"

removal_attemptRemovalAttemptId or null
Example request body
{
  "action": "archive",
  "confirmation": "string",
  "replacement_name": "My Workspace",
  "removal_attempt": "00000000-0000-4000-8000-000000000000"
}

Responses

StatusDescriptionBody
200See above.WorkspaceLifecycleOutcome
400

The body is not a lifecycle request, or the confirmation is not one this gateway issued for this workspace and this action - expired, spent, or never minted here

401

The gateway bearer is absent or invalid

403

The session does not hold workspace.destroy

404

There is no such workspace, deleted ones included - or the mutation refused because another window removed it first, which is a partial failure carrying its outcome

409

The mutation refused: the body carries the outcome, so a partial failure is told apart from nothing having happened

500

A partial failure: the body says which steps completed and whether a retry is safe

503

The session-host bridge is unavailable, so no terminal could be closed

Example 200 response
{
  "workspace": "string",
  "action": "archive",
  "terminated_sessions": [
    "string"
  ],
  "sessions_closed": false,
  "mutation_applied": false,
  "authority_committed": false,
  "listing": {
    "state": "applied"
  },
  "replacement": {
    "state": "created",
    "workspace": {
      "id": "00000000-0000-4000-8000-000000000000",
      "owner": {},
      "name": "string",
      "created_at": 0,
      "last_opened_at": 0,
      "icon": "string",
      "color": "string",
      "pinned": false,
      "archived": false,
      "deleted_at": 0,
      "position": 0
    }
  }
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/lifecycle" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "action": "archive",
  "confirmation": "string",
  "replacement_name": "My Workspace",
  "removal_attempt": "00000000-0000-4000-8000-000000000000"
}'

Compute what moving a workspace to a profile would close, and issue the token that proves it was shown

POST/v1/workspaces/{id}/migration/confirmation

Both ends of the move - the profile that owns the workspace and the one it would go to - the terminals the move would close, named, and a single-use token bound to this workspace and this destination. A token issued for one destination does not spend against another: a person accepted a move to a particular profile, not a move. The same scope as the operation itself.

Authorization
Bearer token with workspace.migrate
Operation id
workspace_migration_confirmation

Parameters

NameInTypeDescription
idrequiredpathstring (uuid)

Request body (required)

application/json WorkspaceMigrationConfirmationRequest

FieldTypeDescription
torequiredstring

The profile the workspace would move to.

Example request body
{
  "to": "string"
}

Responses

StatusDescriptionBody
200See above.WorkspaceMigration
400

The body is not a migration confirmation request, or the destination is not a profile id

401

The gateway bearer is absent or invalid

403

The session does not hold workspace.migrate

404

There is no such workspace, or no such destination profile - a consequence is never computed for a move that could not happen

503

The catalog could not be read, so no consequence could be computed

Example 200 response
{
  "confirmation": "string",
  "workspace": "string",
  "from": "string",
  "to": "string",
  "sessions": [
    {
      "id": "string",
      "display_name": "string",
      "working_directory": "string"
    }
  ],
  "expires_at": 0
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/migration/confirmation" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "string"
}'

Give a workspace to another profile, closing its terminals first

POST/v1/workspaces/{id}/migration

What the operation did: both ends, the sessions it closed, and whether the reassignment committed. A refusal carrying workspace_migration_incomplete with sessions_closed: true and mutation_applied: false is a partial failure - the terminals are gone and the workspace has not moved - and the same request again is the retry, which needs a fresh confirmation because the first one was spent. Moving a workspace to the profile that already owns it succeeds and closes nothing.

Authorization
Bearer token with workspace.migrate
Operation id
workspace_migration

Parameters

NameInTypeDescription
idrequiredpathstring (uuid)

Request body (required)

application/json WorkspaceMigrationRequest

FieldTypeDescription
torequiredstring
confirmationrequiredstring
replacement_namestring

Default "My Workspace"

Example request body
{
  "to": "string",
  "confirmation": "string",
  "replacement_name": "My Workspace"
}

Responses

StatusDescriptionBody
200See above.WorkspaceMigrationOutcome
400

The body is not a migration request, or the confirmation is not one this gateway issued for this workspace and this destination - expired, spent, or never minted here

401

The gateway bearer is absent or invalid

403

The session does not hold workspace.migrate

404

There is no such workspace, or the reassignment refused because the destination profile is gone - which after the terminals closed is a partial failure carrying its outcome

409

The reassignment refused: the body carries the outcome, so a partial failure is told apart from nothing having happened

500

A partial failure: the terminals are closed, the workspace has not moved, and the body says so

503

The session-host bridge is unavailable, so no terminal could be closed

Example 200 response
{
  "workspace": "string",
  "from": "string",
  "to": "string",
  "terminated_sessions": [
    "string"
  ],
  "sessions_closed": false,
  "mutation_applied": false,
  "replacement": {
    "state": "created",
    "workspace": {
      "id": "00000000-0000-4000-8000-000000000000",
      "owner": {},
      "name": "string",
      "created_at": 0,
      "last_opened_at": 0,
      "icon": "string",
      "color": "string",
      "pinned": false,
      "archived": false,
      "deleted_at": 0,
      "position": 0
    }
  },
  "projects_rescoped": [
    "string"
  ]
}
Example request
curl -X POST "http://127.0.0.1:$PORT/v1/workspaces/$ID/migration" \
  -H "Authorization: Bearer $KORU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "string",
  "confirmation": "string",
  "replacement_name": "My Workspace"
}'

Generated from contract version 1. Last updated .

Privacy choices

Choose which optional tools Koru may use on this site. Your choice is saved in a cookie in this browser for 12 months. Privacy policy

Privacy choices

Keep you signed in, protect forms and remember your language and this choice.

Always on

PostHog and Google Analytics 4 measure page views and actions such as downloads, using cookies or local storage.

PostHog records how pages are used, with form inputs masked and private pages excluded. Needs Analytics.

Ahrefs Web Analytics measures visits and where they come from, and Google Ads measures whether our ads led to a visit or download.