> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Swift SDK: Orbit quickstart for iOS

> Official Orbit Swift SDK quickstart — in-app chat, in-app messages and content cards, video rooms, and VoIP push registration for iOS.

# Swift SDK (iOS)

The Orbit Swift SDK embeds Orbit's end-user-facing surfaces into a native iOS app (also macOS, tvOS, and watchOS): in-app chat (the same omnichannel conversation/inbox the web widget uses), owned in-app messages and content cards, video rooms, and VoIP push registration for inbound-call delivery. It ships as four Swift Package Manager libraries — `OrbitChat`, `OrbitInApp`, `OrbitVideoRoom`, and `OrbitPush` — and targets iOS 13+.

This is a **client SDK**: it authenticates only with a publishable key (`dv_live_pk_…`), because it ships inside a distributed app binary. It intentionally does not wrap the server-side management resources (messaging, voice, contacts, campaigns, verify) — those require a secret key and belong in your backend, via the [Node SDK](/sdks/node) or the REST API. See [Client / mobile SDKs (different scope, by design)](/sdks) on the SDK index.

<Note>
  **Pre-publish — source-only.** The Swift SDK is not yet in the Swift
  Package Index — the SPM install line below fails today. Until first
  publish, vendor the source from the monorepo (`packages/sdk-swift/`)
  and add the libraries as a local SPM package.
</Note>

## Installation (Swift Package Manager)

```swift theme={null}
// Package.swift
dependencies: [
    .package(url: "https://github.com/devotel/orbit-swift-sdk.git", from: "0.1.0"),
],
// per target, add only the libraries you use:
.product(name: "OrbitChat", package: "orbit-swift-sdk"),
.product(name: "OrbitInApp", package: "orbit-swift-sdk"),
.product(name: "OrbitVideoRoom", package: "orbit-swift-sdk"),
.product(name: "OrbitPush", package: "orbit-swift-sdk"),
```

In Xcode: **File ▸ Add Package Dependencies…** and paste the same repository URL.

## Headless chat client (OrbitChat)

The client ships zero UI — it owns the conversation lifecycle, history, optimistic send, and the real-time inbound stream; your UIKit or SwiftUI views render it.

```swift theme={null}
import OrbitChat

let chat = try OrbitChatClient(
    config: .init(
        publicKey: "dv_live_pk_…",      // publishable key ONLY — never a secret key
        storage: UserDefaultsStorage(), // persist the conversation across launches
        appInfo: "acme-ios/2.3.1"
    )
)

chat.onMessage { message in print("agent:", message.body) }
chat.onMessages { all in render(all) }
chat.onTyping { isTyping in setTyping(isTyping) }
chat.onConnection { status in print("status:", status) }

await chat.connect()
try await chat.sendMessage("Hi, I need help with my order")
```

Each `on…` method returns an `OrbitSubscription`; call `.cancel()` on it to stop receiving the event. Call `chat.disconnect()` to stop the real-time stream and retain the conversation id.

Send a file attachment (10 MB client-side guard):

```swift theme={null}
try await chat.sendAttachment(
    OrbitChatAttachment(
        url: localFileURL,
        name: "photo.jpg",
        mimeType: "image/jpeg",
        size: 482_133   // optional; enables the 10 MB guard
    )
)
```

## In-app messages and content cards (OrbitInApp)

Owned in-app surfaces — assembled server-side into a per-visitor feed, rendered locally, with impression / click / dismiss reported back.

```swift theme={null}
import OrbitInApp

let inApp = try OrbitInAppClient(
    config: .init(publicKey: "dv_live_pk_…")
)

let feed = await inApp.fetchFeed()
try await inApp.reportEvent(.impression, surfaceId: feed.cards[0].id)
await inApp.dismiss(surfaceId: feed.cards[0].id)
```

## Video rooms (OrbitVideoRoom)

Headless and engine-injected: your backend mints the room token server-side (`POST /api/v1/video/rooms/:id/join`) and hands the token plus server URL to the app. Wire the client over a `VideoRoomEngine` backed by a WebRTC engine (for example the Orbit Media SDK for Swift), so this package carries no hard WebRTC dependency.

```swift theme={null}
import OrbitVideoRoom

let room = OrbitVideoRoomClient(
    config: .init(token: roomToken, serverURL: roomServerURL),
    engine: myVideoRoomEngine
)

room.onTrackSubscribed { track, participant in render(track, participant) }
room.onCaption { caption in showCaption(caption) }
await room.join()

let micOn = await room.toggleMicrophone()
await room.joinBreakout(name: "support-tier-2", token: breakoutToken)
await room.leave()
```

## Inbound-call VoIP push (OrbitPush)

A regular APNs alert token cannot reliably wake a force-quit app into the native CallKit incoming-call UI, so the backend keeps a dedicated VoIP device registry delivered on Apple's PushKit (`apns-push-type: voip`) topic. `OrbitPushClient` hands the raw PushKit token to that registry without hand-rolled HTTP.

```swift theme={null}
import OrbitPush
import PushKit

final class PushDelegate: NSObject, PKPushRegistryDelegate {
    private let push = try! OrbitPushClient(
        config: .init(publicKey: "dv_live_pk_…")
    )

    func pushRegistry(
        _ registry: PKPushRegistry,
        didUpdatePushCredentials credentials: PKPushCredentials,
        for type: PKPushType
    ) {
        let hex = credentials.token.map { String(format: "%02x", $0) }.joined()
        // Re-registering the same token un-revokes it and bumps last-seen,
        // so calling on every token rollover is safe and idempotent.
        // Fire-and-forget: errors surface via onError instead of throwing.
        push.refreshToken(hex, platform: .ios)
    }
}

// On sign-out, stop the device from ringing:
try await push.unregister(deviceId: savedDeviceId)
```

## Error handling

API failures from any of the four libraries surface as typed errors carrying the HTTP status; construction misuse throws immediately.

| Error                                                    | Raised when                                                                                                                          |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `OrbitChatError.missingPublicKey` / `.secretKeyRejected` | Constructor was handed an empty key or a server-side secret key                                                                      |
| `OrbitChatError.attachmentTooLarge`                      | Attachment exceeds the 10 MB client cap                                                                                              |
| `OrbitAPIError` (struct)                                 | Any non-2xx API response; carries the HTTP status and a parsed `Retry-After` — branch on status to throttle a 429 instead of looping |
| `OrbitPushClient.OrbitPushAPIError`                      | Same shape for the push registry                                                                                                     |

```swift theme={null}
do {
    try await chat.sendMessage("Where is my order?")
} catch OrbitChatError.attachmentTooLarge {
    // compress, then retry
} catch let err as OrbitAPIError {
    print("orbit error: HTTP", err.status)
}
```

Only the **publishable key** (`dv_live_pk_…` / `dv_test_pk_…`) may be embedded in the app. The clients refuse a server-side secret key (`dv_live_sk_…` / `dv_test_sk_…`) at construction — `OrbitChatError.secretKeyRejected` — because anything bundled in a distributed binary is extractable by any user. Real-time updates use header-authenticated polling (iOS has no SSE `EventSource`), so the key never lands in a URL log.
