The module lifecycle stage: General Availability
The module has requirements for installation
A Technology Preview of an actively developed Terraform provider is available for Deckhouse Commander; it works via the Integration API described in this section. Currently, the provider supports creating, updating, and deleting clusters and cluster templates within the single workspace in which the API access token was issued.
Overview
Deckhouse Commander exposes an external API for automation and integration scenarios — CI/CD, self-service portals, IDP platforms. Interactive OpenAPI (Swagger) documentation is built into the web interface and always matches the running backend; to open it, click the gear icon in the top menu bar:
- on the start page (the workspaces list), the API Documentation item opens the global API reference at
/documentation; - on a workspace page, the API Documentation item opens the workspace API reference at
/workspaces/<workspace-slug>/auth-tokens/documentation.
Every request must include a token in the X-Auth-Token HTTP header. All endpoints are relative to https://$COMMANDER_HOST; examples in this document use $COMMANDER_HOST for the host and $COMMANDER_GLOBAL_TOKEN / $COMMANDER_TOKEN for tokens.
GET /api/v1/workspaces HTTP/1.1
Host: $COMMANDER_HOST
X-Auth-Token: $COMMANDER_GLOBAL_TOKEN
Accept: application/jsonTwo token types are issued in the Commander web interface (see Integration API and Tokens for the full procedure):
- Global tokens — issued on the start page (the workspaces list), under Parameters → Auth Tokens. Let you manage workspaces, global roles, and workspace role bindings. They cannot manage resources inside workspaces.
- Workspace tokens — issued inside a workspace on its Parameters → Auth Tokens page. Let you manage resources of that workspace: clusters, cluster templates, inventory catalogs, records, container registries, and projects. A workspace token is always scoped to a single workspace; an attempt to access another workspace returns
403 Forbidden.
Requests without a token, or with an invalid or expired token, return 401 Unauthorized. For exhaustive request and response schemas, enums, and per-endpoint status codes, always consult the Swagger UI.
Available via a global token
- View global roles:
GET /api/v1/global_roles
- Manage workspaces:
GET /api/v1/workspacesPOST /api/v1/workspacesPUT /api/v1/workspaces/:idDELETE /api/v1/workspaces/:id
- Manage workspace role bindings:
GET /api/v1/workspace_role_bindingsPOST /api/v1/workspace_role_bindingsGET /api/v1/workspace_role_bindings/:idPUT /api/v1/workspace_role_bindings/:idDELETE /api/v1/workspace_role_bindings/:id
Available via a workspace token
All endpoints are implicitly scoped to the workspace the token was issued in. Endpoints marked (archivable) accept an archived=true query parameter to include archived entities in the response; archived entities are excluded by default.
- Manage cluster templates:
GET /api/v1/cluster_templates(archivable)POST /api/v1/cluster_templatesGET /api/v1/cluster_templates/:idPUT /api/v1/cluster_templates/:idDELETE /api/v1/cluster_templates/:id
- Manage inventory catalogs:
GET /api/v1/catalogs(archivable)POST /api/v1/catalogsGET /api/v1/catalogs/:idPUT /api/v1/catalogs/:idDELETE /api/v1/catalogs/:id
- Manage catalog records:
POST /api/v1/recordsGET /api/v1/records(archivable)GET /api/v1/records/:idPUT /api/v1/records/:idDELETE /api/v1/records/:id
- Manage clusters:
POST /api/v1/clustersGET /api/v1/clusters(archivable)GET /api/v1/clusters/:idGET /api/v1/clusters/:id/statusPUT /api/v1/clusters/:idDELETE /api/v1/clusters/:idDELETE /api/v1/clusters/:id/force
- Manage container registries:
POST /api/v1/registriesGET /api/v1/registriesGET /api/v1/registries/:idDELETE /api/v1/registries/:id
- Manage projects:
GET /api/v1/projectsPOST /api/v1/projectsGET /api/v1/projects/:idPUT /api/v1/projects/:idDELETE /api/v1/projects/:id
- Approve cluster change requests:
GET /api/v1/cluster_change_requestsGET /api/v1/cluster_change_requests/:idPOST /api/v1/cluster_change_requests/:id/approve
- Read cluster tasks and logs:
GET /api/v1/cluster_tasks(archivable)GET /api/v1/cluster_task_logs(archivable)
Errors
All errors share the same JSON envelope:
{
"error": "Human-readable message",
"error_code": "invalid",
"errors": { "name": ["can't be blank"] }
}Rely on error_code as the stable contract for automation; the error message is localized and may change. Standard HTTP status codes apply; the full per-endpoint list is in the Swagger UI. The most common cases:
401 no_token/invalid_token— theX-Auth-Tokenheader is missing, expired, or not recognized.403 forbidden— the token lacks permission or targets a resource outside its workspace.409 stale_objecton aPUT— the resource was modified concurrently; re-fetch, merge, and retry with the newcurrent_revision.409 conflicton/cluster_change_requests/:id/approve(already_approved,already_completed,cluster_cannot_be_convergedand similar) — do not retry approval; re-read the change request and decide on the next action.422 invalid— validation failed; thedatafield contains per-field errors.500 internal_server_error— unexpected backend error; safe to retry idempotent requests with exponential backoff.
When polling a cluster, inspect both status and render_errors: a template-render error leaves the cluster in a non-terminal status but populates render_errors.
Examples
All examples below assume the following shell variables are set:
export COMMANDER_HOST="commander.example.com"
export COMMANDER_GLOBAL_TOKEN="..." # global token
export COMMANDER_TOKEN="..." # workspace tokenCreate a workspace
curl -X "POST" \
"https://$COMMANDER_HOST/api/v1/workspaces" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_GLOBAL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Workspace from API"}'The response contains the workspace data. The id is needed for subsequent requests.
{
"id": "088b8e03-e7c0-4655-aae7-d11b7609c579",
"slug": "sftpp",
"name": "Workspace from API",
"comment": null,
"main": false,
"current_revision": 0,
"created_at": "2026-01-15T12:19:57.002+03:00",
"updated_at": "2026-01-15T12:19:57.002+03:00"
}Look up a global role
To assign a global role to a user or group, fetch the role’s id:
curl -s -X "GET" \
"https://$COMMANDER_HOST/api/v1/global_roles" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_GLOBAL_TOKEN"The response contains role objects; the id field is what’s needed:
[
{
"id": "818c3814-4973-452e-9985-d75fbe35eb4c",
"name": "commander-admin",
"comment": null,
"labels": {
"commander.deckhouse.io/owned-by": "commander"
},
"current_revision": 0,
"commander_rules": [
{
"id": "455ccf46-f34c-4c75-970d-4dbc361adc68",
"verbs": ["*"],
"resources": ["*"],
"resource_names": null,
"created_at": "2025-04-30T23:46:18.616+03:00",
"updated_at": "2025-04-30T23:46:18.616+03:00"
}
],
"kubernetes_rules": [
{
"id": "9713527d-69df-4ecb-9db2-35df194e18b5",
"verbs": ["*"],
"resources": ["*"],
"resource_names": null,
"api_groups": ["*"],
"created_at": "2025-06-06T16:56:32.208+03:00",
"updated_at": "2025-06-06T16:56:32.208+03:00"
}
],
"created_at": "2025-04-30T23:46:18.567+03:00",
"updated_at": "2025-04-30T23:46:18.567+03:00"
}
]Create a workspace role binding
PAYLOAD="$(jq -nc '{
"workspace_id": "088b8e03-e7c0-4655-aae7-d11b7609c579",
"name": "workspace-admins",
"comment": "Role binding from API",
"role": {
"type": "GlobalRole",
"id": "818c3814-4973-452e-9985-d75fbe35eb4c"
},
"subjects": [
{
"kind": "User",
"name": "commander-admin@deckhouse.io"
}
]
}')"
curl -X "POST" \
"https://$COMMANDER_HOST/api/v1/workspace_role_bindings" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_GLOBAL_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"Successful response:
{
"id": "05b3abdf-20a5-447b-823b-8e1a57b46158",
"name": "workspace-admins",
"comment": "Role binding from API",
"current_revision": 0,
"role_type": "GlobalRole",
"role": {
"id": "818c3814-4973-452e-9985-d75fbe35eb4c",
"name": "commander-admin",
"comment": null,
"labels": {
"commander.deckhouse.io/owned-by": "commander"
},
"current_revision": 0,
"created_at": "2025-04-30T23:46:18.567+03:00",
"updated_at": "2025-04-30T23:46:18.567+03:00"
},
"subjects": [
{
"kind": "User",
"name": "commander-admin@deckhouse.io",
"created_at": "2026-01-15T14:01:01.858+03:00",
"updated_at": "2026-01-15T14:01:01.858+03:00"
}
],
"created_at": "2026-01-15T14:01:01.821+03:00",
"updated_at": "2026-01-15T14:01:01.821+03:00"
}Add a user group to a role binding
PUT /api/v1/workspace_role_bindings/:id replaces subjects wholesale and requires current_revision for optimistic locking.
PAYLOAD="$(jq -nc '{
"current_revision": 0,
"subjects": [
{
"kind": "User",
"name": "commander-admin@deckhouse.io"
},
{
"kind": "Group",
"name": "Administrators"
}
]
}')"
curl -X "PUT" \
"https://$COMMANDER_HOST/api/v1/workspace_role_bindings/05b3abdf-20a5-447b-823b-8e1a57b46158" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_GLOBAL_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"Get the current template version
TEMPLATE_ID="fb999a72-efe7-4db7-af53-11b17bc0a687"
curl -s -X "GET" \
"https://$COMMANDER_HOST/api/v1/cluster_templates/$TEMPLATE_ID" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_TOKEN" |
jq "del(.cluster_template_versions)"The current_cluster_template_version_id field references the latest template version:
{
"id": "fb999a72-efe7-4db7-af53-11b17bc0a687",
"name": "YC Dev",
"current_cluster_template_version_id": "8e75210a-f05c-421d-84b3-fc0697814d6d",
"comment": "",
"current_revision": 12,
"immutable": false,
"created_at": "2024-02-05T17:35:44.318+03:00",
"updated_at": "2024-04-10T18:00:57.835+03:00",
"archived_at": null,
"archive_number": null
}Capture it for later use:
TEMPLATE_VERSION_ID="$(curl -s -X "GET" \
"https://$COMMANDER_HOST/api/v1/cluster_templates/$TEMPLATE_ID" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_TOKEN" |
jq -r ".current_cluster_template_version_id")"Find an unused record in the catalog
Inspect the input parameter schema of the selected template version:
curl -s -X "GET" \
"https://$COMMANDER_HOST/api/v1/cluster_templates/$TEMPLATE_ID" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_TOKEN" |
jq -r --arg ver "$TEMPLATE_VERSION_ID" '
.cluster_template_versions[]
| select(.id == $ver)
| .params'The schema has three required parameters; one of them is a record from the yandex-cloud-slot catalog (the catalog field):
[
{ "header": "Cluster Parameters" },
{
"key": "slot",
"span": 4,
"title": "Slot for cluster in Yandex Cloud",
"catalog": "yandex-cloud-slot",
"immutable": true
},
{
"key": "releaseChannel",
"enum": ["Alpha", "Beta", "EarlyAccess", "Stable", "RockSolid"],
"span": 1,
"title": "Update channel",
"default": "EarlyAccess"
},
{
"key": "kubeVersion",
"enum": ["Automatic", "1.25", "1.26", "1.27", "1.28", "1.29"],
"span": 1,
"title": "Kubernetes version",
"default": "Automatic"
}
]Look up the catalog by its slug:
CATALOG_ID="$(curl -s -X "GET" \
"https://$COMMANDER_HOST/api/v1/catalogs" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_TOKEN" |
jq -r '.[] | select(.slug == "yandex-cloud-slot") | .id')"Pick the first record that is not yet taken by another cluster (cluster_id == null) and prepare it for use in the cluster by embedding the record ID in the special x-commander-record-id field:
SLOT_VALUES="$(curl -s -X "GET" \
"https://$COMMANDER_HOST/api/v1/records" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_TOKEN" |
jq -c --arg cid "$CATALOG_ID" '
[ .[]
| select(.catalog_id == $cid and .cluster_id == null) ][0]
| .values + { "x-commander-record-id": .id }')"
echo "$SLOT_VALUES"Result:
{
"ip": "158.166.177.188",
"name": "x",
"x-commander-record-id": "5f6727e7-630c-4b18-bcf0-868ea96a27ee"
}The x-commander-record-id key is used instead of a plain id so it never collides with user-defined id fields inside a record.
Create the cluster
PAYLOAD="$(jq -nc --argjson slot "$SLOT_VALUES" --arg ver "$TEMPLATE_VERSION_ID" '{
"name": "Cluster from API",
"cluster_template_version_id": $ver,
"values": {
"kubeVersion": "1.29",
"releaseChannel": "EarlyAccess",
"slot": $slot
}
}')"
curl -X "POST" \
"https://$COMMANDER_HOST/api/v1/clusters" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"The response carries the cluster state. Some fields (rendered configuration, timestamps) are omitted here for brevity:
{
"id": "5436e6ef-d811-472f-9c9c-46cb9c6321d9",
"name": "Cluster from API",
"values": {
"slot": {
"ip": "158.166.177.188",
"name": "x",
"x-commander-record-id": "5f6727e7-630c-4b18-bcf0-868ea96a27ee"
},
"kubeVersion": "1.29",
"releaseChannel": "EarlyAccess"
},
"cluster_template_version_id": "8e75210a-f05c-421d-84b3-fc0697814d6d",
"was_created": false,
"status": "new"
}Poll cluster provisioning status
After creating or updating a cluster, poll GET /api/v1/clusters/:id until the cluster reaches status: "in_sync". Take render_errors into account: a non-empty list means the template did not render and the operation cannot proceed until the input values are fixed via PUT /api/v1/clusters/:id.
CLUSTER_ID="5436e6ef-d811-472f-9c9c-46cb9c6321d9"
DEADLINE=$(( $(date +%s) + 60 * 60 )) # one-hour timeout
while : ; do
RESP="$(curl -sS -X "GET" \
"https://$COMMANDER_HOST/api/v1/clusters/$CLUSTER_ID" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_TOKEN")"
STATUS="$(jq -r ".status" <<<"$RESP")"
RENDER_ERRORS="$(jq -r ".render_errors // [] | length" <<<"$RESP")"
echo "status=$STATUS render_errors=$RENDER_ERRORS"
if [ "$STATUS" = "in_sync" ]; then
echo "cluster is ready"
break
fi
if [ "$RENDER_ERRORS" != "0" ]; then
echo "template render failed, see render_errors:"
jq ".render_errors" <<<"$RESP"
exit 1
fi
if [ "$(date +%s)" -gt "$DEADLINE" ]; then
echo "timeout while waiting for in_sync"
exit 1
fi
sleep 10
doneTypical sequence of status values: new → creating → in_sync. A stuck creating together with a non-empty render_errors indicates a template or input-value problem; update the cluster via PUT and the poll will resume.
Fetch a lightweight cluster status
For frequent polling and status dashboards, use GET /api/v1/clusters/:id/status. It returns a compact snapshot — current status, active alerts, an Upmeter summary, and master-node connection info — and omits sensitive fields such as rendered configuration and the agent API key.
curl -s -X "GET" \
"https://$COMMANDER_HOST/api/v1/clusters/$CLUSTER_ID/status" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_TOKEN" | jq .{
"id": "5436e6ef-d811-472f-9c9c-46cb9c6321d9",
"status": "in_sync",
"active_alerts": [
{ "name": "KubeletDown", "severity_level": 4 }
],
"upmeter": {
"status": "up",
"components": [
{ "name": "control-plane", "status": "up" },
{ "name": "synthetic", "status": "up" }
]
},
"master_node": {
"host": "203.0.113.10",
"user": "ubuntu"
}
}active_alerts may be empty; upmeter and master_node may be null while the cluster agent is still collecting data.
Delete a cluster and wait for it to be destroyed
DELETE /api/v1/clusters/:id only schedules deletion — the response contains the cluster with status: "delete" and is_locked: true. The cluster is fully removed when a subsequent GET returns 404 not_found.
CLUSTER_ID="5436e6ef-d811-472f-9c9c-46cb9c6321d9"
curl -sS -X "DELETE" \
"https://$COMMANDER_HOST/api/v1/clusters/$CLUSTER_ID" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_TOKEN" | jq "{id, status, is_locked}"{
"id": "5436e6ef-d811-472f-9c9c-46cb9c6321d9",
"status": "delete",
"is_locked": true
}Poll until the cluster is gone:
DEADLINE=$(( $(date +%s) + 60 * 60 ))
while : ; do
HTTP_CODE="$(curl -sS -o /dev/null -w "%{http_code}" -X "GET" \
"https://$COMMANDER_HOST/api/v1/clusters/$CLUSTER_ID" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_TOKEN")"
echo "http_code=$HTTP_CODE"
if [ "$HTTP_CODE" = "404" ]; then
echo "cluster fully deleted"
break
fi
if [ "$(date +%s)" -gt "$DEADLINE" ]; then
echo "timeout while waiting for deletion"
exit 1
fi
sleep 10
doneIf the cluster is locked by another operation (for example, a converge is in progress), DELETE returns 409 Conflict:
{
"error": "Cluster is locked",
"error_code": "conflict",
"errors": null
}Retry DELETE after the blocking operation finishes, or inspect pending cluster change requests via GET /api/v1/cluster_change_requests?cluster_id=$CLUSTER_ID and approve or cancel them.
Force-delete a cluster
Use DELETE /api/v1/clusters/:id/force when the infrastructure has already been destroyed outside Commander, or the cluster is stuck in a state from which the regular DELETE /api/v1/clusters/:id cannot recover. A force delete removes the cluster record from Commander without running dhctl destroy and does not touch any external resources — clean up the infrastructure yourself.
curl -sS -X "DELETE" \
"https://$COMMANDER_HOST/api/v1/clusters/$CLUSTER_ID/force" \
-H "accept: application/json" \
-H "X-Auth-Token: $COMMANDER_TOKEN" | jq "{id, status, is_locked}"Prefer the regular DELETE /api/v1/clusters/:id for normal disposal and keep force delete as a manual recovery tool
Managing Projects
The API lets you list projects and create, update, or delete projects of kind commander. Projects of kind deckhouse are DKP projects (Project resources of the multitenancy-manager module) created directly in the target cluster and not managed by Deckhouse Commander; Commander discovers and displays them, but modifying or deleting such projects through the API is forbidden — an attempt returns 403. For details on project kinds, statuses, and name collisions, see the Projects section of the user guide.
All examples use a workspace token (X-Auth-Token: $COMMANDER_TOKEN).
Listing projects
curl -s -X 'GET' \
"https://$COMMANDER_HOST/api/v1/projects" \
-H 'accept: application/json' \
-H "X-Auth-Token: $COMMANDER_TOKEN"The response contains an array of workspace projects. The example below is trimmed for brevity:
[
{
"id": "1a0c4f1b-8f24-4d0a-9a21-0c2d7e1a2b3c",
"name": "web-frontend",
"kind": "commander",
"description": "Team public site",
"resource_quota": {
"requests": {
"cpu": "2",
"memory": "4Gi",
"storage": "50Gi"
}
},
"cumulative_status": "deployed",
"status_data": {},
"kubernetes_project_id": "b7c1d1d8-9d0e-4cbb-8d9e-2b77b6b9f9c1",
"cluster_id": "5436e6ef-d811-472f-9c9c-46cb9c6321d9",
"cluster_name": "prod-eu-1",
"administrators": [
{ "kind": "User", "name": "alice@example.com" },
{ "kind": "Group", "name": "platform-admins" }
],
"error_messages": [],
"created_at": "2026-04-10T12:00:00Z",
"synced_at": "2026-04-23T18:15:32Z"
}
]Key response fields:
id— project identifier in Deckhouse Commander;name— project name (matches the namespace name in the target cluster);kind— project kind:commanderordeckhouse;cluster_id,cluster_name— the target cluster where the project is created;cumulative_status— combined project status (see Project statuses);kubernetes_project_id— the identifier of theProjectresource in the target cluster; populated once the agent applies it;resource_quota— the project resource quotas that have been set;administrators— users and groups assigned as project administrators;error_messages— project application error messages, if any.
Creating a project
curl -s -X 'POST' \
"https://$COMMANDER_HOST/api/v1/projects" \
-H 'accept: application/json' \
-H "X-Auth-Token: $COMMANDER_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "web-frontend",
"cluster_id": "5436e6ef-d811-472f-9c9c-46cb9c6321d9",
"description": "Team public site",
"resource_quota": {
"requests": {
"cpu": "2",
"memory": "4Gi",
"storage": "50Gi"
}
},
"administrators": [
{ "kind": "User", "name": "alice@example.com" },
{ "kind": "Group", "name": "platform-admins" }
]
}'Request parameters:
name(required) — project name. Must follow the Kubernetes namespace naming rules; the namesdefaultanddeckhouseare reserved; uniqueness is checked within the cluster.cluster_id(required) — target cluster identifier. It cannot be changed after the project is created.administrators(required, at least one entry) — an array of{"kind": "User"|"Group", "name": "..."}objects. OnlyUserandGroupare accepted inkind.description— free-form project description.resource_quota— resource quotas; supported keys arerequests.cpu,requests.memory, andrequests.storage. Limits (limits) are not exposed in the current iteration.
On success, the response is 201 Created with the project object in the same shape as in the list endpoint. Right after creation the cumulative_status is usually initialized; it then transitions to synchronization and, once applied, to deployed.
Validation errors return 422 Unprocessable Entity, for example when the administrators list is empty:
{
"errors": {
"administrators": ["A project must have at least one administrator"]
}
}Updating a project
curl -s -X 'PUT' \
"https://$COMMANDER_HOST/api/v1/projects/1a0c4f1b-8f24-4d0a-9a21-0c2d7e1a2b3c" \
-H 'accept: application/json' \
-H "X-Auth-Token: $COMMANDER_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "web-frontend",
"description": "Team public site (prod)",
"resource_quota": {
"requests": {
"cpu": "4",
"memory": "8Gi",
"storage": "100Gi"
}
},
"administrators": [
{ "kind": "User", "name": "alice@example.com" },
{ "kind": "User", "name": "bob@example.com" },
{ "kind": "Group", "name": "platform-admins" }
]
}'The request body matches the creation payload with one exception: the target cluster of an existing project cannot be changed, so cluster_id is not accepted on update. The response contains the updated project object.
Updating a project of kind deckhouse returns 403 Forbidden.
Deleting a project
Deleting a project through the API is an irreversible operation. All resources created in the project (namespace, workloads, configurations, and secrets) are deleted from the target cluster together with the project. The deletion cannot be cancelled, and neither the project itself nor its resources can be restored afterwards. Before issuing the DELETE request, make sure the project and its data are no longer needed — in integration scenarios it is strongly recommended to require an explicit delete confirmation on the calling system’s side.
curl -s -X 'DELETE' \
"https://$COMMANDER_HOST/api/v1/projects/1a0c4f1b-8f24-4d0a-9a21-0c2d7e1a2b3c" \
-H 'accept: application/json' \
-H "X-Auth-Token: $COMMANDER_TOKEN"Commander removes its internal labels from the project and asks the agent to delete the Project resource in the target cluster. The response contains the deleted project object with the updated status.
Deleting a project of kind deckhouse through the API is not allowed and returns 403 Forbidden; such projects are deleted directly in Deckhouse using the multitenancy-manager module.
Managing project members (the ProjectRoleBinding resources) and user-defined project templates through the Integration API is not supported in the current release and will be added in future releases.