Skip to content
Console

Provision Hubs

Use this page when your code should create the hub instead of clicking through the dashboard.

Provisioning covers five steps: discover skills in the marketplace catalog, create a runtime group, create a hub attached to it, install a skill into the group, and release both onto a channel. Every Thalovant SDK exposes the same five calls, and the MCP Server exposes them as tools.

Step Call What it needs
Discover skills list_marketplace_skills hubs:read. Any plan.
Create a runtime group create_runtime_group hubs:write and a paid plan.
Create a hub create_hub hubs:write and a paid plan.
Install a skill install_runtime_group_skill hubs:write, a paid plan, and marketplace access for paid skills.
Release release_runtime_group and release_hub hubs:write and a paid plan.

Method names follow each language. The table uses the Python spelling; see Call Names By Language for the others.

  1. Mint an API token. Create it on the dashboard’s API Tokens page with the scopes below.
  2. Check the plan. Every hub and runtime group write needs a paid workspace plan. Browsing the catalog does not.
  3. Decide the runtime group. A hub runs its skills in a runtime group. Create the group first so the hub can attach to it.
Scope What it covers
hubs:read The marketplace catalog and the list of runtime groups.
hubs:inspect Runtime group inventory and the group’s resolved marketplace view.
hubs:write Creating, updating, releasing, and deleting hubs and runtime groups, and installing skills.

These scopes imply one another. hubs:write grants hubs:read, which grants hubs:inspect. A token minted with hubs:read covers every discovery call on this page.

This flow discovers skills, creates a runtime group and a hub, installs one skill, and releases both.

import os
from thalovant import ThalovantControlPlane
api = ThalovantControlPlane(access_token=os.environ["THALOVANT_API_TOKEN"])
# 1. Discover what is installable before provisioning anything.
for skill in api.list_marketplace_skills()["data"]:
print(skill["skill_id"], skill["title"], skill["access_tier"])
# 2. Create a runtime group to run the skills.
group = api.create_runtime_group({"name": "kiosks", "description": "Lobby kiosks"})
# 3. Create a hub attached to it.
hub = api.create_hub(
{
"name": "joke-garden",
"runtime_group_id": group["id"],
"spec": {"protocols": {"wss": {"enabled": True}}},
}
)
# 4. Install a skill from the marketplace catalog.
api.install_runtime_group_skill(group["id"], "skill-weather")
# 5. Roll the runtime group and the hub onto a release channel.
api.release_runtime_group(group["id"], channel="stable")
api.release_hub(hub["id"], channel="stable")

Kotlin, Swift, and .NET expose the same five calls with native names. Their SDK pages show the language-specific form.

Hub creation accepts an idempotency key, so a retry after a network timeout returns the original hub instead of creating a second one. Every SDK generates a key when you do not pass one. Reusing a key with a different body is an error; see the table below.

Runtime group creation and skill install do not take an idempotency key.

A skill in the catalog is not always installable for your plan. The group’s marketplace view resolves the catalog against one runtime group and reports whether each entry can be installed, so you can decide up front instead of reading an error afterwards.

Catalog

list_marketplace_skills lists every published skill. It needs hubs:read and no paid plan.

Group marketplace view

list_runtime_group_marketplace resolves the catalog for one runtime group and reports whether each skill is installable and whether it needs a purchase. It needs hubs:inspect.

Group inventory

list_runtime_group_inventory lists the skills actually observed running in the group. It needs hubs:inspect.

Hub update and delete use optimistic locking. Both require the hub’s current etag, sent as an If-Match header by the SDK.

The etag is only in the body of the hub resource. The API sends no ETag response header, so read the hub first and take the etag field from that response.

hub = api.get_hub(hub_id)
hub = api.update_hub(hub_id, {"active": False}, etag=hub["etag"])
api.delete_hub(hub_id, etag=hub["etag"])

A missing etag and a stale etag both fail the same way: HTTP 412 with the detail ETag mismatch, and nothing is changed. Read the hub again, take the new etag, and retry.

Runtime group writes do not use etags. Runtime group resources carry no etag to round-trip, and no runtime group route reads If-Match.

