Skip to content
Console

SDK Contract

The SDK contract defines the behavior each language SDK should expose, even when method names follow local language style.

Use this page when adding a new language SDK, reviewing a release, or checking whether an integration can move between SDKs.

Area Contract
Control plane Discover public hubs, list authenticated hubs, sign in with optional MFA or through device login, accept a direct API token, poll operations with a typed status, and create client identities.
Provisioning Browse the marketplace catalog, create and manage hubs and runtime groups, install skills, and apply releases.
Identity Load the same identity fields in every SDK.
Protocols Support WSS, HTTPS, and MQTT over TLS when the identity, the hub, and the SDK tier allow them.
Requests Support natural language, action payloads, exact codes, sessions, and context.
Intent inventory List what the hub can be asked, per language and with its sentences, over the client’s own session; raise a typed policy error on refusal; raise the runtime error when the hub answers the listing with an error; fall back to names only when the hub allows only the engine manifests.
Replies Normalize text, speech, display items, policy state, and raw event data.

The contract recognizes three tiers. A tier limits which parts of the contract an implementation must ship, never how correctly it ships them.

Tier Implementations Contract scope
Full SDK Python, Node.js, Go, Rust The complete contract: full control plane plus the WSS, HTTPS, and MQTT data planes.
WSS-only SDK Kotlin, Swift, .NET The full control plane, including MFA sign-in, device login, direct token auth, and typed operation polling, with a WSS-only data plane. Requesting https or mqtt must raise a clear, typed unsupported-protocol error rather than failing while connecting.
Protocol library Embedded C Implements the wire, identity, and MQTT topic contracts byte-for-byte without owning a transport or a control plane. The integrator supplies the MQTT or WebSocket client, TLS, and RNG.

Browser builds of the Node SDK follow the full-SDK contract minus MQTT and file loading: both must raise clear errors in a browser bundle rather than being silently absent.

Every supported runtime transport must complete v3 Noise before application traffic. Preserve the full negotiation prologue, derive the specified Argon2id PSK, select only mutually offered patterns and suites, and reject downgrade, plaintext bus frames, altered ciphertext, or conflicting server pins. AESGCM-only implementations are supported when the hub offers that suite; the SDK version table records each implementation’s scope.

Persist the client static key and verified server pins in private storage. A reconnect creates fresh ephemeral keys, cipher states, counters, and reassembly buffers; callbacks or queued writes from the previous connection must not mutate or send through the new one. Keep all chunks of one message together. Readiness follows the authenticated handshake and encrypted client HELLO, rather than a socket-open event or HTTP admission response.

HTTPS preserves replica affinity and cleans up an admission owned by that transport before reconnecting after a failed poll. A failed or unacknowledged disconnect remains owned and is reported as a cleanup failure; the same SDK client must finish cleanup before admitting a replacement connection. Closing an unconnected client must not disconnect another client’s remote session. MQTT reconnects also complete a new handshake before application sends. Use a separate identity for each concurrent client process.

Ask, Query, and event-wait deadlines include connection admission, authenticated readiness, and the applicable sending and reply collection. Intent inventory timeouts apply per query or phase, with a separate optional fallback probe; they are not a single total inventory budget. Timing out a queued caller does not close the active owner. Unfinished owned work keeps its lifecycle barrier until actual cleanup finishes; later calls cannot reuse an uncertain cipher session. Bound queued event streams and report overflow to affected subscribers. Node callback subscriptions deliver events directly without an SDK queue. A soft intent miss may recover with later speech; policy denial and explicit query timeout remain terminal, preserving any partial speech.

Ask requires the matching request ID; a runtime-replaced session ID is accepted. By default, its first nonempty speech starts a fixed 250 ms settlement window. First handled or soft intent-miss without speech starts a fixed five-second empty wait; speech switches to settlement. Callers may override these durations. Later fragments do not reset these windows, and every window is clipped to the original total deadline. That deadline returns collected speech; without speech, a soft miss raises a runtime error and silence raises a timeout. Explicit caller cancellation remains cancellation. Query waits for hive.query.complete; policy denial and explicit query timeout freeze either collector immediately. Collection remains active during an admitted send, whose cancellation and cleanup retain transport ownership.

