About
The Origon SDK for Apple platforms lets you embed Origon directly in your iOS and macOS apps: audio calls, chat, and session history.
A basic chat + voice integration takes around 15 minutes; allow a little longer if you also wire up push notifications or background calls. The SDK authenticates your app by its Bundle ID, which you register once in the Origon Connect web app (see Prerequisites). At runtime all you pass is your Origon endpoint.
Features
- Audio calls — low-latency voice, with automatic Bluetooth device routing.
- Chat — messaging with typing indicators and attachments.
- Push notifications — wake your app for incoming calls and messages.
- Session history — retrieve past sessions and their messages.
Requirements
- iOS 15.0+
- macOS 13.0+
- Xcode 15+
- Swift 5.9+
Prerequisites
Register your app in the Origon Connect web app before the SDK can connect. Your account owner or admin has access to it. The SDK authenticates each app by the Bundle ID it reports, so that Bundle ID has to be on your tenant's allow-list first.
- Sign in to Origon Connect at https://origon.ai/connect.
- Go to Settings → Integrations → Mobile → Setup Mobile SDK.
- Fill in your app details — Company Name, logo, and the routing rules for your flow (where calls and chats are sent). Press Next.
- In the Deployment tab, add your app's Bundle ID (e.g.
com.domain.YourApp) to the Bundle IDs field. It accepts multiple entries, so you can register several apps (e.g. staging and production) against the same config. To run and test the sample app, add its Bundle IDorigon.example.ioshere as well. - Copy the endpoint shown in the Deployment tab and pass it as the
endpointinClientConfigwhen you initializeOrigonClient(see Quick Start). - Save the config.
Your Bundle ID is the target's Bundle Identifier under Xcode's General → Identity; it must match exactly what the app reports at runtime.
Installation
Add the package to your Package.swift or through Xcode's package manager:
dependencies: [
.package(url: "https://github.com/Origon/apple-sdk", from: "0.1.0"),
]
Then add OrigonSDK to your target's dependencies:
.target(
name: "YourApp",
dependencies: [
.product(name: "OrigonSDK", package: "apple-sdk"),
]
),
The pre-built COrigonSDK XCFramework is downloaded automatically by SPM
from GitHub Releases.
Sample app
You'll find the Origon SDK for Apple on GitHub
here. The repo also includes a
runnable sample app — a minimal iOS app that integrates chat and voice
calls — under
examples/origon-sdk-example-ios.
See its README
for build and run instructions, plus a guide to which files to read first
when wiring the SDK into your own app.
Host App Configuration
iOS reads permission and capability declarations only from the main
bundle's Info.plist — keys placed inside an embedded framework are
ignored at runtime. The following entries must be added by the
integrating app.
Required: microphone permission
Voice sessions request audio recording via AVAudioSession. Without
this key the app crashes the first time a call starts.
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required for voice calls.</string>
Optional: background voice calls
If calls must continue while the app is backgrounded (e.g. the user
locks the screen mid-call), declare the audio background mode:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
If you integrate with CallKit / PushKit for system call UI, also add
voip to the same array.
Optional: push notifications
To receive push notifications, enable the Push Notifications
capability on the app target (adds the aps-environment entitlement)
and add the remote-notification background mode if you handle silent
pushes:
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
The host app owns APNs registration (requesting authorization and
calling registerForRemoteNotifications()); the SDK only needs the
resulting device token. See Push notifications.
Quick Start
import OrigonSDK
// Optional: install Rust-side logging once at app launch.
OrigonClient.initLogging()
// 1. Create the client. `userId` is optional — when omitted, the SDK
// falls back to the device identifier so anonymous users still get a
// stable identity.
let client = try OrigonClient(config: ClientConfig(
endpoint: "https://origon.ai/chat/api/<id>",
userId: "user-123"
))
// 2. Start a voice session.
let response = try client.startSession(
StartSessionOptions(channel: .voice)
)
print("session \(response.sessionId) dialing \(response.url)")
// 3. Drain the event stream.
while true {
guard let event = client.pollEvent() else {
try await Task.sleep(nanoseconds: 50_000_000) // 50ms
continue
}
switch event {
case .connected: print("connected")
case .peerAttached(_, let peerId, _): print("peer \(peerId)")
case .audioRouteChanged(_, let route): speakerOn = route == .speaker
case .disconnected(_, let reason): print("disconnected: \(reason)"); return
default: break
}
}
Voice controls
// Mute (per session).
try client.setMute(id: response.sessionId, muted: true)
// Audio output route — process-global, so no session id. Maps onto
// `AVAudioSession.overrideOutputAudioPort`; the SDK re-asserts the choice
// across reconnects and OS route changes (headset plug/unplug). Resets to
// `.automatic` on each new call.
try client.setAudioOutput(.speaker) // force the loudspeaker
try client.setAudioOutput(.automatic) // back to the default route (receiver / wired / Bluetooth)
A speaker toggle is typically client.setAudioOutput(on ? .speaker : .automatic).
Multiple sessions
let active = try client.activeSessions()
try client.setMuteAll(muted: true)
try client.endAllSessions()
Joining a pre-obtained session
try client.joinSession(JoinSessionInput(
channel: .voice,
sessionId: "...",
url: "...",
token: "..."
))
Chat
sendMessage, notifyTyping, and stopTyping all require an active
chat session. Call startSession(channel: .chat, ...) first —
otherwise these throw OrigonError(kind: .noSession). The same
applies after endSession(id:).
// Outbound send. The SDK fires `.messageAdded` (status `.sending`)
// before the wire round-trip and `.messageUpdated` (delivered or
// failed) after — both surface on `pollEvent()`. The return value is
// the server-issued Message.
let msg = try client.sendMessage(
id: sessionId,
payload: SendMessagePayload(text: "hello", html: "hello")
)
// Typing — call per keystroke. The SDK debounces; only one outbound
// `{state: "on"}` fires per typing burst, and a `{state: "off"}` is
// auto-emitted after ~3 s of no further calls. Fire `stopTyping(id:)`
// explicitly when the input clears (e.g. user deleted all text) to
// snap the peer's "typing…" indicator off instantly.
try client.notifyTyping(id: sessionId)
try client.stopTyping(id: sessionId)
Polling chat events:
while let event = client.pollEvent() {
switch event {
case .messageAdded(let sid, let message):
// Store under message.localId ?? message.id
case .messageUpdated(let sid, let id, let message):
// Look up the row by id (matches the original localId / message.id)
case .typing(let sid, let isTyping):
// Show / hide "typing…" indicator
default: break
}
}
Attachments
Chat only. Upload a file, then attach the returned Attachment to your
next message. uploadAttachment is async with overloads for a
filesystem path, Data, or a URL (the url: overload manages
startAccessingSecurityScopedResource() for UIDocumentPicker URLs):
let attachment = try await client.uploadAttachment(
sessionId: sessionId,
url: pickedURL,
fileName: "photo.jpg"
) { progress in
// progress.percent is nil when the total size is unknown
updateProgressBar(progress.percent)
}
try client.sendMessage(
id: sessionId,
payload: SendMessagePayload(attachments: [attachment])
)
// Cancel an in-flight upload (pass the uploadId) or delete a completed
// one (pass attachment.id) — the SDK works out which.
try await client.deleteAttachment(sessionId: sessionId, attachmentId: attachment.id)
Uploads are prechecked against the tenant's attachmentPolicy (type and
size); a disallowed file throws OrigonError before any bytes are sent.
Push notifications
Register this device's APNs token so the backend can deliver push
notifications. The host app owns token acquisition — request
authorization and call registerForRemoteNotifications(), then forward
the device token to the SDK from your UIApplicationDelegate:
import OrigonSDK
import UserNotifications
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
guard granted else { return }
DispatchQueue.main.async { application.registerForRemoteNotifications() }
}
return true
}
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
// Forward the raw token — the SDK hex-encodes it for the wire.
OrigonClient.registerForPushNotifications(deviceToken: deviceToken)
}
}
// On logout:
OrigonClient.unregisterForPushNotifications()
registerForPushNotifications(deviceToken:environment:) is a static
method and is safe to call before the client is initialized — the
token is buffered and sent automatically once OrigonClient is created.
It is also safe to call repeatedly (e.g. on APNs token refresh); the
latest token wins. The call returns immediately and runs the network
request in the background; failures are logged, not thrown.
APNs environment. A device token is bound to the environment of the build that produced it (development builds → sandbox; App Store / TestFlight → production), and the backend must target the matching APNs host. The SDK auto-detects this from the app's embedded provisioning profile, so you normally pass nothing. Override only if detection is wrong:
OrigonClient.registerForPushNotifications(deviceToken: deviceToken, environment: .sandbox)
API Reference
OrigonClient
| Method | Description |
|---|---|
init(config:) |
Create a client. Throws OrigonError on connect failure. |
pollEvent() |
Non-blocking poll. Returns nil when idle. |
startSession(_:) |
Open a session. Returns (sessionId, url, token). |
joinSession(_:) |
Attach to a previously-obtained StartSessionResponse. |
endSession(_:) / endAllSessions() |
Close a single / every session. |
setMute(id:muted:) / setMuteAll(muted:) |
Voice — absolute mute. |
setAudioOutput(_:) |
Voice — override the audio output route (.speaker / .automatic / .bluetooth). Process-global; survives reconnects. |
sendMessage(id:payload:) |
Chat — POST <sessionUrl>/message. Returns the server-issued Message. Fires .messageAdded then .messageUpdated. |
notifyTyping(id:) |
Chat — register a keystroke; SDK debounces outbound /typing POSTs. |
stopTyping(id:) |
Chat — force outbound typing state to "off" immediately. |
uploadAttachment(sessionId:…) |
Chat — async; upload a file (path: / data: / url: overloads) and return the server-issued Attachment. Reports progress via onProgress. |
deleteAttachment(sessionId:attachmentId:) |
Chat — async; cancel an in-flight upload (pass the uploadId) or delete a completed attachment (pass attachment.id). |
activeSessions() |
Snapshot of every active session. |
getSessions() |
GET /sessions — list prior sessions for the configured userId. |
getSession(id:) |
GET /session/<id> — transcript for one session. |
setAttributes(_:) |
Replace session-level attributes injected as data.attributes on startSession. |
OrigonClient.registerForPushNotifications(deviceToken:environment:) |
Static. Register an APNs device token (buffered until init; auto-detects environment). |
OrigonClient.unregisterForPushNotifications() |
Static. Remove this device's push registration (e.g. on logout). |
startMessage / isChatEnabled / isCallEnabled / multipleChannels / attachmentPolicy / serverConfig |
Cached /config getters. |
OrigonClient.initLogging(filter:) |
Install Rust-side tracing subscriber. |
Types
| Type | Description |
|---|---|
ClientConfig |
endpoint, optional token, optional userId, attributes ([String: Any]?). The app is authenticated by its bundle identifier, resolved automatically from Bundle.main.bundleIdentifier and sent as X-Bundle-Id on every HTTPS call (register it first — see Prerequisites). token is an optional auth token. userId defaults to the device identifier (identifierForVendor) when omitted. |
APNSEnvironment |
.sandbox, .production. Optional override for registerForPushNotifications(deviceToken:environment:); auto-detected from the provisioning profile when omitted. |
Channel |
.chat, .voice. |
SessionControl |
.ai, .user. |
MessageRole |
.ai, .external, .user, .system. |
MessageStatus |
.sending, .delivered, .failed. |
MessageState |
.streaming, .completed. |
AudioOutputRoute |
.automatic (default route — receiver / wired / Bluetooth), .speaker (loudspeaker), .bluetooth (on iOS, resolved via .automatic — the active session already routes to a connected HFP device). Argument to setAudioOutput(_:). |
StartSessionOptions |
channel, optional sessionId, optional data (raw JSON). |
StartSessionResponse |
sessionId, url, token. |
JoinSessionInput |
channel, sessionId, url, token. |
ActiveSession |
sessionId, channel. |
AttachmentRule / AttachmentPolicy |
tenant policy for attachments. |
ServerConfig |
full /config snapshot. |
DisconnectReason |
structured disconnect reasons (incl. .serverClosed(code:detail:)). |
ClientEvent |
.messageAdded, .messageUpdated, .connected, .reconnecting, .reconnected, .peerAttached, .peerDetached, .disconnected, .callError, .audioRouteChanged, .controlUpdated, .typing, .sessionUpdated. Every case carries sessionId. .audioRouteChanged carries the now-current AudioOutputRoute (drive a speaker toggle from route == .speaker); it fires on OS-driven route changes (headset plug/unplug) as well as your own setAudioOutput. |
Message |
typed transcript line. Carries id, localId, role, text, html, userId, userName, timestamp, attachments, errorText, status, state. |
Attachment |
uploaded-media descriptor: id, name, contentType, url, and an optional client-side localUrl preview (kept on the local Message, stripped from the wire). Returned by uploadAttachment(...), carried on Message.attachments, and passed back into SendMessagePayload.attachments. |
UploadProgress |
bytesUploaded, optional totalBytes, optional percent (both nil when the transport reports no content length). Passed to the uploadAttachment onProgress callback. |
Contact, SessionSummary, SessionHistory |
typed shapes returned by getSessions() / getSession(id:). |
SendMessagePayload |
text, html, attachments (input shape for sendMessage(id:payload:)). |
OrigonError |
structured error with kind, statusCode, code, message. |
License
Proprietary. All rights reserved.
