iOS quickstart
Install the package, implement one delegate, present the assistant. Your app ships no assistant UI of its own.
The SDK is a UIKit host with a SwiftUI interior, distributed as a binary XCFramework over Swift Package Manager, targeting iOS 16 and above. Android, Flutter, and React Native follow the same four steps.
Install the SDK
Add the package in Xcode and pick the VoqalSDK library. There is nothing to vendor and no build phase to configure.
// File → Add Package Dependencies… → paste the URL, or add it to Package.swift:.package(url: "https://github.com/VoqalAI/voqal-ios", from: "2.1.0")Then import VoqalSDK wherever you configure the app.
Configure at launch
Call setup once, as early as you can. Calling prewarm straight after opens the backend connection in the background so the first turn skips a cold handshake — it is a no-op until your delegate can supply a token.
import VoqalSDKfunc application( _ application: UIApplication, didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { var configuration = VoqalSDKConfiguration( requestId: "prod-\(session.userId)", theme: VoqalTheme(accent: "#2D5BFF", accent2: "#5EC5F1", appearance: .auto) ) configuration.apiKey = "pk_live_…" // sent as X-Voqal-Key VoqalSDKManager.shared.setup(configuration: configuration) VoqalSDKManager.shared.prewarm(delegate: coordinator) return true}Configuration options
| Option | Type | Required | Description |
|---|---|---|---|
requestId | String | Required | Routes the turn by prefix: prod- reaches your production backend, stg- reaches staging. |
apiKey | String? | Required | Your publishable key, sent as X-Voqal-Key. Resolves which tenant config to load. |
agentURL | URL? | Optional | Overrides the baked engine URL. Point it at localhost while developing. |
theme | VoqalTheme | Optional | Accent pair, appearance (.light, .dark, .auto), and corner radius. |
presentationStyle | VoqalPresentationStyle | Optional | .sheet (default) or .fullScreen. |
recordingInteractionMode | RecordingInteractionMode | Optional | Tap-to-toggle or hold-to-talk on the mic control. |
Implement the delegate
VocalButtonDelegate is the whole integration surface: five required methods, all synchronous. Two optional hooks — voqalForwardedHeaders() and voqalDidTapActionButton() — are covered below.
extension AppCoordinator: VocalButtonDelegate { // Read live on every request — always hand back a current token. func getToken() -> String { session.accessToken } func getMetaData() -> String? { #"{"country_code":"EGY","user_id":"84213"}"# } func getViewController() -> UIViewController { navigationController } func voqalButton(didUploadRecording result: String) { analytics.track("voqal_turn", result) } func voqalButton(didFailWith error: Error) { logger.error("voqal", error) }}| Method | Returns | Called when |
|---|---|---|
getToken() | String | Every request. Return your user's current backend token — the SDK never caches it. |
getMetaData() | String? | Every request. A JSON string of client context, sent as X-Client-Metadata. |
getViewController() | UIViewController | The assistant needs a presenter, for example to push one of your own screens. |
voqalButton(didUploadRecording:) | Void | A voice turn finished. The payload is the transcript result. |
voqalButton(didFailWith:) | Void | A turn failed. Log it — the assistant has already shown the user a recoverable state. |
getToken() every time. The SDK reads it live on each request rather than holding a copy, so handing back a token you captured at login will start failing the moment it expires.Present the assistant
Open it from your own button, tab, or deep link. It comes up as a near-full sheet by default and manages its own navigation from there.
@objc func openAssistant() { VoqalSDKManager.shared.presentChat(from: self, delegate: coordinator)}// Optional: forward every SDK event into your own logging.VoqalSDKManager.shared.addLogSink(VoqalConsoleLogSink())Add NSMicrophoneUsageDescription to your Info.plist before you run — iOS terminates the app on first microphone access without it.
Your own button or the Voqal button
Two ways to give users a way in. VoqalButton is a ready-made circular launcher (110pt, accent-colored, your voqalButtonIcon inside) that presents the assistant when tapped — drop it in and pin it. Or use no Voqal chrome at all and call presentChat(from:delegate:) from your own bar button, tab, cell, or deep link. Both reach the same assistant.
// The provided launcher: a 110pt circular button that presents on tap.let button = VoqalButton()button.delegate = coordinator // your VocalButtonDelegatebutton.presentingViewController = selfview.addSubview(button)// Pin it bottom-trailing with Auto Layout; its intrinsic size is 110×110.// Or skip VoqalButton entirely and present from anything of yours:@IBAction func askTapped(_ sender: UIBarButtonItem) { VoqalSDKManager.shared.presentChat(from: self, delegate: coordinator)}The assistant comes up as a near-full sheet by default, swipe-to-dismiss. For an edge-to-edge screen that closes only from its own button, set presentationStyle on the configuration — it is a setup-time setting, not a per-call one.
// Setup-time setting: apply before presentChat.configuration.presentationStyle = .fullScreen // default: .sheetVoqalSDKManager.shared.setup(configuration: configuration)Forward headers to your backend
New in 2.0.0.Some backends need more than the user's token — an API version, a tenant id, a feature flag. Implement the optional voqalForwardedHeaders() to hand Voqal a dictionary; the SDK sends each entry to the engine as X-Voqal-Forward-<name>, and the engine strips the prefix and forwards the value verbatim to your backend or MCP. Nothing to configure in the dashboard.
// Delegate method — read live each turn, like getToken().func voqalForwardedHeaders() -> [String: String] { [ "Api-Version": "2024-10", "Tenant-Id": session.tenantId, "Authorization": "Bearer \(session.partnerToken)", ]}The dictionary above travels as X-Voqal-Forward-Api-Version, X-Voqal-Forward-Tenant-Id, and X-Voqal-Forward-Authorization. Like getToken(), it is read live on every request, so a rotated value applies on the next turn.
Authorizationis forwardable — send a bearer token for your own backend alongside the user'sX-Token.- At most 32 headers are forwarded; extras are dropped rather than failing the turn.
- An empty dictionary — the default — forwards nothing, so existing integrations are unaffected.
x-token, x-voqal-key, x-request-id, x-client-metadata, content-type, accept — is ignored, so it can never displace auth or routing.Metadata vs forwarded headers
Two delegate hooks carry host data, to two different consumers. Use getMetaData() for context the assistant should reason about; use voqalForwardedHeaders() for values your backend needs on the wire.
| Delegate hook | Wire header | Read by |
|---|---|---|
getMetaData() | X-Client-Metadata | The Voqal engine — client context (country, user id) that shapes routing and prompts. |
voqalForwardedHeaders() | X-Voqal-Forward-<name> | Your backend or MCP — the engine strips the prefix and passes each value through verbatim. |
Redirect & checkout
A turn often ends in an action — open checkout, view an order, follow a link. Voqal offers three ways to get the user there, from a hard handoff you own to fully automatic link cards.
Action button
Set configuration.actionButton to show an accent button beside the voice dock. Tapping it fires voqalDidTapActionButton() with no payload — you own the route. Use it for a fixed destination such as a cart or a checkout screen pushed from getViewController().
It is off by default: leave actionButton nil and nothing is drawn beside the voice wave. Set it only when your product has a destination worth a permanent button; a booking or support assistant usually needs none.
// Show the button, to the right of the voice dock…configuration.actionButton = VoqalActionButton(accessibilityLabel: "Checkout")// …then handle the tap. No payload — you own the route.func voqalDidTapActionButton() { navigationController.pushViewController(CheckoutViewController(), animated: true)}Confirm widget
When the agent runs a money-moving or otherwise sensitive action, the engine emits a confirm widget automatically — an in-conversation checkout the user approves inline. High-risk actions gate behind Face ID; you write no code for this.
Link cards
Any URL in the agent's answer becomes a tappable card that opens through the operating system. No configuration — return a link from your backend and the SDK renders it.
Next steps
You now have a working assistant. From here, most teams pick up one of these:
- Connect your backend's MCP server so the agent answers from real data instead of general knowledge.
- Read the widget catalogue to see what the agent can draw, and which kinds require a confirm step.
- Attach a log sink and forward SDK diagnostics into your existing error reporting.
- Work through the engine API if you need to drive a turn from somewhere other than an app.
