iOS SDK Implementation

Build visitor and agent communications inside a native iOS app

This guide covers the visitor runtime and the separate native Agent runtime. Each uses its own short-lived token, state, and lifecycle.

Use the Swift SDK for native iOS CRM, support, marketplace, banking, hotel, travel, tourism, or customer apps. It includes a SwiftUI Managed UI preview for fast installs and a Headless async/await object model for custom screens while platform-specific transport details stay internal.

ios-app

Native visitor flow

One Swift API, Apple adapters where iOS needs them.

SDK token
1

App requests token

2

Backend validates user

3

Ringnity returns SDK session

Chat
AI
Voice
Video

Ringnity Support

Online now

Hi, how can we help today?
I need help with my reservation.
You can continue by chat, voice, or video.
Type a message...

Overview

What this SDK is for

Use this iOS Swift SDK for customer-facing support experiences or internal agent workspaces. Visitor and Agent runtimes remain separate.

Visitor / Client App

This page covers customer-facing iOS apps: chat, AI chat, internet voice call, video call, APNs registration, ringtone, Managed UI, and Headless UI.

Customer Backend Required

The iOS app asks the customer backend for a short-lived SDK token. The backend stores the Ringnity Server API key and validates the app user.

Agent Runtime Included

RingnityAgent provides inbox, presence, direct and group Team chat, call-room control, push, and signaling. joinRoom connects Ringnity-managed group media while the host owns the native UI; admin billing and reports remain separate.

Server and SDK topology

Architecture that must be set up

The SDK is embedded in the iOS app, but secret-bearing work belongs to the customer backend. This prevents developers from shipping the Ringnity Server API key inside the mobile bundle.

iOS App

Customer app with Ringnity SDK

No Server API key
Uses short-lived SDK token

Customer Backend

Owned by the app developer

Validates app user
Stores Server API key

Ringnity Cloud

Tenant, chat, calls, AI, realtime

Issues SDK session
Processes SDK traffic
1. App requests token
2. Backend validates user
3. Backend calls Ringnity
4. SDK token returns
5. SDK opens chat/call

Do not call Ringnity Server API directly from iOS

iOS may call Ringnity runtime endpoints with a short-lived SDK session token. It must not contain the Server API key, platform owner credentials, or long-lived admin credentials.

Overview

Visitor capability map

This map covers the visitor runtime. Agent data and signaling APIs use RingnityAgent and the canonical Agent SDK contract.

Managed Support UI

Preview

Launch a complete SwiftUI support experience with Ringnity-provided views.

Headless Chat

Ready

Create conversations and messages from a native iOS screen.

AI Chat

Progressive

Ask AI over HTTP and render the completed response through progressive async events.

Voice Call

Host integration

Creates call metadata and local media controls; the host must supply signalling and TURN.

Video Call

Host integration

Creates video metadata and local controls; end-to-end WebRTC signalling is not bundled.

APNs and Ringtone

Ready

Register APNs tokens and play foreground incoming call ringtone behavior.

Package shape

The Swift preview package includes the async/await SDK surface, SwiftUI managed views, and adapter hooks for Apple-specific mobile pieces.

text
RingnitySDK
  Swift async/await object model, generated operation layer,
  URLSession adapter, SwiftUI Managed UI preview, and Headless SDK objects.

Apple platform adapter
  AVAudioSession handling, permission helpers, ringtone bridge,
  call state, and SwiftUI video view injection points.

Scalar API Reference

Use Scalar when backend developers need exact endpoint fields, schemas, auth requirements, status codes, or generated client details.

Open Scalar

Server

Server requirements

The backend endpoint is the trust boundary. It authenticates the customer app user, creates Ringnity context, exchanges it to a SDK session token, and returns only that short-lived token to iOS.

Backend checklist

Ringnity tenant slug, for example your-slug.
Server API key created from the tenant dashboard.
Customer backend endpoint, for example POST /ringnity/runtime-token.
HTTPS in staging and production.
User validation in the customer backend before a token is issued.
No Server API key inside Swift source, app bundle, logs, remote config, or compiled iOS artifacts.

iOS compatibility

