.d8888b. 888888b. 888 d8P
d88P Y88b 888 "88b 888 d8P
888 888 888 .88P 888 d8P
888 8888888K. 888d88K
888 888 "Y88b 8888888b
888 888 888 888 888 Y88b
Y88b d88P 888 d88P 888 Y88b
"Y8888P" 8888888P" 888 Y88b .ai
The official Swift SDK for ChatBotKit - a platform for building and deploying conversational AI applications. Use it to add streaming AI chat to iOS, macOS, tvOS, watchOS and visionOS apps, or to call the ChatBotKit API from Swift on the server and on Linux.
async/awaitAPI built onURLSession, with no third-party dependencies- Streaming replies token by token as an
AsyncThrowingStream CodableandSendablerequest and response types generated from the API specification- Short-lived session tokens, so apps never ship the API secret
- Typed errors, retries with backoff and cancellation
- Swift 6.0 or later (Xcode 16 or later). The package builds in the Swift 6 language mode with complete concurrency checking.
- iOS 15, macOS 12, tvOS 15, watchOS 8, visionOS 1, or Linux
Add the package with Swift Package Manager:
dependencies: [
.package(url: "https://github.com/chatbotkit/swift-sdk", from: "0.1.0"),
],
targets: [
.target(name: "MyApp", dependencies: [
.product(name: "ChatBotKit", package: "swift-sdk"),
]),
]In Xcode, choose File > Add Package Dependencies and enter the same URL.
import ChatBotKit
let client = ChatBotKit(token: "your-api-secret")
let conversation = try await client.conversation.create(
ConversationCreateRequest(backstory: "You are a helpful assistant.", model: "gpt-4o")
)
let reply = try await client.conversation.complete(
conversation.id,
ConversationMessageCompleteRequest(text: "Hello!")
).value
print(reply.text)Calls that can stream return a ResponsePromise. Await .value for the JSON
response, or iterate .stream() for the JSONL events as they arrive:
let stream = client.conversation.complete(
conversation.id,
ConversationMessageCompleteRequest(text: "Tell me a story")
).stream()
for try await item in stream {
switch item.type {
case .token:
print(item.data.token ?? "", terminator: "")
case .result:
print("\n\(item.data.usage?.token ?? 0) tokens")
default:
break
}
}The same works for a stateless completion, where you send the whole history and nothing is stored:
let stream = client.conversation.complete(
ConversationCompleteRequest(
messages: [ConversationCompleteRequestMessage(text: "Hi!", type: .user)],
model: "gpt-4o"
)
).stream()Breaking out of the loop or cancelling the surrounding Task cancels the
request. To handle event types the typed stream item does not describe yet,
stream them as StreamEvent, which keeps the raw type and data:
for try await event in client.conversation.receive(conversation.id).stream(as: StreamEvent.self) {
print(event.type, event.data)
}list calls stream too, which is the easiest way to walk through every item:
for try await item in client.bot.list().stream() {
print(item.data.id, item.data.name ?? "")
}Never put your API secret in an app: anyone can extract it from the binary. Keep it on your server and give the app a short-lived session token for one conversation instead.
On your server (Swift, Node, Python or Go - any SDK works):
let server = ChatBotKit(token: ProcessInfo.processInfo.environment["CHATBOTKIT_API_SECRET"]!)
let session = try await server.bot.session.create("bot-id")
// return session.conversationID, session.token and session.expiresAt to the appor, for a conversation that already exists:
let session = try await server.conversation.session.create(
conversationId,
ConversationSessionCreateRequest(durationInSeconds: 3600)
)In the app, use only the token:
let client = ChatBotKit(token: sessionToken)
for try await item in client.conversation.complete(conversationId, ConversationMessageCompleteRequest(text: "Hi")).stream() {
// ...
}A conversation session token can call complete, send and receive,
upload attachments and vote on messages of that one conversation. Everything
else, such as listing conversations or reading the message history, goes
through your server. Tokens expire (one hour by default, up to a day), so ask
your server for a new one before expiresAt.
let client = ChatBotKit(
options: ChatBotKitOptions(
token: "your-api-secret",
baseURL: URL(string: "https://api.chatbotkit.com")!,
runAsUserId: "user-id",
timezone: "Europe/London",
headers: ["X-App-Version": "1.0"],
timeout: 60,
retries: 3,
retryDelay: 0.25,
session: .shared
)
)| Option | Description |
|---|---|
token |
API secret (server) or session token (app). secret is an alias. |
baseURL |
API base URL, e.g. http://localhost:3000 for a self-hosted platform. |
host |
Replaces the host of baseURL. |
runAsUserId |
Run requests as a sub-user. |
runAsChildUserEmail |
Run requests as a child user (experimental). |
timezone |
Sent as X-Timezone. |
headers |
Extra headers for every request. |
timeout |
Request timeout in seconds. Defaults to the URLSession setting. |
retries |
Retries after network errors and 429/5xx responses (default 3). |
retryDelay |
Delay before the first retry in seconds; doubles each time. |
retryTimeout |
Whether timed out requests are retried (default true). |
session |
The URLSession to use. |
client.extend { $0.runAsUserId = "other-user" } returns a new client with
changed options and leaves the original alone.
ChatBotKit, every resource client and ChatBotKitOptions are Sendable
and only hold immutable state, so one client can be created once and shared
between tasks, view models and actors of your app. The SDK itself uses no
actors and no global mutable state: each call is an async function on the
caller's task, and cancelling that task cancels the request.
The client mirrors the structure of the Node SDK:
client.bot // list, fetch, create, update, delete, upvote, downvote
client.bot.session // create (end-user session for a bot)
client.conversation // list, fetch, create, update, delete, complete, dispatch,
// send, receive, upvote, downvote
client.conversation.message // list, fetch, create, update, delete, upvote, downvote
client.conversation.session // create (conversation session token)
client.conversation.attachment // list, upload
client.dataset // list, fetch, create, update, delete, search
client.dataset.record // list, export, fetch, create, update, delete
client.dataset.file // list, attach, detach, sync
client.skillset // list, fetch, create, update, delete
client.skillset.ability // list, export, fetch, create, update, delete, execute
client.file // list, fetch, create, update, delete, upload, download,
// downloadData, sync
client.contact // list, fetch, create, update, delete, ensure
client.contact.conversation // list
client.user // list, fetch, create, update, delete
client.user.session // create
client.platform.model // listEvery resource client can also be used on its own, for example
ConversationClient(options: options). Resource clients share one
ChatBotKitClient that holds the options and performs the requests
(client.conversation.client).
Any other endpoint can be called through the base client while it is not wrapped yet:
let response: BlueprintListResponse = try await client.fetch(
ClientRequest(path: "/api/v1/blueprint/list", query: BlueprintListParams(take: 10))
)| Node SDK client | Swift |
|---|---|
bot, bot.session |
Yes |
conversation, .message, .session, .attachment |
Yes |
dataset, .record, .file |
Yes |
skillset, .ability |
Yes |
file |
Yes |
contact, contact.conversation |
Yes |
contact.secret, .space, .task |
Not yet (use client.fetch) |
user, user.session |
Yes |
user.context, .token |
Not yet (use client.fetch) |
platform.model |
Yes |
other platform catalogues |
Not yet (use client.fetch) |
blueprint, channel, computer, decision, event, graphql, integration, magic, memory, policy, portal, secret, space, task, team, usage |
Not yet (use client.fetch) |
let file = try await client.file.create(FileCreateRequest(name: "handbook.pdf"))
try await client.file.upload(file.id, data: pdfData, name: "handbook.pdf", type: "application/pdf")Attachments for a conversation work the same way, with a session token too:
try await client.conversation.attachment.upload(conversationId, data: imageData, name: "photo.jpg", type: "image/jpeg")The API returns a pre-signed upload request and the bytes go straight to storage.
| Error | When |
|---|---|
FetchError |
Non-2xx response. code (e.g. NOT_FOUND), message, status, data. |
AuthorizationRequiredError |
A secret or connection needs authorizing first; url is where to go. |
TimeoutError |
The request timed out. |
CancellationError |
The task was cancelled. |
DecodingError |
The response did not match the expected type. |
do {
_ = try await client.conversation.fetch("missing")
} catch let error as FetchError where error.code == "NOT_FOUND" {
print("No such conversation")
}All request and response types in Types.swift are generated from the API
specification. Values the API may extend later, such as message and event
types, are open structs with static members (.user, .token), so a new value
from the server decodes instead of failing. Free-form JSON uses JSONValue.
Regenerate the types from the monorepo root after the API changes:
pnpm script:sync-types:swiftexamples/cli- a terminal chat that creates a conversation and streams replies. Runs on macOS and Linux:CHATBOTKIT_API_SECRET=... swift run --package-path examples/cliexamples/swiftui-chat- a complete SwiftUI app for iOS and macOS with a conversation list, streaming chat, attachments and session-token login through a small backend.
swift build
swift testThe tests use a mocked URLProtocol, so they need no network or API key.
Versions are published as Git tags - for a Swift package, the tag is the
release. The version is driven by the VERSION file:
- Bump
VERSION(semver, novprefix - e.g.0.2.0) and add a section toCHANGELOG.mdin a pull request. - Merge to
main. TheTag Releaseworkflow readsVERSIONand, if the matchingvX.Y.Ztag does not yet exist, creates and pushes it, then triggers theReleaseworkflow to publish GitHub release notes.
Consumers then pick up the new version through SwiftPM:
.package(url: "https://github.com/chatbotkit/swift-sdk", from: "0.2.0"),While the API is still evolving the package stays on 0.x (minor versions may
introduce breaking changes); it will move to 1.0.0 once the API is stable.
ISC. See LICENSE for details.