Use a fresh request ID for every logical Ask operation and a fresh query ID for every logical Query operation; generated IDs do this by default. The high-level SDKs reject an overlapping collector with the same ID on the same client, before connecting or publishing. Ask request IDs and Query IDs are separate namespaces. Rust also shares this guard across clients that share a cloned transport. The reservation ends after the logical collector is retired; it is separate from transport write ownership. Reusing an ID for a later operation, including after cancellation, can still match delayed remote replies. Reuse a session ID for conversation context instead. Embedded C leaves these checks to the caller.

Every SDK with a control plane implements these behaviors:

Behavior Contract
Sign in POST /v1/auth/token with email and password; store the returned access token.
MFA sign-in Accept optional otp_code and recovery_code inputs, sent only when set. Surface the API’s HTTP 401 mfa_required rejection with its error code intact.
Device login POST /v1/auth/device/authorize with optional scopes and client name, prompt with the verification URI and user code through an overridable prompt, and best-effort open the browser. Poll POST /v1/auth/device/token honoring the server interval and the authorization_pending, slow_down, expired_token, and access_denied error codes, with a default timeout of 15 minutes. Store the returned durable scoped token exactly like a password login. Validate both verification URLs before prompt, browser callbacks, and polling: HTTP(S) with a host, no userinfo, raw whitespace, or control characters.
Direct token auth Accept an API token in the control-plane constructor and send it as a bearer authorization header, with no login call.
Operation polling Expose get_operation (native naming) returning a typed resource whose status is the OperationStatus set: requested, committed, applied, ready, failed, timed_out.
Identity provisioning Create a client identity, honor the optional active flag (default true), and preserve returned protocol metadata.

Every SDK with a control plane implements these behaviors. Each row below is present in all seven managed language SDKs. Embedded C remains a protocol library without a control-plane client.

Behavior Contract
Catalog discovery List marketplace skills with optional force_refresh. Needs hubs:read only, and must not be gated behind a paid plan in docs or in code paths.
Group discovery List runtime groups, get one runtime group, and read its config.
Group inspection Read a runtime group’s observed skill inventory and its resolved marketplace view, each with an optional refresh flag. Preserve the installable, purchase_required, and access_message fields so callers can check access before installing.
Hub lifecycle Create, get, update, and delete a hub. Create generates a new key per call when omitted. Retries of one logical create require the same explicit key and payload.
Optimistic locking Update and delete require the hub’s current etag as a mandatory, non-defaulted parameter, sent as If-Match. The etag is read from the etag field of the hub resource body, because the API sends no ETag response header.
Immutable fields Do not offer name, namespace, or domain as changeable inputs on hub update. The API rejects a change with HTTP 400.
Group lifecycle Create, get, update, and delete a runtime group. No runtime group route sends If-Match, and runtime group resources carry no etag. Legacy PATCH config updates replace the complete configuration apart from its protected control section. GET also returns a configuration revision for conditional PUT; stale revisions return 412 without writing. The current supported managed SDKs and MCP use this guarded flow for merges, with at most three attempts and only HTTP 412 retries. Merge requires hubs:read and paid hubs:write. Explicit replacement remains available and requires coordinated writers.
Skill install Install and uninstall a skill in a runtime group. Install defaults source_type to catalog and active to true.
Hub skills List, install, update and remove shared-runtime skill attachments through a hub UUID. All hubs sharing the runtime group are affected. Preserve the API envelope and accepted operation_id; Python/Node additionally expose typed hub-skill models. Listing requires hubs:inspect; writes require hubs:write and a paid plan. Version is latest or exact. Optional waiting defaults to 120 seconds and a two-second interval, never repeats an accepted write and retains its operation ID on polling errors. For cancellation-sensitive work, retain acceptance before waiting.
Skill history Read /v1/hubs/{hub_id}/skills/history with a limit from 1 to 200. Preserve newest-first event/operation entries and nullable actor/version fields. History belongs to the shared runtime group.
Release Apply a release to a hub and to a runtime group as two separate calls, each accepting channel, mode, version, images, and reason.
Ratings Set and clear the caller’s rating on a public hub. These need hubs:write but no paid plan.

Provisioning writes need hubs:write and a paid plan. The API checks scope before plan, so SDK docs should describe HTTP 403 as a token problem and HTTP 402 as a workspace problem, and should not present 402 as the free-tier failure for a token that cannot hold the scope.

Provisioning calls return the API’s JSON resource. No SDK is required to expose typed hub or runtime group models.

