Mobile apps
This guide covers best practices for integrating k-ID widgets into mobile applications. On the web, widgets are commonly embedded in iframes, but mobile apps need different approaches to display widget URLs effectively.
Overview
k-ID widgets (CDK and AgeKit+) are URLs returned from API calls that contain complete compliance flows, including age gates, VPC, data notices, permission management, and age verification interfaces. When integrating these widgets into mobile applications, you have several options for displaying them, each with different capabilities and trade-offs. The key considerations are:
- AgeKeys support: Whether users can create and use AgeKeys (FIDO-based passkeys) for future verifications
- Result communication: How widget results are delivered back to your app
- User experience: The level of integration and native feel
Mobile implementation methods
Android options
Android provides three primary methods for displaying widget URLs:
-
Custom Tabs ⭐ Recommended - Opens the widget URL in a customized Chrome browser tab that maintains your app's branding. This provides full browser capabilities while keeping users within your app's context. Custom Tabs share cookies and authentication state with Chrome, enabling seamless experiences.
-
WebView - Embeds web content directly within your app using Android's native WebView component. While simple to implement, WebView has limited support for modern web standards and can't access certain browser features.
WebView doesn't support WebAuthn, which is required for AgeKeys. Users won't be able to create or use AgeKeys when widgets are embedded using WebView. For the best user experience with full AgeKeys support, use Custom Tabs instead.
- Trusted Web Activity (TWA) - Displays web content in full-screen mode, primarily designed for Progressive Web Apps. TWAs require establishing a digital asset link between your app and the k-ID domain. When digital asset links aren't detected, TWA automatically falls back to Custom Tabs.
Trusted Web Activities aren't supported for widget integration. Digital asset links would need to be configured on k-ID's domains, which isn't available. Since TWA falls back to Custom Tabs in this case, use Custom Tabs directly for a simpler implementation.
iOS options
iOS provides three primary methods for displaying widget URLs:
-
ASWebAuthenticationSession ⭐ Recommended - Designed specifically for secure authentication flows, this method presents web content in a system-managed browser view. It shares cookies with Safari and provides access to modern web features such as WebAuthn, making it ideal for verification flows.
-
SFSafariViewController - Presents web content in a Safari-like interface that shares cookies and authentication state with Safari. This provides a familiar browsing experience while maintaining app context.
-
WKWebView - Apple's modern web view component that embeds web content within your app. Similar to Android's WebView, WKWebView has limitations with certain web standards and can't access all browser features.
WKWebView doesn't support WebAuthn, which is required for AgeKeys. Users won't be able to create or use AgeKeys when widgets are embedded using WKWebView. For the best user experience with full AgeKeys support, use ASWebAuthenticationSession instead.
AgeKeys support limitations
AgeKeys are reusable, anonymous age-proof credentials based on FIDO and WebAuthn standards. They allow users to verify their age once and reuse that verification across different services without revealing personal information.
AgeKeys require WebAuthn support, which isn't available in Android WebView or iOS WKWebView. If you embed widgets by using these components, users won't see AgeKeys as an option during verification, and they can't create AgeKeys after successful verification.
To enable AgeKeys for your users, you must use one of these methods:
- Android: Custom Tabs
- iOS: ASWebAuthenticationSession or SFSafariViewController
Receiving widget results
Your mobile app needs to receive results after users complete the widget flow. There are two approaches, each with different availability:
For detailed information about analyzing verification results, including field presence rules, status types, and implementation guidance, see the Verification Event Contract.
Callback URL (universal method)
The recommended approach for all implementation methods is to use a callback URL. When you call the API to generate a widget URL, include a redirectUrl parameter. After the widget flow completes, it redirects to this URL with the results included as query parameters.
Advantages of callback URLs:
- Works with all implementation methods
- More reliable than DOM messages
- Standard deep linking pattern for mobile apps
- Results are always delivered, even if the app moves to the background
How callback URLs work
- Register a deep link handler in your app (for example,
myapp://vpc-completeormyapp://verification-complete) - Include the deep link as
redirectUrlwhen calling the API - The widget redirects to your deep link after completion
- Your app handles the deep link and extracts the results
Redirects only occur when the widget URL is opened directly in a browser or web view, not when embedded in an iframe.
Callback URL parameters
When the widget redirects to your callback URL, it includes query parameters relevant to the widget type. For example:
- Age gate widgets can include
verificationIdandresult - Session-related widgets can include
sessionIdand status information - Age verification widgets include
verificationIdandresult
Example callback URLs:
myapp://vpc-complete?sessionId=608616da-4fd2-4742-82bf-ec1d4ffd8187&result=PASS
myapp://verification-complete?verificationId=7854909b-9124-4bed-9282-24b44c4a3c97&result=PASS
Implementing callback URLs
Include redirectUrl in your request when calling widget APIs. The URL can be:
- An HTTPS URL:
https://example.com/vpc-complete - A custom deep link:
myapp://vpc-complete
DOM messages (WebView/WKWebView only)
When using Android WebView or iOS WKWebView, you can listen for JavaScript messages sent from the widget. This allows you to:
- Receive widget results in real-time
- Control when the web view closes
- Update your app's UI based on widget events
CDK widgets emit different DOM events depending on the widget type:
- End-to-End widget:
Widget.AgeGate.Challenge,Widget.AgeGate.Result - Age Gate widget:
Widget.AgeGate.Challenge,Widget.AgeGate.Result - Data Notices widget:
Widget.DataNotices.ConsentApproved - Session Permissions widget:
Widget.ExitReview
DOM messages are sent as postMessage events that you can intercept in your native code. For details about available events, see the DOM events overview.
DOM messages only work with WebView and WKWebView. They're not available with Custom Tabs, Trusted Web Activity, ASWebAuthenticationSession, or SFSafariViewController.
Third-party app verification flows
Some verification methods, such as ConnectID, require redirecting users to a third-party mobile app as part of the verification process. For example, ConnectID opens the user's banking app to complete identity verification.
How third-party app flows work
When using verification methods that involve third-party apps, the flow passes through multiple applications before returning to your app:
Step-by-step breakdown:
- Your app requests a verification URL from your server
- Your server calls the k-ID API with your API key, including the app's
redirectUrl - k-ID returns a URL containing the verification interface to your server
- Your server returns the URL to your app
- Your app opens the URL in a web component (ASWebAuthenticationSession or Custom Tabs)
- When the user selects a verification method such as ConnectID, the k-ID UI deep links to the third-party verification app (such as a banking app)
- After verification completes, the third-party app redirects back to a k-ID result page in the device's native browser
- k-ID retrieves your stored
redirectUrland redirects the user back to your app with the verification result
Key considerations for third-party app flows
Web component requirement - These verification methods must be opened in a system browser context (ASWebAuthenticationSession, Custom Tabs, or SFSafariViewController) rather than an embedded WebView. The third-party app redirect flow requires the full browser context to work correctly.
Native browser handoff - After the third-party app completes verification, it redirects to a k-ID URL that opens in the device's native browser rather than returning directly to your original web component. This is a platform limitation with how app-to-app redirects work on mobile devices.
Callback URL is essential - Since the verification flow passes through multiple apps and browsers, the redirectUrl parameter is critical for returning users to your app after completion. Always include a redirectUrl when initiating verifications that might use third-party app methods.
Testing third-party app flows
The following verification methods use third-party app redirects:
- ConnectID: Includes test apps for validating the redirect flow in mobile applications.
Method comparison
| Method | Platform | AgeKeys | DOM Messages | Callback URL | Best For |
|---|---|---|---|---|---|
| WebView | Android | ❌ | ✅ | ✅ | Not recommended (no AgeKeys support) |
| Custom Tabs | Android | ✅ | ❌ | ✅ | Most use cases (recommended) |
| Trusted Web Activity | Android | ✅ | ❌ | ✅ | Not recommended (requires digital asset links) |
| WKWebView | iOS | ❌ | ✅ | ✅ | Not recommended (no AgeKeys support) |
| ASWebAuthenticationSession | iOS | ✅ | ❌ | ✅ | Most use cases (recommended) |
| SFSafariViewController | iOS | ✅ | ❌ | ✅ | Safari-like experience |
Recommended implementation
Android: Custom Tabs
Use Custom Tabs with callback URLs for the best balance of features and user experience.
Why Custom Tabs:
- Full AgeKeys support via WebAuthn
- Access to all modern web features
- Seamless user experience with app branding
- Reliable callback mechanism
- Shares authentication state with Chrome
Implementation steps:
- Register a deep link handler for your callback URL
- Include
redirectUrlin your API request (if supported by the endpoint) - Open the widget URL using Custom Tabs
- Handle the deep link callback with widget results
iOS: ASWebAuthenticationSession
Use ASWebAuthenticationSession with callback URLs for secure, native-feeling compliance flows.
Why ASWebAuthenticationSession:
- Full AgeKeys support via WebAuthn
- Access to all modern web features
- System-managed security UI
- Shares cookies with Safari
- Reliable callback mechanism
Implementation steps:
- Register a URL scheme handler for your callback URL
- Include
redirectUrlin your API request (if supported by the endpoint) - Present the widget URL using ASWebAuthenticationSession
- Handle the URL scheme callback with widget results
Complete implementation example
Here's a step-by-step example of implementing the recommended approach with complete code samples:
Step 1: Register deep link handler
- iOS (Swift)
- Android (Kotlin)
Register a URL scheme in Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
Add an intent filter in AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="vpc-complete" />
</intent-filter>
</activity>
Step 2: Generate widget URL from your server
The widget URL must be generated from your server, not directly from the mobile app. This protects your API key from being exposed in client-side code. Your mobile app should call your own server API, which then makes the server-to-server call to k-ID.
Architecture overview
Server implementation
Your server calls the k-ID API with your API key:
- CDK (End-to-End widget)
- AgeKit+ (Access age verification)
POST https://game-api.k-id.com/api/v1/widget/generate-e2e-url
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
{
"jurisdiction": "US-CA",
"options": {
"redirectUrl": "myapp://vpc-complete"
}
}
For testing, use the test environment endpoint: https://game-api.test.k-id.com/api/v1/widget/generate-e2e-url
Response:
{
"id": "7854909b-9124-4bed-9282-24b44c4a3c97",
"url": "https://family.k-id.com/widget?token=eyJhbGciOiJFUzM4NCIs..."
}
POST https://game-api.k-id.com/api/v1/age-verification/perform-access-age-verification
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
{
"jurisdiction": "US-CA",
"criteria": {
"ageCategory": "DIGITAL_YOUTH_OR_ADULT"
},
"options": {
"redirectUrl": "myapp://verification-complete"
}
}
For testing, use the test environment endpoint: https://game-api.test.k-id.com/api/v1/age-verification/perform-access-age-verification
Response:
{
"url": "https://family.k-id.com/widget?token=eyJhbGciOiJFUzM4NCIs..."
}
Mobile client implementation
Your mobile app calls your server to get the widget URL:
- iOS (Swift)
- Android (Kotlin)
import Foundation
func fetchWidgetUrl(completion: @escaping (URL?) -> Void) {
// Call YOUR server endpoint, not k-ID directly
guard let url = URL(string: "https://your-server.com/api/generate-widget-url") else {
completion(nil)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Add your own authentication (session token, etc.)
request.setValue("Bearer USER_SESSION_TOKEN", forHTTPHeaderField: "Authorization")
let requestBody: [String: Any] = [
"jurisdiction": "US-CA",
"options": [
"redirectUrl": "myapp://vpc-complete"
]
]
guard let httpBody = try? JSONSerialization.data(withJSONObject: requestBody) else {
completion(nil)
return
}
request.httpBody = httpBody
URLSession.shared.dataTask(with: request) { data, response, error in
guard error == nil,
let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode),
let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let widgetUrlString = json["url"] as? String,
let widgetUrl = URL(string: widgetUrlString) else {
completion(nil)
return
}
completion(widgetUrl)
}.resume()
}
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.IOException
fun fetchWidgetUrl(callback: (String?) -> Unit) {
val client = OkHttpClient()
val mediaType = "application/json".toMediaType()
val requestBody = JSONObject().apply {
put("jurisdiction", "US-CA")
put("options", JSONObject().apply {
put("redirectUrl", "myapp://vpc-complete")
})
}.toString().toRequestBody(mediaType)
// Call YOUR server endpoint, not k-ID directly
val request = Request.Builder()
.url("https://your-server.com/api/generate-widget-url")
.post(requestBody)
// Add your own authentication (session token, etc.)
.addHeader("Authorization", "Bearer USER_SESSION_TOKEN")
.addHeader("Content-Type", "application/json")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
callback(null)
return
}
response.body?.use { body ->
try {
val jsonResponse = JSONObject(body.string())
val widgetUrl = jsonResponse.optString("url", null)
callback(widgetUrl)
} catch (e: Exception) {
callback(null)
}
} ?: callback(null)
}
override fun onFailure(call: Call, e: IOException) {
callback(null)
}
})
}
Step 3: Display the widget
- iOS (Swift)
- Android (Kotlin)
import AuthenticationServices
// Store session as a property to prevent deallocation
var authSession: ASWebAuthenticationSession?
func displayWidget(widgetUrl: URL) {
authSession = ASWebAuthenticationSession(
url: widgetUrl,
callbackURLScheme: "myapp"
) { callbackURL, error in
if let error = error {
// Handle error (user cancelled, etc.)
return
}
if let callbackURL = callbackURL {
handleWidgetCallback(callbackURL)
}
}
authSession?.presentationContextProvider = self
authSession?.start()
}
import android.app.Activity
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
fun displayWidget(activity: Activity, widgetUrl: String?) {
if (widgetUrl == null) {
// Handle error: failed to generate widget URL
return
}
val builder = CustomTabsIntent.Builder()
val customTabsIntent = builder.build()
val uri = Uri.parse(widgetUrl)
customTabsIntent.launchUrl(activity, uri)
}
Step 4: Handle the callback
- iOS (Swift)
- Android (Kotlin)
func handleWidgetCallback(_ callbackURL: URL) {
// Check if this is a callback URL we're expecting
guard callbackURL.scheme == "myapp",
(callbackURL.host == "vpc-complete" || callbackURL.host == "verification-complete"),
let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems else {
return
}
let sessionId = queryItems.first(where: { $0.name == "sessionId" })?.value
let verificationId = queryItems.first(where: { $0.name == "verificationId" })?.value
let result = queryItems.first(where: { $0.name == "result" })?.value
// Update UI based on widget result
if result == "PASS" {
// Handle successful flow
} else if result == "FAIL" {
// Handle failed flow
}
// Optionally verify server-side using API endpoints
if let sessionId = sessionId {
verifyResultServerSide(sessionId: sessionId)
} else if let verificationId = verificationId {
verifyResultServerSide(verificationId: verificationId)
}
}
import android.content.Intent
import android.net.Uri
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent) // Important: ensure the new intent is used
val data: Uri? = intent.data
if (data != null && (data.scheme == "myapp" && (data.host == "vpc-complete" || data.host == "verification-complete"))) {
val sessionId = data.getQueryParameter("sessionId")
val verificationId = data.getQueryParameter("verificationId")
val result = data.getQueryParameter("result")
// Update UI based on widget result
if (result == "PASS") {
// Handle successful flow
} else {
// Handle failed flow
}
// Optionally verify server-side using API endpoints
if (sessionId != null) {
verifyResultServerSide(sessionId)
} else if (verificationId != null) {
verifyResultServerSide(verificationId)
}
}
}
Always verify widget results server-side using the appropriate API endpoints rather than relying solely on client-side data for security and data integrity. For CDK, use the /session/get endpoint. For AgeKit+, use the /age-verification/get-status endpoint. For detailed information about analyzing verification results, including field presence rules, status types, and implementation guidance, see the Verification Event Contract.
Widget types
k-ID provides several widget types, each designed for specific compliance workflows:
CDK widgets
- End-to-End widget: Handles the complete VPC flow in a single interface, including age gate, verification, data notices, permissions, and preferences
- Age Gate widget: Collects user age and triggers VPC challenges when needed
- Data Notices widget: Displays jurisdiction-appropriate data notices and collects user consent
- Session Permissions widget: Allows users and parents to view and update active session permissions
AgeKit+ widgets
- Access age verification: Complete age verification interface that handles the full verification flow
- Waterfall flow: Multi-method verification flow that tries different verification methods in sequence
- Single method flow: Direct verification using a specific method
For detailed information about each widget type and their specific use cases, see the guides in the CDK and AgeKit+ documentation.