Skip to content
Console
Try one

    Provision Hubs

    Discover marketplace skills, then create a runtime group and a hub, install a skill, and release it from code.

    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. Python, Node.js, Go, Rust, Kotlin, Swift, and .NET expose these calls, and the MCP Server exposes them as tools. Embedded C supplies runtime protocol helpers; provision its hub and client identity separately.

    StepCallWhat it needs
    Discover skillslist_marketplace_skillshubs:read. Any plan.
    Create a runtime groupcreate_runtime_grouphubs:write and a paid plan.
    Create a hubcreate_hubhubs:write and a paid plan.
    Install a skillinstall_runtime_group_skillhubs:write, a paid plan, and marketplace access for paid skills.
    Releaserelease_runtime_group and release_hubhubs:write and a paid plan. Pinned images must be ones the platform releases unless you are a platform administrator; see the platform_image_required row under Error Guidance.

    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.
    ScopeWhat it covers
    hubs:readThe marketplace catalog and the list of runtime groups.
    hubs:inspectRuntime group inventory and the group’s resolved marketplace view.
    hubs:writeCreating, 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.

    The examples create a Standard hub. Autoscaling capacity is a separate hub setting controlled by the workspace plan. Changing a runtime group’s replica settings does not enable it.

    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-addressed skill methods manage the hub’s attached runtime group, affecting every hub sharing it. list_hub_skills returns the hub’s skill rows under data, each with an installed_version, update_available, and a state; install_hub_skill, update_hub_skill, and remove_hub_skill select the runtime group by hub ID, return an operation you can wait on, and apply to all hubs sharing that group. Listing needs hubs:inspect; the writes need hubs:write and a paid plan, and a hub that is not in a runtime group yet answers HTTP 404 with the code hub_without_runtime_group. All seven managed SDKs now provide these methods and skill history at the current versions; see Add a Skill to a Hub for the routes, the app path, and the errors.

    Generate and retain an explicit idempotency key before the first hub-create attempt. Reuse that key and the same payload when retrying that logical create after a network timeout. Omitting the key generates a new one on each SDK call, so a separate call can create a second hub. The SDKs do not automatically retry creation. 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"])

    Pass a nonempty current etag. SDKs reject empty values before sending where local validation is provided; a stale etag reaches the API and fails with HTTP 412 and the detail ETag mismatch. Read the hub again and reconcile your changes before retrying with its new etag.

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

    Legacy PATCH runtime-group config updates replace the configuration, except the protected control section. Omitted runtime keys are dropped; personas changes only when supplied. Coordinate writers using that unconditional route.

    For a guarded update, read GET /v1/runtime-groups/{id}/config, merge your changes into its config, and send PUT to the same path with config and expected_revision set to the returned revision. HTTP 412 means nothing was written: read again and reapply your original changes. PUT requires a revision; legacy PATCH remains supported. Older API versions reject PUT without writing.

    Python 0.7.5, Node/Swift/.NET/Kotlin 0.7.1, Go 0.9.1 and Rust 0.9.0, and MCP 0.5.1 use this guarded flow by default, with at most three write attempts. Only HTTP 412 triggers another read and merge of the original delta. Objects merge recursively; arrays and scalars replace. Omitted personas stay unchanged; supplied personas replace. Merge requires the hubs:read and hubs:write scopes and a paid plan. An older server without a valid revision fails before a write; there is no unconditional fallback.

    For an intentional full replacement, use Python merge=False, Node/MCP merge: false, or the other SDKs’ replaceRuntimeGroupConfig equivalent. Replacement requires the hubs:write scope, a paid plan, and coordination with other writers. When upgrading code that relied on replacement, select it explicitly.

    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.

    Editable fields include active, visibility, slug, spec, capacity_profile, and the runtime group the hub attaches to. Use the etag flow above and send only accepted update fields. The plan determines the worker ceiling when selecting Autoscaling capacity.

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

    TaskPythonNode.jsGoRust
    List catalog skillslist_marketplace_skills(...)listMarketplaceSkills(...)ListMarketplaceSkills(ctx, opts)list_marketplace_skills(opts)
    Create hubcreate_hub(payload, ...)createHub(payload, options)CreateHub(ctx, payload, opts)create_hub(payload, idempotency_key)
    Get hubget_hub(hub_id)getHub(hubId)GetHub(ctx, hubID)get_hub(hub_id)
    Update hubupdate_hub(id, payload, etag=)updateHub(id, payload, { etag })UpdateHub(ctx, id, payload, etag)update_hub(id, payload, etag)
    Delete hubdelete_hub(id, etag=)deleteHub(id, { etag })DeleteHub(ctx, id, etag)delete_hub(id, etag)
    Create runtime groupcreate_runtime_group(payload)createRuntimeGroup(payload)CreateRuntimeGroup(ctx, payload)create_runtime_group(payload)
    List runtime groupslist_runtime_groups(...)listRuntimeGroups(options)ListRuntimeGroups(ctx, ownerID)list_runtime_groups(owner_id)
    Group inventorylist_runtime_group_inventory(...)listRuntimeGroupInventory(...)ListRuntimeGroupInventory(ctx, id, opts)list_runtime_group_inventory(id, refresh)
    Group marketplace viewlist_runtime_group_marketplace(...)listRuntimeGroupMarketplace(...)ListRuntimeGroupMarketplace(ctx, id, opts)list_runtime_group_marketplace(id, refresh)
    Install skillinstall_runtime_group_skill(...)installRuntimeGroupSkill(...)InstallRuntimeGroupSkill(ctx, id, skill, opts)install_runtime_group_skill(id, skill, opts)
    Release runtime grouprelease_runtime_group(...)releaseRuntimeGroup(id, options)ReleaseRuntimeGroup(ctx, id, opts)release_runtime_group(id, opts)
    Release hubrelease_hub(...)releaseHub(id, options)ReleaseHub(ctx, id, opts)release_hub(id, opts)
    List hub skillslist_hub_skills(hub_id)listHubSkills(hubId)pendingpending
    Install hub skillinstall_hub_skill(hub_id, skill, version=, wait=)installHubSkill(hubId, skill, { version, wait })pendingpending
    Update hub skillupdate_hub_skill(hub_id, skill, version=, wait=)updateHubSkill(hubId, skill, { version, wait })pendingpending
    Remove hub skillremove_hub_skill(hub_id, skill, wait=)removeHubSkill(hubId, skill, { wait })pendingpending

    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.

    StatusDetailWhat it means and what to do
    402API access requires a paid plan.The workspace is on the Free plan. The API checks this before the token’s scopes. Upgrade the workspace, then retry. Discovery calls keep working on the Free plan.
    403Insufficient scopesThe plan is paid but the token is missing the scope the route needs. Mint a token with hubs:write for provisioning, or hubs:read for the catalog.
    402This 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.
    412ETag mismatchThe 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.
    409Idempotency key re-used with different payloadA 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.
    400Name cannot be changed after hub creationThe update tried to change name, namespace, or domain. Create a new hub instead.
    403Only an administrator can run an image the platform does not release: …The code is platform_image_required. A release, or a hub update that changed spec.image or spec.previewBridge.image, named an image the platform does not ship. Leave images out to take the recommended release, or pin, for each image key, a catalog pin of the stable or alpha channel, the resource’s current or recommended image, its release-policy image, or the platform’s default image. A runtime group’s core also takes any tag or digest of ghcr.io/thalovant/ovos-core, and a hub’s listener any tag or digest of ghcr.io/thalovant/hivemind-listener. bus and preview_bridge take only the images listed for them. The error’s allowed_images and allowed_repositories fields name what each refused key may be. Only a platform administrator can run anything else.

    The plan check runs before the scope check, so the two never overlap. A Free-plan provisioning call answers 402 API access requires a paid plan. whatever scopes the token carries. That message names the real fix: no scope a Free token can hold would make the call work, because a Free-plan token can never carry hubs:write.

    Only a paid workspace reaches the scope check. There, a token without hubs:write answers 403 Insufficient scopes.

    Read a 402 on a provisioning call as “this workspace cannot do this”, and a 403 as “this token cannot do this”. The one 403 that is about something else is platform_image_required: the token is fine, the image is not. Both 402s share the same shape, so tell them apart by their detail text.

    The error message is one line and can be cut short, so read a refusal from the error’s fields instead. The platform_image_required sentence names every image each refused key may be, and it is longer than any SDK’s message line. problem is the whole error body when it is a JSON object, so a field the API adds later is there without a new SDK release.

    SDKStatusCodeWhole sentenceWhole body
    Python 0.8.7status_codecodedetailproblem (dict)
    Node.js 0.8.7statusCodecodedetailproblem (object)
    Go v0.10.6StatusCodeCodeProblemDetailProblem (map[string]any)
    Rust v0.12.0status_code()api_code()api_detail()api_problem() (ApiProblem)
    Kotlin 0.7.17statusCodeerrorCodedetailproblem (JsonObject)
    Swift 0.9.0statusCodeerrorCodedetailproblem (JSONObject)
    .NET 0.8.6StatusCodeErrorCodeDetailProblem (JsonObject)

    For platform_image_required, problem carries refused_images, allowed_images (per image key) and allowed_repositories (the keys that take any tag or digest). For plan_limit, it carries resource, limit, used, and plan.

    from thalovant import ThalovantAPIError
    try:
    api.release_runtime_group(group["id"], images={"core": "docker.io/me/ovos-core:dev"})
    except ThalovantAPIError as error:
    if error.code != "platform_image_required":
    raise
    print(error.detail)
    print(error.problem["allowed_images"])

    A value that a validation error echoes back from your request stays in problem and never reaches the message. The MCP server puts the code, the whole sentence, and the other fields in the tool error text; see MCP Server.

    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.