Error surfaces differ by tier today. The Kotlin, Swift, and .NET SDKs expose the HTTP status on their API error type. The Python, Node.js, Go, and Rust SDKs carry the status inside the error message text instead. New SDKs should follow the Kotlin, Swift, and .NET shape.

The high-level SDKs implement these behaviors within their supported transports. Python supplies the reference naming; use the current SDK versions for fallback-handler support and the complete contract below.

Behavior Contract
Queries Send ovos.intent.list with {"lang"} and read ovos.intent.list.response; send ovos.intent.describe with {"skill_id", "intent_name", "lang"} and read ovos.intent.describe.response. A row’s method is template or keyword; expose them as engines padatious and adapt.
Definitions Send include_definitions: true on the list query and use a row’s definition when the hub attaches one. Describe row by row when it does not, all requests in flight together, one request id each.
Correlation Set context.request_id on every query and match the reply by it. Take the first matching reply and drop repeats. Match a describe by request id, or by the definition’s own skill_id, intent_name, and lang when the hub does not echo the id.
Negative answers Different for the two queries. A list response with ok: false failed: raise the SDK’s runtime error carrying the hub’s error text, because a listing that failed is not an empty hub and returning no intents would show a person a device that can do nothing. A describe response with ok: false is a real answer meaning the hub does not know that registration: return no definitions, raise nothing, and leave that intent without sentences.
Partial answers A missing describe may leave an intent without sentences when other descriptions supplied usable definitions. Empty or error descriptions alone do not turn later silence into a successful partial result. Explicitly answered empty descriptions are valid empty results.
Languages Compare tags without regard to case, with _ and - treated alike. Keep the tag the caller asked for as the key of the result.
Refusal Turn hive.policy.denied naming the query into a typed policy error at once, a subclass of the SDK’s runtime error, carrying denied_type, code, reason, and allowed. Do not wait for a timeout.
Fallback On by default. When ovos.intent.list is refused or silent until its deadline, ask intent.service.adapt.manifest.get and intent.service.padatious.manifest.get once and return names only, with source set to engine-manifests and denied naming ovos.intent.list. Raise the policy error when those are refused too; raise the timeout error if they remain silent. The legacy denied query marker does not establish a policy refusal.
Result model An inventory with languages, skills, intents, source, denied, and a has-phrases flag. Each intent has skill_id, name, id as skill_id:name, engine, enabled, phrases keyed by language, a phrases-for-language accessor, and examples(lang, limit) that prefers whole sentences to ones with a slot, then shorter ones.
Describe switch The inventory call takes a describe option, on by default. With it off, send ovos.intent.list alone and return names, engines, and enabled state without sentences, so a connection allowed only that type still gets an inventory. With it on, send ovos.intent.describe only for the rows the listing did not already carry a definition for.
Fallback handlers The optional inventory probe queries ovos.skills.fallback.list and reads ovos.skills.fallback.list.response within a separate ceiling of 1.5 seconds including connect, send and wait. Distinguish unknown from confirmed empty support; preserve cancellation. Failed or malformed reply envelopes are unknown; accepted arrays filter invalid rows from a known handler list.
Availability Expose may_answer(lang) or its native equivalent. Return false only for known-empty fallback handlers and no enabled intent phrases in that language. Go/Rust expose additive capability wrappers so existing inventory struct literals remain compatible.
Surface The inventory call plus the two low-level calls, async variants where the SDK has them, and an intents subcommand with JSON output where the SDK ships a CLI.

Connections that Thalovant creates for SDK clients and Thalovant Voice allow the four query types by default. Read-only introspection of intents the connection can already trigger adds no privilege. A connection needs only the types its client sends: ovos.intent.list always, and ovos.intent.describe only when the SDK has to ask for the definitions separately. A runtime that honors include_definitions returns them with the listing, so no describe is sent and that type is not needed. No shipping runtime does today, including the one Thalovant hubs run, so a client that wants sentences always describes. Allow both types.

The embedded C library implements the protocol side only: frame builders for listing, describes and fallback handlers, and a classifier for their replies and for the refusal. The integrator owns correlation and the result model.

Managed Sessions And Presentable Inventory

Section titled “Managed Sessions And Presentable Inventory”

