Portal Embedding
Embed the Worklayer client portal in your application using iframes or native WebViews
The Worklayer client portal can be embedded in your application to provide a seamless experience for your users. When embedding the portal, certain actions and events can be delegated to your host application through a message-based protocol.
Embedding methods
There are two primary ways to embed the portal:
- Iframe (web applications) - Embed the portal in an iframe within your web application
- Native WebView (mobile applications) - Embed the portal in a WebView component in your iOS or Android application
The portal automatically detects which environment it is running in and adjusts its behavior accordingly. Many features work automatically in iframe contexts but require explicit handling in native WebView contexts.
Feature compatibility
The following table summarizes which features work automatically and which require implementation in your host application:
| Feature | Iframe (web) | Native WebView |
|---|---|---|
| External URL opening | Automatic | Requires handler |
| Document downloads | Automatic | Requires handler |
| Calendar events | Automatic | Requires handler |
| Lifecycle events | Sends events* | Requires handler |
| Exit handling | Sends events* | Requires handler |
*While iframes send lifecycle and exit events automatically, implementing handlers is recommended for better integration with your application state.
When embedding in an iframe, the portal uses standard browser capabilities like window.open() for external URLs and
form submissions for downloads. These work without any additional setup from your application.
Message protocol
The portal communicates with the host application using a simple message protocol. Messages are sent using postMessage for iframes or platform-specific bridges for native WebViews.
Message types
There are two categories of messages:
Events - Notifications sent from the portal to inform the host of state changes:
Copied1{2 "event": "authenticated" | "session-expired"3}
Actions - Requests sent from the portal asking the host to perform an operation:
Copied1{2 "action": "open-external-url" | "download-document" | "add-to-calendar" | "exit",3 // Additional properties depending on action type4}
Lifecycle events
The portal emits lifecycle events to notify your application of authentication state changes.
Authenticated event
Sent when a user successfully authenticates via a Magic Link.
Copied1{2 "event": "authenticated"3}
Use this event to:
- Update your application's authentication state
- Track successful portal sessions
- Trigger any post-authentication flows in your application
Session expired event
Sent when the user's session has expired or when magic link token exchange fails.
Copied1{2 "event": "session-expired"3}
Use this event to:
- Prompt the user to re-authenticate
- Generate a new magic link for the user
- Clean up any session-related state in your application
Action requests
When the portal needs to perform certain actions, it sends action requests to the host application. In iframe contexts, these actions are handled automatically by the browser. In native WebView contexts, your application must implement handlers for these actions.
Open external URL
Sent when the user needs to open an external URL, such as joining a video consultation room.
Copied1{2 "action": "open-external-url",3 "url": "https://example.com/consultation-room/abc123"4}
Iframe behavior: Opens in a new browser window using window.open().
Native implementation: Your application should open the URL using the platform's native URL handling (e.g., UIApplication.open on iOS, Intent.ACTION_VIEW on Android).
Download document
Sent when the user requests to download a document.
Copied1{2 "action": "download-document",3 "downloadUrl": "https://api.worklayer.com/v3/documents/.../download?signature=..."4}
Iframe behavior: The portal handles downloads automatically using form submission with the user's authorization token.
Native implementation: Your application receives a pre-signed URL that can be used to download the document directly. You should:
- Download the file from the provided URL
- Save it to the device or present a share sheet
- Handle any download errors appropriately
The downloadUrl provided to native applications is a signed URL that does not require additional authentication
headers. It is valid for a limited time.
Add to calendar
Sent when the user wants to add a consultation or appointment to their calendar.
Copied1{2 "action": "add-to-calendar",3 "icsDataUrl": "data:..."4}
Iframe behavior: The portal creates a downloadable .ics file that the user can open with their default calendar application.
Native implementation: Your application receives the calendar event as a data URL containing iCalendar (ICS) format data. You should:
- Parse the data URL to extract the ICS content
- Use the platform's native calendar APIs to create an event (e.g.,
EventKiton iOS,CalendarContracton Android) - Optionally prompt the user before adding the event
Exit
Sent when the user requests to exit the portal and return to your application.
Copied1{2 "action": "exit"3}
Iframe behavior: The portal attempts to notify the parent window and then redirects to the return URL configured in the Magic Link.
Native implementation: Your application should close the WebView and return the user to the appropriate screen in your application.
The exit action is sent when a return URL was configured in the magic link's return_options. If no return URL is
configured, the portal will not send this action.
Native WebView implementation
When embedding the portal in a native WebView, you must implement a message handler to receive and process messages from the portal.
Platform detection
The portal detects native environments by checking for the presence of platform-specific JavaScript bridges:
| Platform | Detection | Message handler |
|---|---|---|
| React Native | window.ReactNativeWebView | onMessage event |
| Flutter | window.flutter_inappwebview | taxfyleMessageHandler |
| iOS WKWebView | window.webkit | nativeApp message handler |
| Android WebView | window.Android | Android.postMessage() |
React Native implementation
Copied1import { WebView } from 'react-native-webview'2import { Linking } from 'react-native'34function PortalWebView({ magicLinkUrl, onAuthenticated, onSessionExpired, onExit }) {5 const handleMessage = (event) => {6 const message = JSON.parse(event.nativeEvent.data)78 // Handle events9 if (message.event === 'authenticated') {10 onAuthenticated?.()11 } else if (message.event === 'session-expired') {12 onSessionExpired?.()13 }1415 // Handle actions16 if (message.action === 'open-external-url') {17 Linking.openURL(message.url)18 } else if (message.action === 'download-document') {19 // Download the file from message.downloadUrl20 downloadFile(message.downloadUrl)21 } else if (message.action === 'add-to-calendar') {22 // Parse ICS data and add to calendar23 addToCalendar(message.icsDataUrl)24 } else if (message.action === 'exit') {25 onExit?.()26 }27 }2829 return <WebView source={{ uri: magicLinkUrl }} onMessage={handleMessage} />30}
iOS WKWebView implementation
Copied1import WebKit23class PortalViewController: UIViewController, WKScriptMessageHandler {4 var webView: WKWebView!56 override func viewDidLoad() {7 super.viewDidLoad()89 let config = WKWebViewConfiguration()10 config.userContentController.add(self, name: "nativeApp")1112 webView = WKWebView(frame: view.bounds, configuration: config)13 view.addSubview(webView)1415 if let url = URL(string: magicLinkUrl) {16 webView.load(URLRequest(url: url))17 }18 }1920 func userContentController(21 _ userContentController: WKUserContentController,22 didReceive message: WKScriptMessage23 ) {24 guard let body = message.body as? [String: Any] else { return }2526 // Handle events27 if let event = body["event"] as? String {28 switch event {29 case "authenticated":30 handleAuthenticated()31 case "session-expired":32 handleSessionExpired()33 default:34 break35 }36 }3738 // Handle actions39 if let action = body["action"] as? String {40 switch action {41 case "open-external-url":42 if let urlString = body["url"] as? String,43 let url = URL(string: urlString) {44 UIApplication.shared.open(url)45 }46 case "download-document":47 if let urlString = body["downloadUrl"] as? String {48 downloadDocument(from: urlString)49 }50 case "add-to-calendar":51 if let icsDataUrl = body["icsDataUrl"] as? String {52 addToCalendar(icsDataUrl: icsDataUrl)53 }54 case "exit":55 dismiss(animated: true)56 default:57 break58 }59 }60 }61}
Android WebView implementation
Copied1import android.webkit.WebView2import android.webkit.JavascriptInterface3import android.content.Intent4import android.net.Uri5import org.json.JSONObject67class PortalActivity : AppCompatActivity() {8 private lateinit var webView: WebView910 override fun onCreate(savedInstanceState: Bundle?) {11 super.onCreate(savedInstanceState)1213 webView = WebView(this).apply {14 settings.javaScriptEnabled = true15 addJavascriptInterface(PortalBridge(), "Android")16 loadUrl(magicLinkUrl)17 }1819 setContentView(webView)20 }2122 inner class PortalBridge {23 @JavascriptInterface24 fun postMessage(messageJson: String) {25 val message = JSONObject(messageJson)2627 // Handle events28 when (message.optString("event")) {29 "authenticated" -> handleAuthenticated()30 "session-expired" -> handleSessionExpired()31 }3233 // Handle actions34 when (message.optString("action")) {35 "open-external-url" -> {36 val url = message.getString("url")37 startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))38 }39 "download-document" -> {40 val downloadUrl = message.getString("downloadUrl")41 downloadDocument(downloadUrl)42 }43 "add-to-calendar" -> {44 val icsDataUrl = message.getString("icsDataUrl")45 addToCalendar(icsDataUrl)46 }47 "exit" -> finish()48 }49 }50 }51}
Flutter implementation
Copied1import 'package:flutter_inappwebview/flutter_inappwebview.dart';2import 'dart:convert';34class PortalWebView extends StatefulWidget {5 final String magicLinkUrl;6 final VoidCallback? onAuthenticated;7 final VoidCallback? onSessionExpired;8 final VoidCallback? onExit;910 @override11 _PortalWebViewState createState() => _PortalWebViewState();12}1314class _PortalWebViewState extends State<PortalWebView> {15 @override16 Widget build(BuildContext context) {17 return InAppWebView(18 initialUrlRequest: URLRequest(url: Uri.parse(widget.magicLinkUrl)),19 onWebViewCreated: (controller) {20 controller.addJavaScriptHandler(21 handlerName: 'taxfyleMessageHandler',22 callback: (args) {23 if (args.isEmpty) return;24 final message = args[0] is String25 ? jsonDecode(args[0])26 : args[0];27 handleMessage(message);28 },29 );30 },31 );32 }3334 void handleMessage(Map<String, dynamic> message) {35 // Handle events36 switch (message['event']) {37 case 'authenticated':38 widget.onAuthenticated?.call();39 break;40 case 'session-expired':41 widget.onSessionExpired?.call();42 break;43 }4445 // Handle actions46 switch (message['action']) {47 case 'open-external-url':48 launchUrl(Uri.parse(message['url']));49 break;50 case 'download-document':51 downloadDocument(message['downloadUrl']);52 break;53 case 'add-to-calendar':54 addToCalendar(message['icsDataUrl']);55 break;56 case 'exit':57 widget.onExit?.call();58 break;59 }60 }61}
Iframe implementation
When embedding the portal in an iframe within a web application, most features work automatically. However, you should listen for lifecycle events to integrate with your application state.
Listening for events
Copied1// Replace with your portal domain2const PORTAL_ORIGIN = 'https://<slug>.worklayer.com'34window.addEventListener('message', (event) => {5 // Verify the origin of the message for security6 if (event.origin !== PORTAL_ORIGIN) {7 return8 }910 const message = event.data1112 // Ensure message is a valid object13 if (typeof message !== 'object' || message === null) {14 return15 }1617 // Handle events18 if (message.event) {19 switch (message.event) {20 case 'authenticated':21 console.log('User authenticated in portal')22 // Update your application state23 break24 case 'session-expired':25 console.log('Portal session expired')26 // Generate a new magic link or prompt re-authentication27 break28 }29 }3031 // Handle actions32 if (message.action === 'exit') {33 console.log('User requested to exit portal')34 // Navigate away from the portal or close the iframe35 }36})