Skip to content
Console

Swift SDK

Use the Swift SDK when your client runs on iOS, macOS, or Swift on Linux.

The package is resolved with Swift Package Manager from the public GitHub repository. Swift 5.9 or newer is required, on iOS 15 or newer, macOS 12 or newer, or Linux with Foundation networking. The SDK has no third-party dependencies.

Source and releases: thalovant-swift-sdk on GitHub.

Add the package to your Package.swift and depend on the ThalovantSDK product:

dependencies: [
.package(url: "https://github.com/thalovant/thalovant-swift-sdk", from: "0.1.9"),
]

This flow signs in, creates a client identity, and sends one request over WSS.

import ThalovantSDK
let api = ThalovantControlPlane()
try await api.login(email: "[email protected]", password: "password")
let result = try await api.createClientIdentity(
hubId: "hub-id",
options: CreateClientIdentityOptions(name: "swift-demo-client")
)
let client = try ThalovantClient(identity: result.identity)
do {
try await client.connect()
let reply = try await client.ask("Tell me a short clean joke.")
print(reply.text)
await client.close()
} catch {
await client.close()
throw error
}

ThalovantControlPlane() uses https://api.thalovant.com by default.

Accounts with multi-factor authentication enabled must include a TOTP code or a one-time recovery code with the login. Without one, the API rejects the sign-in with HTTP 401 and code mfa_required, surfaced as ThalovantApiError.errorCode.

try await api.login(email: "[email protected]", password: "password", otpCode: "123456")
// Or use a one-time recovery code instead:
try await api.login(email: "[email protected]", password: "password", recoveryCode: "abcd-efgh-ijkl")

The otpCode and recoveryCode values are sent only when provided.

Device login asks your browser to approve the sign-in, so apps and tools never handle your password. The SDK prints a short code and the address https://dash.thalovant.com/activate, opens that page when it can, and waits while you approve the request in the dashboard with your normal sign-in, including Google sign-in or MFA. On approval the SDK holds a scoped, revocable API token, returned as a typed DeviceLoginResult.

Device login needs SDK 0.1.1 or newer. Approving the request needs a paid workspace plan; a free plan gets HTTP 402.

let token = try await api.loginWithBrowser(options: DeviceLoginOptions(
scopes: ["hubs:read", "clients:write"],
clientName: "my-macbook"
))
print(token.tokenId ?? "", token.scopes)

Manage the resulting token on the dashboard’s API Tokens page.

Pass a stored API token when the process should start authenticated, such as CI jobs and services. Mint one on the API Tokens page or with loginWithBrowser; the page shows scopes, expiry, and last use, and can revoke the token at any time.

let api = ThalovantControlPlane(accessToken: ProcessInfo.processInfo.environment["THALOVANT_API_TOKEN"])

ThalovantClient connects to the hub over WSS only in this release. Constructing it with hubProtocol: .https or .mqtt throws ThalovantUnsupportedProtocolError. Endpoint selection still prefers wss, then https, then mqtt, so identities created for multi-protocol hubs keep working.

Use the Python, Node.js, Go, or Rust SDK when the client needs the HTTPS or MQTT data plane today.

Identities can be built from JSON or loaded from a JSON file. The file must not be group- or world-readable; run chmod 600 <path> first.

let identity = try ThalovantIdentity.fromFile("/path/to/identity.json")
let client = try ThalovantClient(identity: identity)

The identity document uses the same snake_case fields the API returns, including access_key, password, site_id, and the optional data_plane_endpoints, protocols, and mqtt sections. A crypto_key in an older identity file is still parsed and ignored.

Mutating control-plane commands return durable operations. Poll them with the typed helper:

let operation = try await api.getOperation(id: "operation-id")
print(operation.status) // .requested, .committed, .applied, .ready, .failed, .timedOut

See Operations for lifecycle and retry guidance.

let subscription = client.on("speak") { event in
print(event.displayText)
}
// Later, when the listener is no longer needed:
subscription.close()

