Android quickstart
Add the Maven repository, configure the SDK once at launch, implement one delegate, and present the assistant from any button.
Install the SDK
The SDK ships as a binary from a public, token-free Maven repository. Add the repository, then the dependency.
dependencyResolutionManagement { repositories { google() mavenCentral() // Voqal SDK — public, binary-only Maven repo (no token required). maven { url = uri("https://raw.githubusercontent.com/VoqalAI/voqal-android-maven/main") } }}dependencies { implementation("ai.voqal:voqal-sdk:2.1.0")}Configure at launch
Call VoqalSDK.setup once in your Application. Calling prewarm right after opens the engine connection in the background so the first turn skips a cold handshake.
import android.app.Applicationimport ai.voqal.sdk.*class App : Application() { val voqalDelegate = VoqalCredentials(authStore) override fun onCreate() { super.onCreate() VoqalSDK.setup( VoqalConfiguration( requestId = "prod-yourapp", // "prod-" / "stg-" apiKey = "pk_live_…", // sent as X-Voqal-Key theme = VoqalTheme(accent = "#2d5bff", appearance = VoqalTheme.Appearance.AUTO), ) ) VoqalSDK.prewarm(voqalDelegate) // warm the engine at launch }}requestId selects the environment by prefix: prod- routes to production, stg- to staging.Implement the delegate
VoqalDelegate supplies credentials live — the SDK reads the token on every request, so always return one that is currently valid. getForwardedHeaders and onActionButtonTapped are new in 2.0.0 and both optional; the two sections below cover them.
import ai.voqal.sdk.VoqalDelegateclass VoqalCredentials(private val authStore: AuthStore) : VoqalDelegate { // Read live on every request — always hand back a current token. override fun getToken(): String = authStore.currentToken // Optional JSON: country, user id — read by the engine as X-Client-Metadata. override fun getMetadata(): String? = """{"country_code":"EGY","user_id":"285"}""" // New in 2.0.0 — forwarded verbatim to your backend (see below). override fun getForwardedHeaders(): Map<String, String> = mapOf("X-Tenant-Id" to authStore.tenantId) // Optional — host navigates when the action button is tapped (see below). override fun onActionButtonTapped() { openCheckout() } override fun onUploadResult(result: String) {} // recording lifecycle override fun onError(error: Throwable) {}}Forward headers to your backend
New in 2.0.0 Return extra headers from getForwardedHeaders() and the SDK sends each one namespaced as X-Voqal-Forward-<name>. The engine strips the prefix and forwards the value verbatimto your tenant’s backend — on both HTTP turns and the voice WebSocket, with no dashboard configuration.
// Read live per turn — rotating a value takes effect on the next request.override fun getForwardedHeaders(): Map<String, String> = mapOf( "X-Tenant-Id" to session.tenantId, // → X-Voqal-Forward-X-Tenant-Id "Authorization" to "Bearer ${session.backendToken}", // reaches your backend as-is)- Read live on every request (like
getToken), so rotating a value takes effect on the next turn. Authorizationis forwardable — it rides asX-Voqal-Forward-Authorizationand reaches your backend intact.- Reserved control headers are dropped (
x-token,x-voqal-key,x-request-id,x-client-metadata,content-type,accept) so a forwarded key can never displace Voqal’s own. - At most 32 headers are forwarded; any beyond that are dropped, not errored.
getMetadata() and getForwardedHeaders() are different channels. Metadata is sent as X-Client-Metadata and read by the Voqal engine to route region and personalize; forwarded headers are passed through to your backend and never interpreted by the engine.Present the assistant
Open the assistant from any of your own buttons — Views or Compose.
// From any Activity — Views:assistantButton.setOnClickListener { VoqalSDK.present(this, (application as App).voqalDelegate)}// Or from Compose:Button(onClick = { VoqalSDK.present(activity, delegate) }) { Text("Talk to Voqal") }- Voice, transcription, widgets, and confirmations are handled inside the sheet — your app ships no assistant UI.
- Theming, presentation style, header branding, and the action button share one model across platforms — see Configuration.
Your own button
There is no Voqal launcher view on Android. Any click target you already have — a FloatingActionButton, a toolbar action, a list row, a notification, a deep link — calls VoqalSDK.present(activity, delegate). The activity must be a ComponentActivity (every AppCompat and Compose activity is).
// Compose: a floating action button. The SDK ships no launcher view; this is yours.Scaffold( floatingActionButton = { ExtendedFloatingActionButton( onClick = { VoqalSDK.present(activity, delegate) }, icon = { Icon(Icons.Default.Mic, contentDescription = null) }, text = { Text("Ask") }, ) },) { … }The assistant opens as a draggable bottom sheet over a scrim by default. For an opaque, edge-to-edge surface that closes only from its own button, set presentationStyle on the configuration — a setup-time setting, not a per-call one.
// Setup-time setting: apply before present().VoqalSDK.setup( VoqalConfiguration( requestId = "prod-yourtenant", apiKey = "pk_live_…", presentationStyle = VoqalConfiguration.PresentationStyle.FULL_SCREEN, // default: SHEET ),)Redirect & checkout
When a turn needs to hand the user off to your own screen — a checkout, a receipt, an external payment page — Voqal gives you three paths, from most to least host-driven.
1. Action button. Set actionButton on the config to draw a call-to-action next to the voice dock. Tapping it fires onActionButtonTapped(), where your app navigates wherever it wants. It is off by default — leave actionButton null and nothing is drawn beside the voice wave.
// App.kt — opt in to the dock's call-to-action button.VoqalConfiguration( requestId = "prod-yourapp", actionButton = VoqalActionButton(contentDescription = "Checkout"),)import android.content.Contextimport android.content.Intentclass VoqalCredentials(private val appContext: Context) : VoqalDelegate { // Do NOT keep an Activity in the delegate — it outlives the screen and leaks. // Navigate from applicationContext with FLAG_ACTIVITY_NEW_TASK. override fun onActionButtonTapped() { val intent = Intent(appContext, CheckoutActivity::class.java) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) appContext.startActivity(intent) }}Activity — it would leak. Navigate from applicationContext with an Intent flagged FLAG_ACTIVITY_NEW_TASK.2. Confirm widget.For an in-conversation approval — “Pay 250 EGP now?” — the engine emits a confirm widget and the SDK renders the approve/cancel step inside the sheet. Your app writes no UI; the confirmed action runs on the engine.
3. Automatic link cards. When the engine returns a link or redirect widget (a payment link, an invoice), the SDK opens it for you with an ACTION_VIEW intent — no host code required. The equivalent your app would otherwise write:
// The SDK opens engine link/redirect widgets for you — equivalent to:val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)context.startActivity(intent)