AppleSDK

iOS and macOS SDK for the Origon platform.

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.

  1. Sign in to Origon Connect at https://origon.ai/connect.
  2. Go to Settings → Integrations → Mobile → Setup Mobile SDK.
  3. Fill in your app details — Company Name, logo, and the routing rules for your flow (where calls and chats are sent). Press Next.
  4. 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 ID origon.example.ios here as well.
  5. Copy the endpoint shown in the Deployment tab and pass it as the endpoint in ClientConfig when you initialize OrigonClient (see Quick Start).
  6. 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.

© 2026 Origon Inc.

SOC 2 CERTIFIED
GDPR COMPLIANT
HIPAA COMPLIANT