RuntimeNative iOS app using Swift and async/await
Install modeLocal Swift Package Manager dependency during preview
Minimum targetiOS 15+ for SwiftUI Managed UI preview
PackageAdd the local Package.swift and link the RingnitySDK product
PermissionsNSMicrophoneUsageDescription and NSCameraUsageDescription when calls are used
PushAPNs token registration when background delivery is needed
CallsRingnityAppleCallAdapter for AVAudioSession and permission handling
NetworkHTTPS access to customer backend and Ringnity API
RingtoneOptional ringnity_default.caf inside the app bundle

Server

Create the runtime token endpoint

Keep secrets on your backend while iOS receives only short-lived access. The example below creates a trusted visitor context, exchanges it for a SDK session, then returns the SDK session token.

.env
RINGNITY_API_BASE_URL=https://api.ringnity.com
RINGNITY_SERVER_API_KEY=sk_live_or_test_from_ringnity_dashboard
server.js
import express from "express";
import { RingnityServerApi } from "@ringnity/server-api";

const app = express();
app.use(express.json());

const ringnity = RingnityServerApi.create({
  apiBaseUrl: process.env.RINGNITY_API_BASE_URL,
  apiKey: process.env.RINGNITY_SERVER_API_KEY,
});

async function requireCustomer(req) {
  // Replace this with your own session, JWT, or account validation.
  return {
    id: req.body.userId,
    name: req.body.name,
    email: req.body.email,
  };
}

app.post("/ringnity/runtime-token", async (req, res) => {
  const customer = await requireCustomer(req);

  const context = await ringnity.tokens.visitorContext({
    visitor: {
      externalId: customer.id,
      name: customer.name,
      email: customer.email,
    },
    metadata: {
      source: "ios-app",
      appVersion: req.body.appVersion,
      platform: "ios",
    },
    expiresIn: 900,
  });

  res.json({
    token: context.contextToken,
    expiresIn: context.expiresIn,
    tokenType: context.tokenType,
  });
});

app.listen(3000);

iOS receives

json
{
  "token": "eyJhbGciOi...",
  "expiresIn": 3600,
  "tokenType": "Bearer"
}

Contract

Keep the mobile response small: token, expiresIn, and tokenType. Avoid returning the Server API key, tenant internal id, credential id, scopes, or debug payloads to iOS.

Server

Token lifecycle

Move from app entry to support session with a predictable flow. iOS can ask the backend for a new token when the app starts, when the SDK returns unauthorized, or before a long chat/call session.

Create

Backend validates the app user and returns a short-lived SDK token.

Use

SDK sends the token as Ringnity SDK session authorization for chat, calls, AI, and push registration.

Refresh

When expired, request a new runtime token from the customer backend and recreate or refresh the SDK client.

Server

Security checklist

Use this checklist before sharing the iOS build with a real customer.

Server API key is stored only in backend environment variables or secret manager.
Backend verifies the customer user before issuing a token.
Token endpoint is protected by HTTPS, authentication, and rate limiting.
Logs do not print full runtime token, Server API key, or Ringnity credential values.
iOS requests microphone, camera, and notification permissions through the host app flow.
Production builds point to the production customer backend endpoint.

iOS Setup

Download what you need

Current downloadable SDK bundle: 0.2.0-beta.0. Use the Swift SDK for the app and the Server API SDK for the customer backend token endpoint.

iOS Setup

Install the Swift SDK

During preview, download the source package and install it through Swift Package Manager as a local package. When public package publishing is enabled, this section can switch to a Git or registry dependency.

1. Download and extract

Download the Swift SDK zip, extract it beside your app workspace, then add the extracted Package.swift through Xcode.

Download Swift SDK
text
# Pilot install from a local Swift package
1. Download ringnity-swift-sdk-0.2.0-beta.0.zip
2. Extract it beside your iOS app workspace
3. Open Xcode
4. File > Add Package Dependencies
5. Add Local... and select the extracted Package.swift
6. Link the RingnitySDK product to your app target

iOS Setup

Initialize SDK

Create the SDK from a SwiftUI screen, view model, app service, or support module. The token provider calls the customer backend endpoint from the Server section.

Swift
import Foundation
import RingnitySDK

