Mobile Integration
Flow doesn't currently have mobile-specific SDKs, but with our hosted verification system, verifying users with an embedded WebView inside of your native application is extremely easy.
Hosted verifications are Cognito-hosted URLs that contain your full verification experience. These hosted verifications can be generated via API and then the URLs for these verifications can be presented by your mobile app. Via API, you’re able to send a request from your backend that looks approximately like this:
POST /flow_sessions?idempotent=true HTTP/1.1 { "shareable": true, "template_id": "flwtmp_11111111111111", // Replace with your template ID "user": { // Read this for more context: https://cognitohq.com/docs/flow/trying-your-flow#customer-references "customer_reference": "your-users-internal-database-id-here", // Optional, but useful for fraud signals: https://cognitohq.com/docs/flow/trying-your-flow#supplementing-email "email": "your.users.email@example.com" } }
We’ve omitted the other required headers from that example for brevity. You can read about our authentication system on our docs.
One additional note about the query parameter:
You can optionally supply ?idempotent=true
for this specific request. With idempotency enabled, we will respond with a 201 Created
HTTP status code the first time you send us the associated (customer_reference, template_id)
pair. After that, you can always send the same (customer_reference, template_id)
pair again and we will not create a new session, instead returning a 200 OK
status code with the previously created session resource in the body.
In general, Flow is designed so that everything can be configured and managed without code changes. Part of this includes not forcing you to persist the flwses_111...
IDs we return, instead using your internal identifier. We provide this idempotency feature so that you can, for example, have your backend do POST /flow_sessions?idempotent=true
every time a user enters your KYC flow on mobile, without having to worry about either doing a POST
or GET
depending on their past interactions.
The above request returns a full FlowSession
resource that will include a shareable_url
entry:
{
"id": "flwses_42424242424242",
"customer_reference": "your-users-internal-database-id-here",
"shareable_url": "https://dashboard.cognitohq.com/verify/flwses_42424242424242?key=069d2f5f8f03c6e4c1f2287049037450",
"template": {
"id": "flwtmp_11111111111111",
"version": 7
}
// Trimmed for brevity
// Full docs on this response payload here: https://cognitohq.com/docs/reference#flow_get_flow_session
}
That shareable_url
is unique to this session and should only be opened by the customer associated with the customer_reference
and email
you provided.
For your mobile application, you can then embed this shareable_url
in a WebView and everything should just work from there. Once you have it configured via our dashboard, you’ll receive webhooks as the user completes each step of their session, ending with a webhook letting you know if they passed or failed the session.
The implementation within your app should be fairly straightforward, but one additional note just to help make sure your integration is secure:
Within your app, when your user clicks the UI element for verifying their identity, your application should either POST
or GET
to an internal API of yours without any payload in the body. Meaning that the customer_reference
and email
you provide from your backend to our API should not be controllable by the end user. This is important to the product’s security model, since we use the (customer_reference, template_id)
pair’s uniqueness to enforce that a user can only make one verification attempt until you manually authorize a retry.
Camera access in iOS
Flow uses the device's camera to capture images for document verification and selfie verification. Make sure that NSCameraUsageDescription
is defined in your app's plist.
The selfie check uses an HTML5 video element. Set allowsInlineMediaPlayback
to prevent the video element from going full-screen, which obscures the on-screen instructions to the user.
func makeUIView(context: Context) -> WKWebView {
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
return WKWebView(frame: .zero, configuration: config)
}
Camera access in Android
Flow uses the device's camera to capture images for document verification and selfie verification. Make sure that the CAMERA
permission is requested in your app's manifest.
The selfie check uses WebRTC. Because Flow is within a WebView, you must grant RESOURCE_VIDEO_CAPTURE
permission for the WebChromeClient. There is no need to grant permission for RESOURCE_AUDIO_CAPTURE
, as the selfie check is video only.
override fun onPermissionRequest(request: PermissionRequest?) {
request?.resources?.forEach { r ->
if (r == PermissionRequest.RESOURCE_VIDEO_CAPTURE) {
request.grant(arrayOf(r))
return
}
}
super.onPermissionRequest(request)
}
Listening for Flow Events in iOS
If you are embedding hosted Flows inside of a webview, you can attach a WKUserContentController
to a WKWebView
to receive events. A bare bones implementation might look like this:
import SwiftUI
import WebKit
struct WebView : UIViewRepresentable {
let request: URLRequest
let contentController = ContentController()
func makeUIView(context: Context) -> WKWebView {
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
return WKWebView(frame: .zero, configuration: config)
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.configuration.userContentController.add(contentController, name: "cognito")
uiView.load(request)
}
class ContentController: NSObject, WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print("Received a Flow event")
print(message.body)
}
}
}
struct ContentView: View {
@State var showSafariView = false
let flowUrl: URL
@ViewBuilder var body: some View {
Button("Launch flow", action: { showSafariView = true }).sheet(isPresented: $showSafariView) {
WebView(request: URLRequest(url: flowUrl))
}
}
}
Listening for Flow Events in Android
If you are embedding hosted Flows inside of a webview, you can use a WebMessagePort
via createWebMessageChannel()
to receive events. A bare bones implementation might look like this:
class ExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_example)
val webView: WebView = findViewById(R.id.webview)
webView.loadUrl(flowUrl)
webView.settings.javaScriptEnabled = true
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
initializeWebMessages(webView)
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initializeWebMessages(webView: WebView) {
val webMessageChannel = webView.createWebMessageChannel()
val (receiver, sender) = webMessageChannel
receiver.setWebMessageCallback(object : WebMessagePort.WebMessageCallback() {
override fun onMessage(port: WebMessagePort, message: WebMessage?) {
Log.i("success_example", "Received ${message!!.data}")
}
})
webView.postWebMessage(WebMessage("subscribeToFlowWebMessagePort", arrayOf(sender)), Uri.EMPTY)
}
}
Don't forget to close()
the receiver port when you are done using it. A logical place to do that would be in the onDestroy()
method of your activity. This will free up resources and prevent memory leaks. You cannot close the sender port because it was transferred to Javascript.
HTML input element in Android
Flow uses <input type="file" accept="image/jpeg" capture />
elements to capture images for things like document verification and selfie verification. Mobile browsers open the native camera app for those inputs, but Android's WebView does not. So when you are embedding hosted Flows inside of a WebView, your code needs to open the camera by overriding onShowFileChooser()
and starting an activity with an image capture intent. You also need to return the captured image as a URI to the WebView. A bare bones implementation might look like this:
class ExampleActivity : AppCompatActivity() {
val REQUEST_IMAGE_CAPTURE = 1
var uploadMessage: ValueCallback<Array<Uri?>?>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_example)
val webView: WebView = findViewById(R.id.webview)
webView.loadUrl(flowUrl)
webView.settings.javaScriptEnabled = true
webView.webChromeClient = object : WebChromeClient() {
override fun onShowFileChooser(
webView: WebView?,
valueCallback: ValueCallback<Array<Uri?>?>,
fileChooserParams: FileChooserParams?
): Boolean {
if (uploadMessage != null) {
uploadMessage!!.onReceiveValue(null)
uploadMessage = null
}
uploadMessage = valueCallback
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
try {
startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
} catch (e: ActivityNotFoundException) {
// display error to the user
}
return true
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
if (uploadMessage != null && data != null) {
val bitmapUri = getImageUri(this.applicationContext, data.extras?.get("data") as Bitmap)
uploadMessage?.onReceiveValue(arrayOf(bitmapUri))
uploadMessage = null
}
}
super.onActivityResult(requestCode, resultCode, data)
}
private fun getImageUri(inContext: Context, image: Bitmap): Uri? {
val bytes = ByteArrayOutputStream()
image.compress(Bitmap.CompressFormat.JPEG, 100, bytes)
val path = MediaStore.Images.Media.insertImage(inContext.getContentResolver(), image, "", null)
return Uri.parse(path)
}
}