The seven managed SDKs expose HubSession, HubSessionPolicy, Inventory, Skill, Intent, and optional InventoryCache within their existing transport tiers. Sessions retain subscriptions across connection replacement, serialize admission with shutdown, and retain failed cleanup ownership. An admitted Ask or Emit is never automatically replayed after an uncertain response. Factories supply connected clients; request timeouts do not include session queueing or factory execution.

Presentation inventories preserve unknown catalogue locale support and explicit intent language order. Cache records are bounded, validated, private and atomically replaced where supported; malformed, expired or unreadable records are misses. Full normalized hostnames participate in cache keys. MCP exposes the presentation view while keeping runtime sessions scoped to a tool invocation; embedded C leaves the object model, networking and storage to its caller.

Python 0.7.4 requires HiveMind bus client 1.1.9a1 for one inbound BUS callback after slave-protocol session/routing processing. It no longer suppresses repeated Python message objects. Native SDK transports have their own delivery paths; raw context is not proof of a verified peer signature.

See the managed SDK guides for examples and platform-specific ownership details.

Public discovery

List public hubs without requiring an access token.

Authenticated hubs

List hubs visible to the signed-in workspace when API access is allowed.

Identity provisioning

Create a client identity and preserve returned protocol metadata.

Protocol selection

Select WSS, HTTPS, or MQTT explicitly and fail clearly when unavailable.

Health

Check that the endpoint, identity, and transport are usable.

Ask

Send one request and return a normalized reply object.

Action

Send a structured command or UI selection without turning it into prose.

Code

Send exact input such as scanned values, serial numbers, or typed codes.

Every SDK should understand these fields:

Field Meaning
access_key Client identifier used during hub auth.
password Client secret used during hub auth.
site_id Stable client, service, device, or runtime label.
data_plane_endpoints Explicit WSS, HTTPS, and MQTT endpoints.
protocols Hub protocol flags returned with the identity.
mqtt Per-client MQTT broker credentials and topic scope.

crypto_key is gone. It keyed the pre-shared transport cipher that v3 Noise replaced, and hubs no longer issue it. An SDK should accept the field where old identity files still carry it, and ignore it.

SDKs should load ~/.config/thalovant/config.yaml by default for local development, honor XDG_CONFIG_HOME, support named profiles, and reject group-readable or world-readable config files on Linux and macOS.

Every SDK should expose these concepts:

Field Meaning
text, utterances Collected answer text and utterances, which can include SSML.
display_text Text with markup removed for visual display.
display_items() Structured UI items such as choices, tables, images, and attachments.
session_id Session used by the request or returned by the hub.
request_id Request trace key when provided or generated.
handled, ok, failure_event Completion state and any recorded runtime failure. A request can also raise an error before returning a reply.
events Collected events with names, data, and context for advanced integrations and policy handling.

The table uses Python names; each language follows its own naming conventions. For example, Node exposes displayText, displayItems(), and failureEvent. These are concepts shared by the SDKs, not a promise of speech, policy_denied, or raw_event properties. See Events and Rich Output for a working example.

