Skip to main content

Groups API

Groups are hierarchical containers that own resources, notes, and other groups. Custom relationships between groups are defined through the relations system.

List Groups

Retrieve a paginated list of groups with optional filtering.

GET /v1/groups

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
NamestringFilter by name (partial match)
DescriptionstringFilter by description (partial match)
Tagsinteger[]Filter by tag IDs
Groupsinteger[]Filter by related Groups or parent (checks both group_related_groups and owner_id). Multiple values are combined with AND: only groups linked to (or owned by) every listed group are returned
Notesinteger[]Filter by associated note IDs
Resourcesinteger[]Filter by associated resource IDs
Categoriesinteger[]Filter by category IDs
CategoryIdintegerFilter by single category ID
OwnerIdintegerFilter by owner group ID
Idsinteger[]Filter by specific group IDs
URLstringFilter by URL field
CreatedBeforestringFilter by creation date (ISO 8601)
CreatedAfterstringFilter by creation date (ISO 8601)
UpdatedBeforestringFilter by last-updated date (ISO 8601)
UpdatedAfterstringFilter by last-updated date (ISO 8601)
RelationTypeIdintegerFilter by relation-type eligibility. Returns groups whose category matches the relation type's from category (or to category when RelationSide is non-zero). This filters by category eligibility, not by existing relation instances
RelationSideintegerWhich side of the relation type's category constraint to match (0=from, non-zero=to)
MetaQuerystring[]Filter by metadata conditions (supports parent.key and child.key prefixes)
MRQLstringFilter with an MRQL expression (type group is implied)
SearchParentsForNamebooleanSearch parent groups for name match
SearchChildrenForNamebooleanSearch child groups for name match
SearchParentsForTagsbooleanInclude parent groups when filtering by tags
SearchChildrenForTagsbooleanInclude child groups when filtering by tags
SortBystring[]Sort order

Example

# List all groups
curl http://localhost:8181/v1/groups

# Filter by category
curl "http://localhost:8181/v1/groups?CategoryId=1"

# Filter by tags
curl "http://localhost:8181/v1/groups?Tags=1&Tags=2"

# Find groups by relation
curl "http://localhost:8181/v1/groups?RelationTypeId=1&RelationSide=1"

Response

[
{
"ID": 1,
"Name": "Project Alpha",
"Description": "Main project group",
"URL": "https://example.com/project-alpha",
"CategoryId": 1,
"OwnerId": null,
"Meta": {"status": "active"},
"CreatedAt": "2024-01-15T10:00:00Z",
"UpdatedAt": "2024-01-15T10:00:00Z",
"Tags": [...],
"Category": {...}
}
]

The list endpoint preloads only Tags and Category. Related-group associations serialize under the RelatedGroups key (not Groups) and are null here because they are not preloaded; fetch a single group with GET /v1/group?id={id} to load them.

Get Single Group

Retrieve details for a specific group.

GET /v1/group?id={id}

Preloaded associations are truncated: owned and related resources to 5 rows, and the other collection associations to 50. To enumerate a group's contents, use GET /v1/resources?OwnerId={id} (or ?Groups={id}) rather than this payload.

Example

curl http://localhost:8181/v1/group?id=123

Get Group Parents

Get all parent groups of a specific group.

GET /v1/group/parents?id={id}

Example

curl http://localhost:8181/v1/group/parents?id=123

Response

The chain is ordered from the most distant ancestor down to the queried group, which is always included as the final element.

[
{"ID": 2, "Name": "Grandparent Group", ...},
{"ID": 1, "Name": "Parent Group", ...},
{"ID": 123, "Name": "The Queried Group", ...}
]

Get Group Tree Children

Get child groups for a tree view with counts.

GET /v1/group/tree/children?parentId={parentId}

Query Parameters

ParameterTypeDescription
parentIdintegerParent group ID. Defaults to 0 (root groups) when omitted
limitintegerMax children to return (default: 50, max: 100)

Example

# Get root-level groups
curl "http://localhost:8181/v1/group/tree/children?parentId=0"

# Get children of group 10
curl "http://localhost:8181/v1/group/tree/children?parentId=10&limit=25"

Response

[
{
"id": 10,
"name": "Sub-Group",
"categoryName": "Project",
"childCount": 3,
"ownerId": 1
}
]

Create or Update Group

Create a new group or update an existing one.

POST /v1/group

Parameters

ParameterTypeDescription
IDintegerGroup ID (include to update, omit to create)
NamestringRequired for create. Group name
DescriptionstringDescription text
CategoryIdintegerCategory ID
OwnerIdintegerParent/owner group ID
Groupsinteger[]Associated group IDs
Tagsinteger[]Tag IDs
MetastringJSON metadata object
URLstringAssociated URL

