# Get Courses Source: https://docs.doshi.app/api-reference/academy/get-courses /client.json get /client/academy/courses Fetches a list of available courses for the user's institution. # Get Lesson Submissions Source: https://docs.doshi.app/api-reference/academy/get-lesson-submissions /client.json get /client/academy/lesson-submissions Fetches lesson submission data, with optional filters. # Get Lessons Source: https://docs.doshi.app/api-reference/academy/get-lessons /client.json get /client/academy/lessons Fetches a list of available lessons, including core and institution-specific lessons. # Get Paths Source: https://docs.doshi.app/api-reference/academy/get-paths /client.json get /client/academy/paths Fetches a list of available learning paths for the user's institution. # Get Custom Authentication Token Source: https://docs.doshi.app/api-reference/auth/get-custom-authentication-token /client.json post /client/auth/custom-token Generates a custom Firebase authentication token for a user. Requires either email or partnerUserId. Does not support 2FA/OTP flow. # Get Redeemed Rewards Source: https://docs.doshi.app/api-reference/marketplace/get-redeemed-rewards /client.json get /client/marketplace/redeemed-rewards Fetches a list of rewards redeemed by users, filterable by userId. # Get Rewards Source: https://docs.doshi.app/api-reference/marketplace/get-rewards /client.json get /client/marketplace/rewards Fetches a list of available rewards in the marketplace for the user's institution. # Get Quest Attempts Source: https://docs.doshi.app/api-reference/quests/get-quest-attempts /client.json get /client/quests/attempts Fetches quest attempt records. # Submit Quest Result Source: https://docs.doshi.app/api-reference/quests/submit-quest-result /client.json post /client/quests/submit Submits a quest result for a user by a partner. # Get Sessions Source: https://docs.doshi.app/api-reference/user/get-sessions /client.json get /client/user/sessions Fetches user session data with durations. Returns sessions filtered by institution. # Get Survey Responses Source: https://docs.doshi.app/api-reference/user/get-survey-responses /client.json get /client/user/survey-responses Fetches survey response submissions. Returns answer submissions where answerType is 'survey'. # Get User Source: https://docs.doshi.app/api-reference/user/get-user /client.json get /client/user/{partnerUserId} Get user details by partnerUserId. Only returns a subset of user fields. # Get User Attributes Source: https://docs.doshi.app/api-reference/user/get-user-attributes /client.json get /client/user/attributes Fetches user attribute data. # Get Users Source: https://docs.doshi.app/api-reference/user/get-users /client.json get /client/users Fetches a list of users for the institution, optionally filtered by branchId. # Best Practices Source: https://docs.doshi.app/webview/best-practices Optimization tips and best practices for webview authentication ## Security Best Practices ### API Key Management **Critical**: Never expose your API key in client-side code. Always call the Doshi API from your backend. ```typescript theme={null} // ✅ Correct - Backend handles API key // backend/api/auth.ts app.post('/generate-doshi-token', async (req, res) => { const response = await fetch('https://api.doshi.app/client/auth/token', { headers: { 'Authorization': `Bearer ${process.env.DOSHI_API_KEY}`, 'Access-Type': 'client' } }); // ... }); // ❌ Never do this - API key exposed // frontend/app.tsx const response = await fetch('https://api.doshi.app/client/auth/token', { headers: { 'Authorization': 'Bearer sk_live_abc123...', // EXPOSED! 'Access-Type': 'client' } }); ``` ### Environment Variables Store your API key securely: ```bash theme={null} # .env DOSHI_API_KEY=your_api_key_here DOSHI_API_URL=https://api.doshi.app ``` ```typescript theme={null} // config.ts export const config = { doshiApiKey: process.env.DOSHI_API_KEY, doshiApiUrl: process.env.DOSHI_API_URL, }; // Never commit .env files // Add to .gitignore: // .env // .env.local ``` ### Always Use HTTPS ```javascript theme={null} // ✅ Correct const webviewUrl = "https://embed.v2.doshi.app"; const apiUrl = "https://api.doshi.app"; // ❌ Never use HTTP in production const insecureUrl = "http://embed.v2.doshi.app"; ``` ### Implement Proper Origin Checks ```javascript theme={null} // For postMessage window.addEventListener("message", (event) => { // ✅ Always verify origin in production if (event.origin !== 'https://embed.v2.doshi.app') { console.warn('Rejected message from:', event.origin); return; } // Process message }); ``` ### Never Use Wildcards in Production ```javascript theme={null} // ❌ Development/testing only webview.contentWindow.postMessage(data, "*"); // ✅ Production - always specify exact origin webview.contentWindow.postMessage(data, "https://embed.v2.doshi.app"); ``` ### Validate All Data ```typescript theme={null} function validateAuthData(data: any): data is AuthData { if (!data || typeof data !== 'object') { throw new Error('Invalid data format'); } if (!data.token || typeof data.token !== 'string') { throw new Error('Missing or invalid token'); } if (data.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) { throw new Error('Invalid email format'); } return true; } // Use in your code try { validateAuthData(authData); // Proceed with authentication } catch (error) { console.error('Validation failed:', error); // Handle error } ``` ## Performance Best Practices ### Minimize API Calls Cache the custom token temporarily if you need to render multiple iframes for the same user session ```typescript theme={null} // Token cache with expiration class TokenCache { private cache = new Map(); private readonly TTL = 5 * 60 * 1000; // 5 minutes async getToken(userId: string): Promise { const cached = this.cache.get(userId); if (cached && cached.expires > Date.now()) { return cached.token; } // Fetch new token const token = await this.fetchNewToken(userId); this.cache.set(userId, { token, expires: Date.now() + this.TTL }); return token; } private async fetchNewToken(userId: string): Promise { // Call your backend const response = await fetch('/api/generate-doshi-token', { method: 'POST', body: JSON.stringify({ userId }) }); const data = await response.json(); return data.token; } } const tokenCache = new TokenCache(); ``` ### Optimize Message Size ```javascript theme={null} // ✅ Send all data in a single message const authData = { token: user.token, email: user.email, segment: user.segment, branchId: user.branchId, type: "AUTH" }; webview.contentWindow.postMessage(JSON.stringify(authData), origin); // ❌ Avoid multiple sequential messages webview.contentWindow.postMessage(JSON.stringify({ token }), origin); webview.contentWindow.postMessage(JSON.stringify({ email }), origin); webview.contentWindow.postMessage(JSON.stringify({ segment }), origin); ``` ### Implement Proper Cleanup ```typescript theme={null} useEffect(() => { const handleMessage = (event: MessageEvent) => { // Handle message }; window.addEventListener("message", handleMessage); // ✅ Always cleanup return () => { window.removeEventListener("message", handleMessage); }; }, []); ``` ### Lazy Load the Iframe ```tsx theme={null} import React, { useState, useEffect } from 'react'; const LazyWebview: React.FC = () => { const [shouldLoad, setShouldLoad] = useState(false); useEffect(() => { // Load iframe only when needed const timer = setTimeout(() => { setShouldLoad(true); }, 1000); return () => clearTimeout(timer); }, []); if (!shouldLoad) { return
Preparing...
; } return ; }; ``` ### Preconnect to Doshi Domain ```html theme={null} ``` ## Mobile App Integration When embedding Doshi Frontend in a mobile app (iOS/Android WebView), there are special considerations: ### Disable Zoom Zooming should be disabled on mobile to prevent layout issues and ensure consistent user experience. **For Web (HTML Meta Tag):** ```html theme={null} ``` **For React:** ```tsx theme={null} // Add to your main HTML file or use react-helmet import { Helmet } from 'react-helmet'; function App() { return ( <> ); } ``` **For iOS (Swift/UIWebView):** ```swift theme={null} // Disable pinch to zoom webView.scrollView.isScrollEnabled = false webView.scrollView.bounces = false // For WKWebView let webConfiguration = WKWebViewConfiguration() webView = WKWebView(frame: .zero, configuration: webConfiguration) webView.scrollView.isScrollEnabled = false // Inject viewport meta tag let viewportScript = """ var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'); document.getElementsByTagName('head')[0].appendChild(meta); """ let userScript = WKUserScript(source: viewportScript, injectionTime: .atDocumentEnd, forMainFrameOnly: true) webView.configuration.userContentController.addUserScript(userScript) ``` **For Android (WebView):** ```kotlin theme={null} // In your Activity or Fragment webView.settings.apply { setSupportZoom(false) builtInZoomControls = false displayZoomControls = false } // Load HTML with viewport meta tag val htmlContent = """ """ webView.loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null) ``` ### Handle Keyboard in Mobile Apps When the keyboard opens in mobile apps, it can squeeze the iframe height and make content not visible. Handle this in your app environment. The Doshi Frontend is designed to work within mobile WebViews, but your app needs to handle keyboard behavior correctly. **iOS (Swift) - Adjust WebView when Keyboard Opens:** ```swift theme={null} class ViewController: UIViewController { var webView: WKWebView! var webViewBottomConstraint: NSLayoutConstraint! override func viewDidLoad() { super.viewDidLoad() // Setup WebView with constraints webView = WKWebView() view.addSubview(webView) webView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), webView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) webViewBottomConstraint = webView.bottomAnchor.constraint(equalTo: view.bottomAnchor) webViewBottomConstraint.isActive = true // Observe keyboard notifications NotificationCenter.default.addObserver( self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil ) } @objc func keyboardWillShow(_ notification: Notification) { guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } // Adjust WebView bottom constraint webViewBottomConstraint.constant = -keyboardFrame.height UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() } } @objc func keyboardWillHide(_ notification: Notification) { // Reset WebView bottom constraint webViewBottomConstraint.constant = 0 UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() } } deinit { NotificationCenter.default.removeObserver(self) } } ``` **Android (Kotlin) - Adjust WebView when Keyboard Opens:** ```kotlin theme={null} class WebViewActivity : AppCompatActivity() { private lateinit var webView: WebView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Set window to adjust resize when keyboard appears window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) setContentView(R.layout.activity_webview) webView = findViewById(R.id.webView) webView.settings.apply { javaScriptEnabled = true domStorageEnabled = true } // Optional: Listen to keyboard visibility val rootView = findViewById(android.R.id.content) rootView.viewTreeObserver.addOnGlobalLayoutListener { val heightDiff = rootView.rootView.height - rootView.height if (heightDiff > 200) { // Keyboard is visible onKeyboardVisible(heightDiff) } else { // Keyboard is hidden onKeyboardHidden() } } } private fun onKeyboardVisible(keyboardHeight: Int) { // Adjust WebView or scroll to focused input webView.evaluateJavascript(""" (function() { var activeElement = document.activeElement; if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) { activeElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); } })(); """, null) } private fun onKeyboardHidden() { // Reset any adjustments if needed } } ``` **React Native (if applicable):** ```javascript theme={null} import { KeyboardAvoidingView, Platform } from 'react-native'; import { WebView } from 'react-native-webview'; function DoshiWebView() { return ( ); } ``` **In Manifest (Android):** ```xml theme={null} ``` ### Best Practices for Mobile Always disable zoom for consistent experience Handle keyboard visibility in your app wrapper Use `adjustResize` on Android to resize layout when keyboard appears Use constraint-based layout on iOS to adjust for keyboard Test on various screen sizes and keyboard types Ensure input fields remain visible when keyboard is open ## Handling Link Clicks Doshi Frontend cannot handle external link clicks that open in new windows/popups due to security restrictions. ### Why Popups Are Blocked For security reasons, Doshi Frontend does not support opening links in popups or new windows from within the iframe. This prevents: * Phishing attacks * Unauthorized redirects * Cross-site scripting vulnerabilities ### postMessage Callback for Link Clicks When users click on external links within Doshi Frontend, the iframe will send a postMessage to your parent application. You must handle these messages and open links in your environment. **Listen for Link Click Messages:** ```typescript theme={null} window.addEventListener('message', (event) => { // Verify origin if (event.origin !== 'https://embed.v2.doshi.app') return; try { const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; // Handle external link clicks if (data.type === 'EXTERNAL_LINK') { const { url, title } = data; // Open in same tab window.location.href = url; // Or open in new tab // window.open(url, '_blank'); // Or handle in your app's navigation // navigateToExternal(url); } } catch (error) { console.error('Error handling message:', error); } }); ``` **iOS (Swift) - Handle Link Callbacks:** ```swift theme={null} // Add WKScriptMessageHandler extension ViewController: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { if message.name == "linkHandler" { if let body = message.body as? [String: Any], let url = body["url"] as? String { // Handle the link if let linkUrl = URL(string: url) { // Option 1: Open in Safari UIApplication.shared.open(linkUrl) // Option 2: Open in SFSafariViewController let safariVC = SFSafariViewController(url: linkUrl) present(safariVC, animated: true) } } } } } // Setup message handler override func viewDidLoad() { super.viewDidLoad() let contentController = webView.configuration.userContentController contentController.add(self, name: "linkHandler") // Inject message handler script let script = """ window.addEventListener('message', function(event) { if (event.data.type === 'EXTERNAL_LINK') { window.webkit.messageHandlers.linkHandler.postMessage({ url: event.data.url, title: event.data.title }); } }); """ let userScript = WKUserScript( source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: false ) contentController.addUserScript(userScript) } ``` **Android (Kotlin) - Handle Link Callbacks:** ```kotlin theme={null} webView.webViewClient = object : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { val url = request?.url.toString() // If it's an external link, handle it in your app if (url.startsWith("http") && !url.contains("embed.v2.doshi.app")) { // Option 1: Open in external browser val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) startActivity(intent) return true // Option 2: Open in Chrome Custom Tabs val builder = CustomTabsIntent.Builder() val customTabsIntent = builder.build() customTabsIntent.launchUrl(this@WebViewActivity, Uri.parse(url)) return true } return false } } // Add JavaScript interface for postMessage webView.addJavascriptInterface(object { @JavascriptInterface fun handleExternalLink(url: String, title: String) { runOnUiThread { // Handle the link val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) startActivity(intent) } } }, "AndroidInterface") // Inject message listener webView.evaluateJavascript(""" window.addEventListener('message', function(event) { if (event.data.type === 'EXTERNAL_LINK') { AndroidInterface.handleExternalLink(event.data.url, event.data.title); } }); """, null) ``` **React - Handle Link Callbacks:** ```tsx theme={null} import React, { useEffect } from 'react'; function DoshiEmbed() { useEffect(() => { const handleMessage = (event: MessageEvent) => { if (event.origin !== 'https://embed.v2.doshi.app') return; try { const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; if (data.type === 'EXTERNAL_LINK') { // Option 1: Navigate in same window window.location.href = data.url; // Option 2: Open in new tab // window.open(data.url, '_blank', 'noopener,noreferrer'); // Option 3: Use React Router (if applicable) // navigate(`/external?url=${encodeURIComponent(data.url)}`); // Option 4: Show confirmation dialog // if (confirm(`Open ${data.title || 'link'}?`)) { // window.open(data.url, '_blank'); // } } } catch (error) { console.error('Error handling link message:', error); } }; window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); }, []); return ( ``` ### 4. Pass Token to Iframe Choose your preferred method: ```tsx postMessage theme={null} // Listen for PING from iframe window.addEventListener("message", (event) => { if (event.data.type === "PING") { iframe.contentWindow.postMessage( JSON.stringify({ token: "your_custom_token", email: "user@example.com", type: "AUTH" }), "https://embed.v2.doshi.app" ); } }); ``` ```tsx Query Parameters theme={null} const params = new URLSearchParams({ token: "your_nonce_token", email: "user@example.com", segment: "premium" }); const iframeUrl = `https://embed.v2.doshi.app?${params.toString()}`; ``` ## 2FA Support When 2FA is enabled for your organization, pass the required user information: ```typescript theme={null} const authData = { token: "your_custom_token", email: "user@example.com", is2FaEnabled: true, dob: "1990-01-15", organizationId: "org_123", partnerUserId: "partner_123", firstName: "John", lastName: "Doe" }; ``` The Doshi iframe will automatically: 1. Display the OTP input screen 2. Send the OTP to the user's phone 3. Verify the OTP 4. Complete authentication You don't need to call `/client/auth/send-otp` or `/client/auth/verify-otp` separately. The iframe handles the entire 2FA flow. ## Session Management Once authenticated, sessions are managed automatically: * **ID Token**: Valid for 1 hour * **Refresh Token**: Valid for 12 hours * **Auto-refresh**: Tokens are refreshed automatically by the iframe No action required from your application! ## Parameter Details ### segment The `segment` parameter is used for handling multiple learning paths under the same organization. This allows you to direct users to different educational journeys based on their needs or preferences. **Example:** ```typescript theme={null} { segment: "premium", // or "basic", "enterprise", etc. } ``` ### branchId Used to identify which branch or location the user belongs to within your organization. **Example:** ```typescript theme={null} { branchId: "branch_789", // Your branch identifier } ``` ## Mobile App Considerations When embedding Doshi Frontend in mobile apps: Prevent pinch-to-zoom for consistent UI Adjust layout when keyboard opens Implement link click callbacks See [Best Practices](/webview/best-practices) for detailed mobile implementation guides. ## Next Steps Complete walkthrough with examples Security best practices Detailed postMessage implementation # postMessage Authentication Source: https://docs.doshi.app/webview/postmessage Implement secure real-time authentication using the postMessage API ## Overview The postMessage API provides secure, real-time cross-origin communication between your parent window and the embedded webview. This is the recommended method for handling sensitive authentication data. Examples use the production host. On sandbox, both the origin you verify and the `targetOrigin` you post to must be `https://sandbox.embed.doshi.app`. See [Environments](/webview/environments). ## How It Works Your backend calls the Doshi API to generate a custom token The embedded webview sends a PING message to signal it's ready Parent window listens for the PING and prepares authentication data Parent sends the custom token and user data back to the webview Webview receives and processes the authentication, handling 2FA if needed ## TypeScript Interfaces ```typescript theme={null} interface AuthData { token: string; // Required - custom token from API email?: string; // Optional - user's email segment?: string; // Optional - for multiple paths branchId?: string; // Optional - branch identifier // 2FA Parameters (optional, required when is2FaEnabled is true) is2FaEnabled?: boolean; // Enable 2FA flow in iframe dob?: string; // Date of birth (YYYY-MM-DD) organizationId?: string; // Organization ID partnerUserId?: string; // Partner user ID firstName?: string; // First name lastName?: string; // Last name } interface WebviewMessage { type: string; [key: string]: any; } ``` `pathId` is not a frontend parameter. Adding it to the `AUTH` payload has no effect — it is silently ignored. Set the starting path when you generate the token instead. See [Setting the Starting Learning Path](/webview/quickstart#setting-the-starting-learning-path). ## React Implementation ### Complete Example with API Call ```tsx theme={null} import React, { useEffect, useState } from "react"; interface AuthData { token: string; email?: string; segment?: string; branchId?: string; is2FaEnabled?: boolean; dob?: string; organizationId?: string; partnerUserId?: string; firstName?: string; lastName?: string; } interface WebviewMessage { type: string; [key: string]: any; } interface Props { userEmail: string; segment?: string; branchId?: string; // 2FA props is2FaEnabled?: boolean; dob?: string; organizationId?: string; partnerUserId?: string; firstName?: string; lastName?: string; } const WebviewComponent: React.FC = ({ userEmail, segment, branchId, is2FaEnabled = false, dob, organizationId, partnerUserId, firstName, lastName }) => { const [authToken, setAuthToken] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); // Step 1: Get nonce token from your backend useEffect(() => { async function fetchToken() { try { // Call YOUR backend which securely calls Doshi API const response = await fetch('/api/generate-doshi-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: userEmail, branchId }) }); if (!response.ok) { throw new Error('Failed to generate token'); } const data = await response.json(); setAuthToken(data.token); } catch (error) { console.error('Failed to fetch auth token:', error); setError('Authentication failed. Please try again.'); } finally { setIsLoading(false); } } fetchToken(); }, [userEmail, branchId]); // Step 2: Setup postMessage listener useEffect(() => { if (!authToken) return; const handleMessage = (event: MessageEvent) => { // ✅ Verify origin in production if (event.origin !== 'https://embed.v2.doshi.app') { console.warn('Rejected message from:', event.origin); return; } try { const data: WebviewMessage = typeof event.data === "string" ? JSON.parse(event.data) : event.data; // When we receive PING from webview, send the auth data if (data.type === "PING") { handleSendAuth(); } } catch (error) { console.error("Error processing message:", error); } }; window.addEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage); }, [authToken]); const handleSendAuth = () => { if (!authToken) return; // Build auth data object const authData: AuthData = { token: authToken, type: "AUTH" }; // Add optional fields if (userEmail) authData.email = userEmail; if (segment) authData.segment = segment; if (branchId) authData.branchId = branchId; // Add 2FA fields if enabled if (is2FaEnabled) { authData.is2FaEnabled = true; if (dob) authData.dob = dob; if (organizationId) authData.organizationId = organizationId; if (partnerUserId) authData.partnerUserId = partnerUserId; if (firstName) authData.firstName = firstName; if (lastName) authData.lastName = lastName; } // Send to iframe const webview = document.querySelector("iframe"); if (webview && webview.contentWindow) { webview.contentWindow.postMessage( JSON.stringify(authData), "https://embed.v2.doshi.app" // ⚠️ Never use "*" in production ); } }; if (isLoading) { return (

Loading...

); } if (error) { return (

{error}

); } if (!authToken) { return
Failed to authenticate
; } return (