struct RingnityRuntimeTokenResponse: Decodable {
    let token: String
    let expiresIn: Int
    let tokenType: String?
}

let callAdapter = RingnityAppleCallAdapter()

func fetchRingnityToken() async throws -> RingnityRuntimeToken {
    var request = URLRequest(
        url: URL(string: "https://your-backend.example.com/ringnity/runtime-token")!
    )
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONSerialization.data(withJSONObject: [
        "userId": currentUser.id,
        "name": currentUser.name,
        "email": currentUser.email,
        "appVersion": "1.0.0",
        "platform": "ios"
    ])

    let (data, response) = try await URLSession.shared.data(for: request)
    guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
        throw URLError(.badServerResponse)
    }

    let decoded = try JSONDecoder().decode(RingnityRuntimeTokenResponse.self, from: data)
    return RingnityRuntimeToken(
        token: decoded.token,
        expiresIn: decoded.expiresIn,
        tokenType: decoded.tokenType ?? "Bearer"
    )
}

let ringnity = try await Ringnity.create(
    config: RingnityConfig(
        tokenProvider: fetchRingnityToken,
        callMode: .basic,
        notifications: RingnityNotificationPreferences(
            incomingCall: RingnityRingtoneOptions(
                soundName: "ringnity_default",
                vibrate: true
            )
        ),
        mediaAdapter: callAdapter,
        notificationAdapter: callAdapter
    )
)

Managed UI

Managed UI overview

Launch a complete Ringnity support screen with SwiftUI. Use Managed UI when the app wants Ringnity to draw the support screen.

Swift
import RingnitySDK
import SwiftUI

struct SupportView: View {
    let callAdapter = RingnityAppleCallAdapter()

    var body: some View {
        RingnityView(
            config: RingnityConfig(
                tokenProvider: fetchRingnityToken,
                callMode: .basic,
                mediaAdapter: callAdapter,
                notificationAdapter: callAdapter
            ),
            mode: .full,
            conversationSubject: "Mobile customer support"
        )
    }
}

Custom video renderer

If the host app already owns video surfaces, inject remote and local preview views. Otherwise, use the default managed call surface.

Swift
RingnityView(
    config: config,
    mode: .call,
    remoteVideo: {
        YourRemoteVideoView()
    },
    localPreview: {
        YourLocalPreviewView()
    }
)

Managed UI

Included features

With Managed UI, the feature implementation is bundled into RingnityView. The host app still owns backend token flow, Apple permissions, APNs setup, and app lifecycle.

Chat

Managed SwiftUI views render the visitor conversation, message list, composer, and basic chat state.

AI Chat

AI surface is available when the tenant plan and AI readiness allow it.

Voice Call

Managed preview exposes local controls after the host supplies signalling and remote media.

Video Call

Managed preview exposes video controls after the host supplies signalling, TURN, and remote media.

APNs Push

Host app still registers APNs; managed experiences benefit from the same device registration.

Ringtone

Managed foreground call experience can use the configured notification adapter.

Smaller managed surfaces

Use smaller managed surfaces when the app owns navigation but still wants Ringnity to render one support area, such as only Chat, AI, or Call.

Swift
RingnityView(
    config: config,
    mode: .chat,
    conversationSubject: "Mobile customer support"
)

RingnityView(config: config, mode: .aiChat)
RingnityView(config: config, mode: .call)

let ringnity = try await Ringnity.create(config: config)

VStack {
    RingnityChatView(ringnity: ringnity)
    RingnityCallView(ringnity: ringnity)
        .frame(height: 320)
}

Headless UI

Headless UI overview

Use Ringnity as the support engine behind your own native iOS screens. Headless UI is for apps that own navigation, message rendering, call controls, and analytics.

You own UI

Render message bubbles, composer, loading states, call buttons, AI response UI, and empty states in your app design.

SDK owns product logic

Use Ringnity for runtime token usage, conversation creation, message send, AI, calls, APNs token, and ringtone operations.

Headless UI

Feature APIs

Each visitor feature can be used from custom iOS screens. The same secure tokenProvider and SDK instance power every feature.

The examples below are for Headless UI. Managed UI uses the same product capabilities through RingnityView.