Example - Create

curl -X POST http://localhost:8181/v1/group \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"Name": "New Project",
"Description": "A new project group",
"CategoryId": 1,
"Tags": [1, 2],
"Meta": "{\"status\": \"planning\"}"
}'

Example - Update

curl -X POST http://localhost:8181/v1/group \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"ID": 123,
"Name": "Updated Project Name",
"Description": "Updated description"
}'

Delete Group

Delete a group. Deleting a group that is assigned as a user's scope group returns 409 Conflict with group is assigned as a user scope; reassign or clear that user's scope first.

POST /v1/group/delete?Id={id}

Example

curl -X POST "http://localhost:8181/v1/group/delete?Id=123" \
-H "Accept: application/json"

Clone Group

Create a copy of an existing group with all its metadata.

POST /v1/group/clone

Parameters

ParameterTypeDescription
IDintegerRequired. Group ID to clone

Example

curl -X POST http://localhost:8181/v1/group/clone \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"ID": 123}'

Response

Returns the newly created group:

{
"ID": 456,
"Name": "New Project",
...
}

The clone has identical name, description, meta, URL, owner, Category, and copies all related entity associations (Resources, Notes, Groups, Tags). It also duplicates the group's relation instances in both directions (outgoing and incoming), pointing them at the new clone.

Get Group Meta Keys

Get all unique metadata keys used across groups.

GET /v1/groups/meta/keys

Example

curl http://localhost:8181/v1/groups/meta/keys

Response

Each key is returned as an object with a key field:

[{"key": "status"}, {"key": "priority"}, {"key": "deadline"}, {"key": "budget"}]

Bulk Operations

Bulk Add Tags

Add tags to multiple groups at once.

POST /v1/groups/addTags

Parameters

ParameterTypeDescription
IDinteger[]Group IDs to modify
EditedIdinteger[]Tag IDs to add

Example

curl -X POST http://localhost:8181/v1/groups/addTags \
-H "Content-Type: application/json" \
-d '{
"ID": [1, 2, 3],
"EditedId": [10, 11]
}'

Bulk Remove Tags

Remove tags from multiple groups.

POST /v1/groups/removeTags

Bulk Add Metadata

Add or merge metadata to multiple groups.

POST /v1/groups/addMeta

Parameters

ParameterTypeDescription
IDinteger[]Group IDs to modify
MetastringJSON metadata to merge

Mass Edit

Apply several edits — tags, related groups, related notes, related resources, owner (the parent group) and metadata — to many groups in one transaction. Target an explicit ID list, or set Target=filter with the list page's raw query string and an ExpectedCount the server re-checks with 409 on mismatch. Ops: TagsOp/TagIds, RelatedGroupsOp/RelatedGroupIds, NotesOp/NoteIds, ResourcesOp/ResourceIds, OwnerOp/OwnerId, and MetaOp/Meta/MetaKeys; DryRun resolves the set and echoes the parsed ops, committing nothing (the far-endpoint and cycle checks run on the real submit).

A group's owner is its parent, so OwnerOp=set re-parents: self-ownership and re-parents that would create an ownership cycle are refused (409) rather than repaired, and nothing else about the tree is touched.

curl -X POST http://localhost:8181/v1/groups/massEdit \
-H "Content-Type: application/json" \
-d '{"ID": [4, 5], "OwnerOp": "set", "OwnerId": 2}'

The verbs, the all-or-nothing transaction, the typed errors and the response shape are the same as the resources endpoint.

Bulk Delete

Delete multiple groups. If any of them is assigned as a user's scope group, the request returns 409 Conflict with group is assigned as a user scope; reassign or clear that user's scope first.

POST /v1/groups/delete

Parameters

ParameterTypeDescription
IDinteger[]Group IDs to delete

Merge Groups

Merge multiple groups into one, combining their relationships.

POST /v1/groups/merge

Parameters

ParameterTypeDescription
WinnerintegerGroup ID to keep
Losersinteger[]Group IDs to merge and delete

Example

curl -X POST http://localhost:8181/v1/groups/merge \
-H "Content-Type: application/json" \
-d '{
"Winner": 1,
"Losers": [2, 3]
}'

Inline Editing

Both endpoints take the new value in the request body: send Name for editName and Description for editDescription (JSON or form-encoded). An empty Name is rejected with 400.

Edit Name

POST /v1/group/editName?id={id}

Body field: Name (required, non-empty).

Edit Description

POST /v1/group/editDescription?id={id}

Body field: Description.

Edit Meta

Edit a single metadata field at a dot-notation path using deep merge.

POST /v1/group/editMeta?id={id}

Query Parameters

ParameterDescription
idRequired. Group ID