Rule Why it matters
Unknown identity fields should be ignored. New fields must not break older clients.
Unknown display items should be ignored. New UI outputs must not break older apps.
Missing optional metadata should use defaults. Small clients should not need every field.
Method names should feel native. Every SDK, from Python through C#, should match its language norms.
Error objects should include context. Callers need endpoint, status, request ID, and policy details when available.
Runtime protocol defaults should be predictable. Automatic selection helpers prefer WSS, then HTTPS, then MQTT when broker credentials exist. Go NewClient(identity) and Rust Client::new(identity) retain an HTTPS default; use their options or auto-selection helpers to choose another transport.
MQTT requires client-scoped credentials. Broker access must stay limited to one client and its topics.
Area Requirement
API tokens Send only as authorization headers. Do not put tokens in URLs or diagnostic messages.
API destinations Refuse redirects and embedded URL credentials. Authenticated requests and bodies require HTTPS, with HTTP restricted to explicit localhost, 127.0.0.1, or [::1] development endpoints. A custom HTTP client must preserve these restrictions.
Identity output Redact passwords, broker passwords, and crypto keys by default.
Local config Require owner-only permissions for ~/.config/thalovant/config.yaml on Linux and macOS.
TLS Honor mqtt.tls: true even when the endpoint scheme is not already mqtts://.
Public endpoints Prefer https://, wss://, and mqtts:// in examples and generated identities.
MQTT topics Use returned client topics or derive topics inside the client scope only.
Timeouts Ask, Query, and event waits share a caller budget across connection and applicable send/wait work. Inventory uses per-query or phase budgets. Retain ownership while unfinished cleanup runs; never replay an ambiguous write.
Rule Why it matters
Observable SDK behavior changes require a version bump. CI publishes existing versions, it does not invent new ones.
Tests cover protocol selection. Every SDK must reject unavailable protocols clearly.
Tests cover secret redaction. SDKs must not leak credentials in normal summaries.
Docs update with new features. Users should not need source code to discover supported behavior.
Capability Python Node Go Rust
Ask ask ask Ask ask
Context build_client_context buildClientContext BuildClientContext build_client_context
Action send_action sendAction SendAction send_action
Code send_code sendCode SendCode send_code
Protocol check supports_protocol supportsProtocol SupportsProtocol supports_protocol
MFA sign-in login(otp_code=, recovery_code=) login(..., { otpCode, recoveryCode }) LoginWithOptions login_with_options
Device login login_with_browser loginWithBrowser LoginWithBrowser login_with_browser
Get operation get_operation getOperation GetOperation get_operation
List marketplace skills list_marketplace_skills listMarketplaceSkills ListMarketplaceSkills list_marketplace_skills
Create hub create_hub createHub CreateHub create_hub
Update hub update_hub(..., etag=) updateHub(..., { etag }) UpdateHub(..., etag) update_hub(..., etag)
Create runtime group create_runtime_group createRuntimeGroup CreateRuntimeGroup create_runtime_group
Install skill install_runtime_group_skill installRuntimeGroupSkill InstallRuntimeGroupSkill install_runtime_group_skill
Release hub release_hub releaseHub ReleaseHub release_hub
List hub skills list_hub_skills listHubSkills ListHubSkills list_hub_skills
Install hub skill install_hub_skill(..., version=, wait=) installHubSkill(..., { version, wait }) InstallHubSkill install_hub_skill
Update hub skill update_hub_skill(..., version=, wait=) updateHubSkill(..., { version, wait }) UpdateHubSkill update_hub_skill
Remove hub skill remove_hub_skill(..., wait=) removeHubSkill(..., { wait }) RemoveHubSkill remove_hub_skill
Intent inventory intents intents Intents intents
Inventory with fallback capabilities intents intents IntentsWithCapabilities intents_with_capabilities
Fallback handlers Inventory fallbacks / fallbacks_known Inventory fallbacks / fallbacksKnown ListFallbacks list_fallbacks
List intents list_intents listIntents ListIntents list_intents
Describe intent describe_intent describeIntent DescribeIntent describe_intent
Policy refusal ThalovantPolicyDeniedError ThalovantPolicyDeniedError PolicyDeniedError ThalovantError::PolicyDenied

Kotlin, Swift, and C# follow the same native-naming rule: login(otpCode = ...), loginWithBrowser, getOperation in Kotlin; login(otpCode:), loginWithBrowser(options:), getOperation(id:) in Swift; LoginAsync(otpCode: ...), LoginWithBrowserAsync, GetOperationAsync in C#. Their SDK pages show the full surface.

The provisioning rows follow the same rule: createHub and updateHub(hubId, payload, etag) in Kotlin; createHub(_:idempotencyKey:) and updateHub(_:_:etag:) in Swift; CreateHubAsync and UpdateHubAsync(hubId, options, etag) in C#. The hub skill rows arrive in Python SDK 0.5.15 and Node SDK 0.3.15; Go, Rust, Kotlin, Swift, and .NET will follow the same naming rule when they add them.

The intent rows follow it too: intents, listIntents, describeIntent, and ThalovantPolicyDeniedException in Kotlin; intents(languages:), listIntents(lang:), describeIntent(skillId:intentName:lang:), and ThalovantPolicyDeniedError in Swift; IntentsAsync, ListIntentsAsync, DescribeIntentAsync, and ThalovantPolicyDeniedException in C#. Every one of those is published; see the SDKs page for the version each name arrived in.

Last reviewed: September 13, 2026. Review this page when SDK contracts, transport readiness, Noise negotiation, or persistent state requirements change.

History methods follow native naming: Python/Rust list_hub_skill_history, Node/Kotlin/Swift listHubSkillHistory, Go ListHubSkillHistory, and .NET ListHubSkillHistoryAsync.