Visitor Feature

Chat

Create a visitor conversation, send messages, and keep your screen updated while support is active.

Managed UI

Use RingnityView mode full or chat when Ringnity should render the conversation screen.

Headless UI

Use chat.createConversation and chat.sendMessage when your app owns the entire chat UI.

Swift
let readiness = try await ringnity.account.readiness()

let conversation = try await ringnity.chat.createConversation(
    subject: "Need help with my order"
)

let conversationId = conversation["conversationId"] as! String

let message = try await ringnity.chat.sendMessage(
    conversationId: conversationId,
    body: "Hi, I need help."
)

try await ringnity.chat.markRead(
    conversationId: conversationId,
    messageId: message["messageId"] as! String
)

// Also available: availability, activeConversation, conversation,
// closeConversation, updateProfile, editMessage, deleteMessage, createAttachment,
// uploadAttachment, requestCall, and finalizeCallRequest. AI also exposes routing and searchKnowledge.

Visitor Feature

AI Chat

Ask AI and render the answer in your own component, while plan readiness and tenant configuration stay controlled by Ringnity.

Managed UI

Use Managed UI when the AI surface should appear inside Ringnity support screens.

Headless UI

Use ai.streamChat for progressive rendering after the HTTP response completes; it is not transport streaming.

Swift
for try await event in ringnity.ai.streamChat(
    message: "Summarize the customer issue.",
    conversationId: conversationId
) {
    switch event["type"] as? String {
    case "ai.message.delta":
        let data = event["data"] as? [String: Any]
        appendToChat(data?["delta"] as? String ?? "")
    case "ai.message.completed":
        setAiAnswer(event["data"] as? [String: Any] ?? [:])
    case "ai.message.failed":
        showAiError(event)
    default:
        break
    }
}

Visitor Feature

Voice Call

Create voice call metadata and local controls. Production audio requires host-provided signalling, ICE/TURN, and remote media wiring.

Managed UI

Use Managed UI when Ringnity should render call controls.

Headless UI

Use calls.startAudio only with a host signalling transport; the Apple adapter controls local media and UI integration.

Swift
let microphoneGranted = await RingnityAppleCallAdapter.requestMicrophonePermission()

if microphoneGranted {
    let call = try await ringnity.calls.startAudio(
        conversationId: conversationId
    )

    try await call.mute()
    try await call.unmute()
    try await call.end()
}

Visitor Feature

Video Call

Create video call metadata and local controls. Production video requires host-provided signalling, ICE/TURN, and remote media wiring.

Managed UI

Use Managed UI with default call surfaces or inject custom remote/local video views.

Headless UI

Use calls.startVideo only with host signalling and TURN; the Apple adapter controls local camera and views.

Swift
let microphoneGranted = await RingnityAppleCallAdapter.requestMicrophonePermission()
let cameraGranted = await RingnityAppleCallAdapter.requestCameraPermission()

if microphoneGranted && cameraGranted {
    let call = try await ringnity.calls.startVideo(
        conversationId: conversationId
    )

    try await call.setVideoEnabled(false)
    try await call.setVideoEnabled(true)
    try await call.end()
}

Visitor Feature

APNs Push Notification

Register the APNs token so Ringnity can target the visitor when background delivery is needed.

Managed UI

Managed UI benefits from the same registered APNs token but the host app still owns notification setup.

Headless UI

Use devices.registerPushToken after your app obtains an APNs or VoIP token from Apple services.

Swift
try await ringnity.devices.registerPushToken(
    platform: .ios,
    token: apnsToken,
    provider: .apns,
    audience: .customer,
    externalId: "customer-123",
    appId: "com.example.customer"
)

Visitor Feature

Ringtone

Play or preview incoming call sounds while the app is in the foreground, then stop when the call is answered, declined, or expired.

Managed UI

Managed foreground calls can use the configured notification adapter.

Headless UI

Use notifications.previewRingtone, playIncomingRingtone, and stopRingtone when your app owns the call invitation UI.

Swift
// Add iOS asset: ringnity_default.caf in the app bundle.

try await ringnity.notifications.previewRingtone()