Form Fields

FieldDescription
pathDot-notation path into the Meta field (e.g., cooking.time, address.city)
valueJSON-encoded value to set at that path

Response

{"ok": true, "id": 123, "meta": {"cooking": {"time": 30, "difficulty": "easy"}}}

Behavior

  • Creates intermediate objects as needed (e.g., setting a.b.c on empty meta creates the full chain)
  • Preserves sibling fields at every nesting level
  • If the path does not exist, it is created
  • If an intermediate key holds a scalar, it is overwritten to become an object
  • Returns the full updated meta in the response

Errors

  • 400: Missing ID, missing path, missing value, invalid JSON, malformed path (empty segments), or corrupt existing meta
  • 404: Group not found

Group Relations API

Relations define typed, directional connections between groups (e.g., "Person works at Company").

note

With -auth enabled, every relation and relation-type write requires the admin or editor role and returns 403 Forbidden otherwise. Listing relation types is open to any authenticated role.

List Relation Types

Get all available relation types.

GET /v1/relationTypes

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
NamestringFilter by name
DescriptionstringFilter by description
FromCategoryintegerFilter by source category ID
ToCategoryintegerFilter by target category ID
ForFromGroupintegerFilter types valid for this group's category (source)
ForToGroupintegerFilter types valid for this group's category (target)

Example

# List all relation types
curl http://localhost:8181/v1/relationTypes

# Filter by category constraints
curl "http://localhost:8181/v1/relationTypes?FromCategory=1&ToCategory=2"

Response

[
{
"ID": 1,
"CreatedAt": "2024-01-15T10:00:00Z",
"UpdatedAt": "2024-01-15T10:00:00Z",
"Name": "works at",
"Description": "Employment relationship",
"FromCategory": null,
"FromCategoryId": 1,
"ToCategory": null,
"ToCategoryId": 2,
"BackRelation": null,
"BackRelationId": 3
}
]

FromCategory and ToCategory are associations that this endpoint does not preload, so they serialize as null; the IDs are in FromCategoryId and ToCategoryId. The reverse relation type is a separate row, reached through BackRelationId.

Create Relation Type

Create a new relation type.

POST /v1/relationType

Parameters

ParameterTypeDescription
NamestringRequired. Relation name (e.g., "works at")
ReverseNamestringCreates (or links to) a second relation type with the categories swapped, joined to this one through BackRelationId. Setting it equal to Name requires FromCategory and ToCategory to be the same category
DescriptionstringDescription
FromCategoryintegerRequired. Source group category ID
ToCategoryintegerRequired. Target group category ID

Example

curl -X POST http://localhost:8181/v1/relationType \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"Name": "works at",
"ReverseName": "employs",
"FromCategory": 1,
"ToCategory": 2
}'

Edit Relation Type

Update an existing relation type.

POST /v1/relationType/edit

Parameters

Same as create, but include the Id field to identify which relation type to update.

Delete Relation Type

Delete a relation type.

POST /v1/relationType/delete?Id={id}

Create or Update Relation

Create a relation instance between two groups.

POST /v1/relation

Parameters

ParameterTypeDescription
IdintegerRelation ID (include to update)
FromGroupIdintegerRequired. Source group ID
ToGroupIdintegerRequired. Target group ID
GroupRelationTypeIdintegerRequired. Relation type ID
NamestringOptional relation instance name
DescriptionstringOptional description

Constraints

  • A group cannot be related to itself: FromGroupId == ToGroupId is rejected with cannot relate to self.
  • Both groups and the relation type must have categories assigned.
  • Each group's category must match the relation type's: the source group's category must equal FromCategory, and the target group's must equal ToCategory. A mismatch returns 400 with a message naming both sides.

When the relation type carries a BackRelationId, the reverse edge is created in the same transaction, so one POST can produce two rows. Only the forward relation is returned.

Example

curl -X POST http://localhost:8181/v1/relation \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"FromGroupId": 10,
"ToGroupId": 20,
"GroupRelationTypeId": 1
}'

Delete Relation

Delete a relation instance.

POST /v1/relation/delete?Id={id}

Example

curl -X POST "http://localhost:8181/v1/relation/delete?Id=5" \
-H "Accept: application/json"

Inline Editing for Relations

As with group inline editing, send the new value in the request body: Name for editName (required, non-empty) and Description for editDescription.

Edit Name

POST /v1/relation/editName?id={id}

Edit Description

POST /v1/relation/editDescription?id={id}

Inline Editing for Relation Types

Send the new value in the request body: Name for editName (required, non-empty) and Description for editDescription.

Edit Name

POST /v1/relationType/editName?id={id}

Edit Description

POST /v1/relationType/editDescription?id={id}