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
// uses a random, persisted app-install id as an opaque anonymous id.
// It never uses IDFV or another hardware identifier.
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)
Retained chat continuity
Restore is owned by the SDK's session manager. On foreground/bootstrap, call
the bounded passive restore once; it attaches only rows the server reports as
active and never takes over another installation:
let report = try client.restoreActiveChats()
for result in report where result.status == .activeElsewhere {
// Keep the row view-only until the user explicitly opens it.
}
// History can render cache immediately while the authoritative refresh runs.
for try await update in try client.sessionUpdates(id: sessionId) {
if case .snapshot(let snapshot) = update {
render(snapshot.session.history)
}
}
// Named authority keeps passive work from accidentally taking over.
_ = try client.openChat(sessionId: sessionId, intent: .explicitNavigation)
The host owns app lifecycle triggers; it must not create a second restore loop or attach the same id independently. An ended chat remains view-only until the ordinary first-send reopen path runs.
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
Upload a file, then attach the returned Attachment to your next
message. There is no sessionId: — attachments are scoped to the
widget the client was created for, so an attachment can be the first
thing a visitor sends, before any session exists. 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(
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(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 {
// Required only when a Notification Service Extension shares the
// endpoint-generation gate with the app.
OrigonClient.configurePushNotifications(
appGroupIdentifier: "group.com.example.app"
)
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:
try client.unregisterPushNotificationsForLogout() // completes before release
The static OrigonClient.unregisterForPushNotifications() remains the
fire-and-forget convenience. Use the instance method above when logout will
immediately release the client.
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.
Each successful registration persists the opaque server generation. A
Notification Service Extension must compare the incoming
endpointGeneration before showing the optional human-agent title or
preview; use
OrigonPushNotification.contentForDelivery as shown in
examples/notification-service-extension/NotificationService.swift. On an
exact match, the helper copies each present, nonblank field over the APNs alert.
An absent or blank title keeps the APNs title. A stale or missing generation
keeps the original provider/APNs title and body unchanged—the SDK never invents
fallback branding. Foreground delegates may instead suppress the notification
by returning no presentation options when
OrigonPushNotification.isCurrent(...) is false. On a matching notification
tap, read OrigonPushNotification.sessionId(...) and call
openChat(sessionId:intent: .notification).
Call registration on every APNs token refresh. On logout, unregister before discarding the client and clear delivered notifications in the host app. An uninstall cannot call logout; APNs invalid-token feedback and the server's 90-day endpoint TTL perform eventual cleanup.
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). |
restoreActiveChats() |
Passively attach all retained active chats and return per-id outcomes. |
openChat(sessionId:intent:) |
Open a retained chat with named passive, navigation, or notification authority. |
sessionUpdates(id:policy:) |
Finite AsyncThrowingStream: optional cache snapshot, one network snapshot or typed refresh failure, then completion. |
sessionDirectoryUpdates(policy:) |
Cache-first finite directory stream with the same ordering. |
cachedSession(s) / refreshSession(s) |
Explicit cache-only and authoritative network snapshots. |
removeCachedSession(id:) / clearChatCache() / pruneChatCache() |
Explicit cache maintenance. |
close() / OrigonClient.clearAllChatCaches() |
Close joins native loaders and cache writers; after all clients close, atomically clear every cached scope. |
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(path:|data:|url:) |
async; upload a file (path: / data: / url: overloads) against the client's widget and return the server-issued Attachment. No session required. Reports progress via onProgress. |
deleteAttachment(attachmentId:) |
async; cancel an in-flight upload (pass the uploadId) or delete a completed attachment (pass attachment.id). No session required. |
activeSessions() |
Snapshot of every active 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, and default-on chatCachePolicy. The protected cache root is fixed under Application Support and excluded from backup. |
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. |
SessionLoadPolicy, SessionSnapshot, SessionDirectorySnapshot, load updates |
Typed cache/network source, authority, refresh time, snapshots, and refresh failures. |
Contact, SessionSummary, SessionHistory |
typed directory/transcript shapes carried by snapshots. |
SendMessagePayload |
text, html, attachments (input shape for sendMessage(id:payload:)). |
OrigonError |
structured error with kind, statusCode, code, message. |
License
Proprietary. All rights reserved.