A connected client can list every intent its hub answers, per language, with the sentences a person says to reach each one, over its own session and with no control-plane token. See What Can My Hub Be Asked? for what the hub returns, which message types the connection must be allowed to publish, and what a refusal or a failed listing looks like.

Since SDK 0.1.8 the calls are client.intents(languages:options:), which returns the inventory grouped by skill, client.listIntents(lang:options:), which returns the registration rows, and client.describeIntent(skillId:intentName:lang:options:), which returns the definitions behind one intent.

let client = try ThalovantClient.fromIdentityFile("_identity.json")
do {
let inventory = try await client.intents(languages: ["en-us", "fr-fr"])
print(inventory.source, inventory.languages)
for skill in inventory.skills {
for intent in skill.intents {
print(intent.id, intent.engine, intent.examples(lang: "fr-fr"))
}
}
} catch let denied as ThalovantPolicyDeniedError {
print("refused:", denied.deniedType, "allowed:", denied.allowed)
}
await client.close()

intents(languages:) asks ovos.intent.list once per language, then fills in the sentences the listing did not carry, in batches of defaultDescribeBatchSize requests so a hub with many intents is never sent the whole burst at once. IntentInventoryOptions carries the deadline and the switches: describe: false stops at names, engines, and enabled state. examples(lang:limit:) prefers whole sentences to ones with a {slot}, then shorter ones; phrasesFor(_:) returns all of them, and the inventory is Codable, so asJSON() writes the same document the other SDKs write. Language tags compare without regard to case or _ and -, so fr-fr and fr_FR are one request.

A hub that refuses a query throws ThalovantPolicyDeniedError at once with deniedType, code, reason, and allowed. With the default IntentInventoryOptions(fallback: true), a refused ovos.intent.list returns names only instead: inventory.source is .engineManifests, inventory.denied names the refused query, and inventory.hasPhrases is false, so check that flag before promising sentences. A hub that accepts the listing and answers it with ok: false is a different case: since 0.1.9 that throws ThalovantRuntimeError carrying the hub’s own error text, because a failed listing is not an empty hub. describeIntent keeps returning an empty list for the same answer, which is a real one: the hub does not know that registration.

ask

Send one request and receive a normalized reply.

on

Observe hub bus events by name with a closeable subscription.

getOperation

Poll a durable control-plane command with a typed status.

listPublicHubs

Discover public hubs before sign-in.

For cross-language recipes, see SDK Functions.

Since SDK 0.1.3 the control plane can create hubs, runtime groups, and skill installs. Browsing the catalog with listMarketplaceSkills() needs hubs:read and works on any plan. createHub, createRuntimeGroup, installRuntimeGroupSkill, releaseHub, and releaseRuntimeGroup need hubs:write and a paid plan.

let group = try await api.createRuntimeGroup(["name": "kiosks"])
let hub = try await api.createHub([
"name": "joke-garden",
"runtimeGroupId": .string(group["id"]?.stringValue ?? ""),
"spec": .object([:]),
])
let current = try await api.getHub(hub["id"]?.stringValue ?? "")
_ = try await api.updateHub(
current["id"]?.stringValue ?? "",
["active": false],
etag: current["etag"]?.stringValue ?? "",
)

updateHub and deleteHub take the hub’s current etag as a required argument. See Provision Hubs for the full flow, the immutable fields, and the error table.

Symptom Check
ThalovantApiError with Missing Thalovant API access token Call api.login(...) or api.loginWithBrowser(...) before private API actions, or pass accessToken: to ThalovantControlPlane.
HTTP 401 with errorCode mfa_required Pass otpCode or recoveryCode to api.login(...).
API access requires a paid plan Upgrade the workspace before provisioning private resources through the API.
ThalovantUnsupportedProtocolError The hub does not expose WSS, or the code requested https or mqtt, which this release does not connect over.
ThalovantIdentityError The identity file is malformed or readable by other users.