Skip to content

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:

  1. Iframe (web applications) - Embed the portal in an iframe within your web application
  2. 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:

FeatureIframe (web)Native WebView
External URL openingAutomaticRequires handler
Document downloadsAutomaticRequires handler
Calendar eventsAutomaticRequires handler
Lifecycle eventsSends events*Requires handler
Exit handlingSends events*Requires handler

*While iframes send lifecycle and exit events automatically, implementing handlers is recommended for better integration with your application state.

Note

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:

Copied
1{
2 "event": "authenticated" | "session-expired"
3}

Actions - Requests sent from the portal asking the host to perform an operation:

Copied
1{
2 "action": "open-external-url" | "download-document" | "add-to-calendar" | "exit",
3 // Additional properties depending on action type
4}

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.

Copied
1{
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.

Copied
1{
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.

Copied
1{
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.

Copied
1{
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:

  1. Download the file from the provided URL
  2. Save it to the device or present a share sheet
  3. Handle any download errors appropriately
Note

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.

Copied
1{
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:

  1. Parse the data URL to extract the ICS content
  2. Use the platform's native calendar APIs to create an event (e.g., EventKit on iOS, CalendarContract on Android)
  3. Optionally prompt the user before adding the event

Exit

Sent when the user requests to exit the portal and return to your application.

Copied
1{
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.

Note

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:

PlatformDetectionMessage handler
React Nativewindow.ReactNativeWebViewonMessage event
Flutterwindow.flutter_inappwebviewtaxfyleMessageHandler
iOS WKWebViewwindow.webkitnativeApp message handler
Android WebViewwindow.AndroidAndroid.postMessage()

React Native implementation

Copied
1import { WebView } from 'react-native-webview'
2import { Linking } from 'react-native'
3
4function PortalWebView({ magicLinkUrl, onAuthenticated, onSessionExpired, onExit }) {
5 const handleMessage = (event) => {
6 const message = JSON.parse(event.nativeEvent.data)
7
8 // Handle events
9 if (message.event === 'authenticated') {
10 onAuthenticated?.()
11 } else if (message.event === 'session-expired') {
12 onSessionExpired?.()
13 }
14
15 // Handle actions
16 if (message.action === 'open-external-url') {
17 Linking.openURL(message.url)
18 } else if (message.action === 'download-document') {
19 // Download the file from message.downloadUrl
20 downloadFile(message.downloadUrl)
21 } else if (message.action === 'add-to-calendar') {
22 // Parse ICS data and add to calendar
23 addToCalendar(message.icsDataUrl)
24 } else if (message.action === 'exit') {
25 onExit?.()
26 }
27 }
28
29 return <WebView source={{ uri: magicLinkUrl }} onMessage={handleMessage} />
30}

iOS WKWebView implementation

Copied
1import WebKit
2
3class PortalViewController: UIViewController, WKScriptMessageHandler {
4 var webView: WKWebView!
5
6 override func viewDidLoad() {
7 super.viewDidLoad()
8
9 let config = WKWebViewConfiguration()
10 config.userContentController.add(self, name: "nativeApp")
11
12 webView = WKWebView(frame: view.bounds, configuration: config)
13 view.addSubview(webView)
14
15 if let url = URL(string: magicLinkUrl) {
16 webView.load(URLRequest(url: url))
17 }
18 }
19
20 func userContentController(
21 _ userContentController: WKUserContentController,
22 didReceive message: WKScriptMessage
23 ) {
24 guard let body = message.body as? [String: Any] else { return }
25
26 // Handle events
27 if let event = body["event"] as? String {
28 switch event {
29 case "authenticated":
30 handleAuthenticated()
31 case "session-expired":
32 handleSessionExpired()
33 default:
34 break
35 }
36 }
37
38 // Handle actions
39 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 break
58 }
59 }
60 }
61}

Android WebView implementation

Copied
1import android.webkit.WebView
2import android.webkit.JavascriptInterface
3import android.content.Intent
4import android.net.Uri
5import org.json.JSONObject
6
7class PortalActivity : AppCompatActivity() {
8 private lateinit var webView: WebView
9
10 override fun onCreate(savedInstanceState: Bundle?) {
11 super.onCreate(savedInstanceState)
12
13 webView = WebView(this).apply {
14 settings.javaScriptEnabled = true
15 addJavascriptInterface(PortalBridge(), "Android")
16 loadUrl(magicLinkUrl)
17 }
18
19 setContentView(webView)
20 }
21
22 inner class PortalBridge {
23 @JavascriptInterface
24 fun postMessage(messageJson: String) {
25 val message = JSONObject(messageJson)
26
27 // Handle events
28 when (message.optString("event")) {
29 "authenticated" -> handleAuthenticated()
30 "session-expired" -> handleSessionExpired()
31 }
32
33 // Handle actions
34 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

Copied
1import 'package:flutter_inappwebview/flutter_inappwebview.dart';
2import 'dart:convert';
3
4class PortalWebView extends StatefulWidget {
5 final String magicLinkUrl;
6 final VoidCallback? onAuthenticated;
7 final VoidCallback? onSessionExpired;
8 final VoidCallback? onExit;
9
10 @override
11 _PortalWebViewState createState() => _PortalWebViewState();
12}
13
14class _PortalWebViewState extends State<PortalWebView> {
15 @override
16 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 String
25 ? jsonDecode(args[0])
26 : args[0];
27 handleMessage(message);
28 },
29 );
30 },
31 );
32 }
33
34 void handleMessage(Map<String, dynamic> message) {
35 // Handle events
36 switch (message['event']) {
37 case 'authenticated':
38 widget.onAuthenticated?.call();
39 break;
40 case 'session-expired':
41 widget.onSessionExpired?.call();
42 break;
43 }
44
45 // Handle actions
46 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

Copied
1// Replace with your portal domain
2const PORTAL_ORIGIN = 'https://<slug>.worklayer.com'
3
4window.addEventListener('message', (event) => {
5 // Verify the origin of the message for security
6 if (event.origin !== PORTAL_ORIGIN) {
7 return
8 }
9
10 const message = event.data
11
12 // Ensure message is a valid object
13 if (typeof message !== 'object' || message === null) {
14 return
15 }
16
17 // Handle events
18 if (message.event) {
19 switch (message.event) {
20 case 'authenticated':
21 console.log('User authenticated in portal')
22 // Update your application state
23 break
24 case 'session-expired':
25 console.log('Portal session expired')
26 // Generate a new magic link or prompt re-authentication
27 break
28 }
29 }
30
31 // Handle actions
32 if (message.action === 'exit') {
33 console.log('User requested to exit portal')
34 // Navigate away from the portal or close the iframe
35 }
36})
Last updated on December 13, 2025