A hub’s name, namespace, and domain are fixed at creation. Sending a new value for any of them fails with HTTP 400 and a message such as Name cannot be changed after hub creation. Pick them carefully, because moving a hub to a new name means creating a new hub and connecting its clients again.

Sending the current value again is accepted. The field is dropped from the update rather than rejected, so a read-modify-write that carries these fields along does not break.

Everything else on the hub, including active, visibility, slug, spec, and the runtime group it attaches to, stays editable through the etag flow above.

Use this table when you already know the task and need the language-specific name.

Task Python Node.js Go Rust
List catalog skills list_marketplace_skills(...) listMarketplaceSkills(...) ListMarketplaceSkills(ctx, opts) list_marketplace_skills(opts)
Create hub create_hub(payload, ...) createHub(payload, options) CreateHub(ctx, payload, opts) create_hub(payload, idempotency_key)
Get hub get_hub(hub_id) getHub(hubId) GetHub(ctx, hubID) get_hub(hub_id)
Update hub update_hub(id, payload, etag=) updateHub(id, payload, { etag }) UpdateHub(ctx, id, payload, etag) update_hub(id, payload, etag)
Delete hub delete_hub(id, etag=) deleteHub(id, { etag }) DeleteHub(ctx, id, etag) delete_hub(id, etag)
Create runtime group create_runtime_group(payload) createRuntimeGroup(payload) CreateRuntimeGroup(ctx, payload) create_runtime_group(payload)
List runtime groups list_runtime_groups(...) listRuntimeGroups(options) ListRuntimeGroups(ctx, ownerID) list_runtime_groups(owner_id)
Group inventory list_runtime_group_inventory(...) listRuntimeGroupInventory(...) ListRuntimeGroupInventory(ctx, id, opts) list_runtime_group_inventory(id, refresh)
Group marketplace view list_runtime_group_marketplace(...) listRuntimeGroupMarketplace(...) ListRuntimeGroupMarketplace(ctx, id, opts) list_runtime_group_marketplace(id, refresh)
Install skill install_runtime_group_skill(...) installRuntimeGroupSkill(...) InstallRuntimeGroupSkill(ctx, id, skill, opts) install_runtime_group_skill(id, skill, opts)
Release runtime group release_runtime_group(...) releaseRuntimeGroup(id, options) ReleaseRuntimeGroup(ctx, id, opts) release_runtime_group(id, opts)
Release hub release_hub(...) releaseHub(id, options) ReleaseHub(ctx, id, opts) release_hub(id, opts)

Kotlin and Swift use the Node.js spelling with native option types, such as createHub and updateHub(id, payload, etag). C# adds the async suffix, such as CreateHubAsync and UpdateHubAsync(id, options, etag). The Kotlin, Swift, and .NET SDK pages show the exact signatures.

Status Detail What it means and what to do
403 Insufficient scopes The token is missing the scope the route needs. Mint a token with hubs:write for provisioning, or hubs:read for the catalog.
402 API access requires a paid plan. The scope is right but the workspace is on a free plan. Upgrade the workspace, then retry. Discovery calls keep working on the free plan.
402 This skill requires paid marketplace access for the tenant plan. The plan is paid but does not cover this paid catalog entry. Check the group marketplace view first, which reports installable, purchase_required, and access_message per skill.
412 ETag mismatch The If-Match etag on a hub update or delete was missing or stale. Read the hub again, take the etag from the response body, and retry. Nothing was changed.
409 Idempotency key re-used with different payload A hub create reused an idempotency key with a different body. Use a fresh key for a genuinely new hub, or resend the exact original body to replay the first result.
400 Name cannot be changed after hub creation The update tried to change name, namespace, or domain. Create a new hub instead.

The scope check runs before the plan check, so the two never overlap. A free-plan API token is capped at hubs:read, clients:read, and clients:write, so it can never carry hubs:write. A provisioning call with that token fails at the scope check with 403 and never reaches the plan check.

Read a 403 on a provisioning call as “this token cannot do this”, and a 402 as “this workspace cannot do this”. Both 402s share the same shape, so tell them apart by their detail text.

The MCP Server exposes the same flow as tools, so an agent can discover skills and provision a hub without writing SDK code. The delete tools are not available by default; see Destructive Tools.