try await ringnity.notifications.playIncomingRingtone()

// Stop when the customer answers, declines, or the call expires.
try await ringnity.notifications.stopRingtone()

Operations

Lifecycle

Keep setup predictable by treating the SDK as a runtime service owned by a support screen, view model, or app service.

1

App opens the support entry point.

2

iOS requests a runtime token from the customer backend.

3

Customer backend validates the app user and returns a short-lived context token.

4

iOS creates the Ringnity SDK instance with tokenProvider.

5

App chooses Managed UI or Headless UI.

6

Visitor conversation is created or restored.

7

App reads or refreshes state while the screen is active.

8

Visitor sends chat, AI, voice, or video actions.

9

App stops call/ringtone work and releases screen-owned tasks when leaving support.

10

App asks the backend for a new token when the SDK session expires.

Operations

Troubleshooting

Most integration failures come from missing tenant context, token exchange mistakes, or Apple permission setup.

MISSING_TENANT

Check that the backend exchanges context with the correct tenant slug before returning the SDK session token.

SDK_ORIGIN_NOT_ALLOWED

For mobile SDK sessions, check allowed app/domain configuration and make sure the token was created for the right tenant.

401 or expired token

Request a new runtime token from the customer backend and recreate or refresh the SDK instance.

No call audio/video

Check microphone/camera usage strings, permission state, AVAudioSession setup, and call adapter wiring.

No push notification

Confirm APNs token registration, appId, audience customer, externalId, and notification entitlement.

Operations

Full example

Use Managed UI for the fastest install. Use Headless UI when the product screen must follow the host app design system.

Swift
import Foundation
import RingnitySDK
import SwiftUI

let callAdapter = RingnityAppleCallAdapter()

func fetchRingnityToken() async throws -> RingnityRuntimeToken {
    let url = URL(string: "https://your-backend.example.com/ringnity/runtime-token")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONSerialization.data(withJSONObject: [
        "userId": currentUser.id,
        "name": currentUser.name,
        "email": currentUser.email,
        "platform": "ios"
    ])

    let (data, _) = try await URLSession.shared.data(for: request)
    let decoded = try JSONDecoder().decode(RingnityRuntimeTokenResponse.self, from: data)
    return RingnityRuntimeToken(
        token: decoded.token,
        expiresIn: decoded.expiresIn,
        tokenType: decoded.tokenType ?? "Bearer"
    )
}

let config = RingnityConfig(
    tokenProvider: fetchRingnityToken,
    callMode: .basic,
    mediaAdapter: callAdapter,
    notificationAdapter: callAdapter
)

struct ManagedSupportScreen: View {
    var body: some View {
        RingnityView(
            config: config,
            mode: .full,
            conversationSubject: "Mobile customer support"
        )
    }
}

@MainActor
final class HeadlessSupportModel: ObservableObject {
    @Published var messages: [[String: Any]] = []
    private var ringnity: Ringnity?

    func start() async {
        do {
            let sdk = try await Ringnity.create(config: config)
            let conversation = try await sdk.chat.createConversation(
                subject: "Mobile customer support"
            )
            let conversationId = conversation["conversationId"] as! String

            let message = try await sdk.chat.sendMessage(
                conversationId: conversationId,
                body: "Hi, I need help."
            )

            messages.append(message)
            ringnity = sdk
        } catch {
            showError(error)
        }
    }

    func startVideo(conversationId: String) async throws {
        let microphoneGranted = await RingnityAppleCallAdapter.requestMicrophonePermission()
        let cameraGranted = await RingnityAppleCallAdapter.requestCameraPermission()
        guard microphoneGranted && cameraGranted else { return }

        let call = try await ringnity?.calls.startVideo(
            conversationId: conversationId
        )
        try await call?.setVideoEnabled(true)
    }
}

struct HeadlessSupportScreen: View {
    @StateObject private var model = HeadlessSupportModel()

    var body: some View {
        YourCustomSupportView(messages: model.messages)
            .task { await model.start() }
    }
}

Last updated: June 30, 2026.

Turn Your Website Into a Real-Time Call Center

Let customers call your team directly from your website, no phone numbers and no apps required. Just add one <script>.