# 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 (
);
}
```
### Message Format
When a user clicks an external link, Doshi Frontend sends:
```typescript theme={null}
{
type: 'EXTERNAL_LINK',
url: 'https://example.com/article',
title: 'Article Title', // Optional
timestamp: 1234567890
}
```
### Security Considerations
Always verify `event.origin` before processing messages
Validate URLs before opening (check for malicious protocols)
Use `noopener,noreferrer` when opening links in new tabs
Consider showing user confirmation for external links
Log external link clicks for security auditing
### URL Validation Example
```typescript theme={null}
function isValidUrl(url: string): boolean {
try {
const urlObj = new URL(url);
// Only allow http and https protocols
if (!['http:', 'https:'].includes(urlObj.protocol)) {
return false;
}
// Optional: Whitelist or blacklist domains
const blockedDomains = ['malicious-site.com', 'phishing-site.com'];
if (blockedDomains.some(domain => urlObj.hostname.includes(domain))) {
return false;
}
return true;
} catch {
return false;
}
}
// Use in message handler
if (data.type === 'EXTERNAL_LINK') {
if (isValidUrl(data.url)) {
window.open(data.url, '_blank', 'noopener,noreferrer');
} else {
console.warn('Invalid or blocked URL:', data.url);
}
}
```
## Error Handling Best Practices
### Comprehensive Error Handling
```typescript theme={null}
const handleAuthentication = async () => {
try {
// Attempt to get token
const token = await fetchToken();
if (!token) {
throw new Error('No token received');
}
// Attempt to send to iframe
await sendAuthToIframe(token);
} catch (error) {
// Log error
console.error('Authentication error:', error);
// Send to monitoring service
if (window.analytics) {
window.analytics.track('Authentication Failed', {
error: error.message,
timestamp: new Date().toISOString()
});
}
// Show user-friendly message
setError('Unable to authenticate. Please try again.');
// Optionally retry
if (retryCount < MAX_RETRIES) {
setTimeout(() => {
setRetryCount(retryCount + 1);
handleAuthentication();
}, RETRY_DELAY);
}
}
};
```
### User Feedback
```tsx theme={null}
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState(null);
// Show appropriate UI based on status
return (
{status === 'loading' && (
Authenticating...
)}
{status === 'error' && (
{errorMessage}
)}
{status === 'success' && (
)}
);
```
### Timeout Handling
```typescript theme={null}
const AUTH_TIMEOUT = 10000; // 10 seconds
const authenticateWithTimeout = () => {
return Promise.race([
authenticate(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Authentication timeout')), AUTH_TIMEOUT)
)
]);
};
try {
await authenticateWithTimeout();
} catch (error) {
if (error.message === 'Authentication timeout') {
setError('Authentication is taking longer than expected. Please check your connection.');
}
}
```
## Method Selection Guide
### When to Use postMessage
Handling sensitive authentication data
Need for real-time, bidirectional communication
Production environments
Multiple authentication steps
Maximum security requirements
Complex 2FA flows
```typescript theme={null}
// Use postMessage for production
const ProductionWebview = ({ userData }) => {
// postMessage implementation
// More secure, no data in URLs
};
```
### When to Use Query Parameters
Simple, one-time authentication
Rapid prototyping and development
Need for easier debugging
Simpler implementation requirements
Quick demos or proof of concepts
```typescript theme={null}
// Use query params for development/testing
const DevelopmentWebview = ({ userData }) => {
const url = `https://embed.v2.doshi.app?token=${token}`;
return ;
};
```
## Code Organization
### Separation of Concerns
```typescript theme={null}
// services/doshi-auth.service.ts
export class DoshiAuthService {
private apiUrl: string;
private embedUrl: string;
constructor(config: Config) {
this.apiUrl = config.apiUrl;
this.embedUrl = config.embedUrl;
}
async getAuthToken(userData: UserData): Promise {
const response = await fetch(`${this.apiUrl}/generate-token`, {
method: 'POST',
body: JSON.stringify(userData)
});
const data = await response.json();
return data.token;
}
buildEmbedUrl(token: string, params: AuthParams): string {
const urlParams = new URLSearchParams({ token, ...params });
return `${this.embedUrl}?${urlParams.toString()}`;
}
}
// components/DoshiEmbed.tsx
export const DoshiEmbed: React.FC = ({ userData }) => {
const authService = useDoshiAuthService();
// Component logic
};
```
### Configuration Management
```typescript theme={null}
// config/doshi.config.ts
export const doshiConfig = {
development: {
apiUrl: 'https://sandbox.api.doshi.app',
embedUrl: 'https://staging-embed.doshi.app',
enableDebugLogs: true,
strictOriginCheck: false
},
production: {
apiUrl: 'https://api.doshi.app',
embedUrl: 'https://embed.v2.doshi.app',
enableDebugLogs: false,
strictOriginCheck: true
}
};
export const getConfig = () => {
const env = process.env.NODE_ENV || 'development';
return doshiConfig[env];
};
```
## Styling Best Practices
### Responsive Container
```css theme={null}
.webview-container {
/* Full viewport height */
width: 100%;
height: 100vh;
/* Prevent overflow */
overflow: hidden;
/* Optional: Add subtle border */
border: 1px solid #e5e7eb;
border-radius: 8px;
}
/* Mobile responsive */
@media (max-width: 768px) {
.webview-container {
/* Adjust for mobile viewport */
height: calc(100vh - 60px); /* Account for mobile browser UI */
}
}
/* Tablet */
@media (min-width: 769px) and (max-width: 1024px) {
.webview-container {
height: calc(100vh - 80px);
}
}
```
### Loading States
```tsx theme={null}
const LoadingOverlay: React.FC = () => (
Loading Doshi...
);
// Usage
{isLoading && }
```
### Full-Screen Support
```css theme={null}
.webview-container iframe {
width: 100%;
height: 100%;
border: none;
display: block;
}
/* Allow fullscreen */
.webview-container iframe:fullscreen {
width: 100vw;
height: 100vh;
}
```
## Testing Best Practices
### Environment-Specific Testing
```typescript theme={null}
describe('Doshi Authentication', () => {
beforeEach(() => {
// Use sandbox environment for testing
process.env.DOSHI_API_URL = 'https://sandbox.api.doshi.app';
});
it('should generate auth token', async () => {
const token = await generateAuthToken({
email: 'test@example.com'
});
expect(token).toBeDefined();
expect(typeof token).toBe('string');
});
it('should handle authentication errors', async () => {
// Test error handling
await expect(
generateAuthToken({ email: 'invalid' })
).rejects.toThrow();
});
});
```
### Mock API Responses
```typescript theme={null}
// __mocks__/doshi-api.ts
export const mockDoshiApi = {
generateToken: jest.fn().mockResolvedValue({
token: 'mock_token_123',
userId: 'user_123',
isNewUser: false
}),
mockError: jest.fn().mockRejectedValue(
new Error('API Error')
)
};
```
### Integration Testing
```typescript theme={null}
import { render, screen, waitFor } from '@testing-library/react';
test('displays webview after successful authentication', async () => {
render();
// Should show loading initially
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Should show iframe after auth
await waitFor(() => {
expect(screen.getByTitle('Doshi Embed')).toBeInTheDocument();
});
});
```
## Monitoring and Logging
### Track Key Events
```typescript theme={null}
// Track authentication flow
const trackAuthEvent = (event: string, metadata?: any) => {
if (window.analytics) {
window.analytics.track(event, {
...metadata,
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV
});
}
};
// Usage
trackAuthEvent('Auth Token Requested', { userId: user.id });
trackAuthEvent('Auth Token Received', { userId: user.id });
trackAuthEvent('Iframe Loaded', { userId: user.id });
```
### Error Monitoring
```typescript theme={null}
// Send errors to monitoring service
const logError = (error: Error, context?: any) => {
console.error('Doshi Auth Error:', error);
// Send to error tracking service (e.g., Sentry)
if (window.Sentry) {
window.Sentry.captureException(error, {
extra: context
});
}
};
// Usage
try {
await authenticate();
} catch (error) {
logError(error, {
userId: user.id,
action: 'authentication'
});
}
```
### Performance Monitoring
```typescript theme={null}
// Measure authentication performance
const measureAuthPerformance = async () => {
const startTime = performance.now();
try {
await authenticate();
const endTime = performance.now();
const duration = endTime - startTime;
// Log performance metric
console.log(`Authentication took ${duration}ms`);
// Send to analytics
if (window.analytics) {
window.analytics.track('Auth Performance', {
duration,
status: 'success'
});
}
} catch (error) {
const endTime = performance.now();
const duration = endTime - startTime;
if (window.analytics) {
window.analytics.track('Auth Performance', {
duration,
status: 'error'
});
}
}
};
```
## Production Checklist
API key stored securely in environment variables
API calls made from backend only
HTTPS used for all URLs
Origin verification enabled for postMessage
No "\*" wildcards in production
Comprehensive error handling implemented
User feedback for all states (loading, error, success)
Timeout handling for slow connections
Proper cleanup of event listeners
Error logging and monitoring configured
Performance tracking in place
Tested across multiple browsers
Tested on mobile devices
Responsive design implemented
Accessibility considerations addressed
## Next Steps
Common issues and solutions
Review security considerations
# Environments
Source: https://docs.doshi.app/webview/environments
Production and sandbox hosts for the Doshi embed and API
## Overview
Doshi runs two environments: **production** and **sandbox**. Sandbox is a fully
isolated environment for testing your integration without touching production
data.
Each environment has its own embed host (the iframe / webview URL) and its own
API host. The API hosts are already selectable from the server dropdown in the
[Client API reference](/api-reference) — sandbox embed works the same way, just
with a different host.
## Hosts
| Environment | Embed (iframe / webview) | API |
| ----------- | --------------------------------- | ------------------------------- |
| Production | `https://embed.v2.doshi.app` | `https://api.doshi.app` |
| Sandbox | `https://sandbox.embed.doshi.app` | `https://sandbox.api.doshi.app` |
## Switching to sandbox
Your **client token is the same in both environments** — there's no separate
sandbox key. To test against sandbox, just point the iframe at
`https://sandbox.embed.doshi.app`. Nothing else in your integration changes.
```js theme={null}
// Production
const embedUrl = "https://embed.v2.doshi.app";
// Sandbox
const embedUrl = "https://sandbox.embed.doshi.app";
```
The code examples throughout these docs use the production host. Swap in the
sandbox host wherever you see `embed.v2.doshi.app` to run against sandbox.
## One thing to watch: origin checks
If you point the iframe at sandbox, the origin you verify in your `message`
listener — and the `targetOrigin` you pass to `postMessage` — must match the
**sandbox** host. A listener hardcoded to `https://embed.v2.doshi.app` will
silently reject messages coming from `https://sandbox.embed.doshi.app`.
The simplest approach is to allow both hosts:
```javascript theme={null}
const allowedOrigins = [
"https://embed.v2.doshi.app", // Production
"https://sandbox.embed.doshi.app", // Sandbox
];
window.addEventListener("message", (event) => {
if (!allowedOrigins.includes(event.origin)) {
console.warn("Rejected message from unauthorized origin:", event.origin);
return;
}
// Process message
});
```
See [Security](/webview/security) for the full origin-verification guidance.
## Next Steps
Complete walkthrough with examples
Origin verification and token handling
# Webview Authentication Overview
Source: https://docs.doshi.app/webview/introduction
Learn how to implement secure webview-based authentication in your web application
## Introduction
Doshi provides a secure webview authentication system that uses a two-step process:
1. **Generate a custom token** from the Doshi API using your API key
2. **Pass the token** to the embedded Doshi iframe
This guide explains the authentication methods available after you've obtained your custom token.
New to Doshi authentication? Start with the [Quick Start Guide](/webview/quickstart) for a complete walkthrough.
## Authentication Methods
After obtaining your custom token from the API, you can pass it to the iframe using two methods:
**Recommended for production**
Real-time, secure cross-origin communication. Best for sensitive data and dynamic flows.
**Simple implementation**
URL-based authentication. Best for quick setup and debugging.
## How It Works
Your backend calls `/client/auth/token` with your API key to generate a custom token
Your frontend passes the token to the Doshi iframe using postMessage or query parameters
Doshi iframe handles user authentication, including 2FA if enabled
User session is managed automatically (1hr ID token + 12hr refresh token)
## Data Structure
### Required Parameter
```typescript theme={null}
interface AuthData {
token: string; // Custom token from API (required)
}
```
### Optional Parameters
```typescript theme={null}
interface AuthData {
token: string; // Required
email?: string; // User's email
segment?: string; // For multiple paths in same org
branchId?: string; // Branch/location identifier
// 2FA Parameters (when is2FaEnabled is true)
is2FaEnabled?: boolean; // Enable 2FA flow
dob?: string; // Date of birth (YYYY-MM-DD)
organizationId?: string; // Your organization ID
partnerUserId?: string; // Your internal user ID
firstName?: string; // User's first name
lastName?: string; // User's last name
}
```
## Basic Setup
### 1. Get Your API Key
Contact [hello@doshi.app](mailto:hello@doshi.app) to receive your static API key.
### 2. Generate Custom Token
```bash theme={null}
curl -X POST https://api.doshi.app/client/auth/token \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Access-Type: client" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com"
}'
```
See the [full API reference](/api-reference/auth/get-custom-authentication-token).
### 3. Embed the Webview
Examples use the production host. To test, swap it for
`https://sandbox.embed.doshi.app` — the same client token works in both. See
[Environments](/webview/environments).
```html theme={null}
```
### 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 (
);
};
export default WebviewComponent;
```
### Usage Example
```tsx theme={null}
import WebviewComponent from './WebviewComponent';
function App() {
return (
My Application
);
}
// With 2FA enabled
function AppWith2FA() {
return (
My Application
);
}
```
## Vanilla JavaScript Implementation
### Parent Window
```javascript theme={null}
// Configuration
const config = {
doshiEmbedUrl: 'https://embed.v2.doshi.app',
backendUrl: '/api/generate-doshi-token'
};
// User data
const userData = {
email: "user@example.com",
segment: "premium",
branchId: "branch_789"
};
// 2FA data (optional)
const twoFactorData = {
is2FaEnabled: true,
dob: "1990-01-15",
organizationId: "org_123",
partnerUserId: "partner_123",
firstName: "John",
lastName: "Doe"
};
let authToken = null;
// Step 1: Get nonce token from your backend
async function fetchAuthToken() {
try {
const response = await fetch(config.backendUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: userData.email,
branchId: userData.branchId
})
});
if (!response.ok) {
throw new Error('Failed to generate token');
}
const data = await response.json();
authToken = data.token;
// Create iframe after token is ready
createWebview();
} catch (error) {
console.error('Failed to fetch auth token:', error);
showError('Authentication failed. Please try again.');
}
}
// Step 2: Create and add webview to the page
function createWebview() {
const webview = document.createElement("iframe");
webview.src = config.doshiEmbedUrl;
webview.className = "h-full w-full";
webview.frameBorder = "0";
webview.allowFullscreen = true;
document.querySelector("#doshi-container").appendChild(webview);
// Setup message listener
setupMessageListener();
}
// Step 3: Handle messages from webview
function setupMessageListener() {
window.addEventListener("message", handleMessage);
// Cleanup on page unload
window.addEventListener("beforeunload", () => {
window.removeEventListener("message", handleMessage);
});
}
function handleMessage(event) {
// ✅ Verify origin in production
if (event.origin !== config.doshiEmbedUrl) {
console.warn('Rejected message from:', event.origin);
return;
}
try {
const data = typeof event.data === "string"
? JSON.parse(event.data)
: event.data;
// When we receive PING from webview, send the auth data
if (data.type === "PING") {
sendAuthData();
} else if (data.type === "AUTH_SUCCESS") {
console.log('Authentication successful');
onAuthSuccess(data);
} else if (data.type === "AUTH_ERROR") {
console.error('Authentication failed:', data.error);
onAuthError(data.error);
}
} catch (error) {
console.error("Error processing message:", error);
}
}
// Step 4: Send auth data to webview
function sendAuthData() {
if (!authToken) {
console.error('No auth token available');
return;
}
// Build auth data
const authData = {
token: authToken,
email: userData.email,
segment: userData.segment,
branchId: userData.branchId,
type: "AUTH"
};
// Add 2FA data if enabled
if (twoFactorData.is2FaEnabled) {
Object.assign(authData, twoFactorData);
}
// Send to webview
const webview = document.querySelector("iframe");
if (webview && webview.contentWindow) {
webview.contentWindow.postMessage(
JSON.stringify(authData),
config.doshiEmbedUrl // ⚠️ Never use "*" in production
);
}
}
// Callback functions
function onAuthSuccess(data) {
console.log('User authenticated:', data);
// Hide loading indicator, update UI, etc.
}
function onAuthError(error) {
console.error('Authentication error:', error);
showError('Authentication failed. Please try again.');
}
function showError(message) {
const container = document.querySelector("#doshi-container");
container.innerHTML = `
${message}
`;
}
// Initialize
fetchAuthToken();
```
### HTML Structure
```html theme={null}
Doshi Integration
```
## Backend Implementation
Your backend should handle the API key securely:
```typescript theme={null}
// backend/routes/auth.ts
import express from 'express';
const router = express.Router();
router.post('/generate-doshi-token', async (req, res) => {
try {
// Verify user is authenticated in your system
if (!req.session?.userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { email, branchId } = req.body;
// Call Doshi API with your API key
const response = await fetch(
'https://api.doshi.app/client/auth/token',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.DOSHI_API_KEY}`,
'Access-Type': 'client'
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
partnerUserId: req.session.userId,
branchId
})
}
);
if (!response.ok) {
throw new Error('Doshi API request failed');
}
const data = await response.json();
// Log for security monitoring
console.log(`Generated token for user ${req.session.userId}`);
res.json({ token: data.token });
} catch (error) {
console.error('Error generating Doshi token:', error);
res.status(500).json({ error: 'Failed to generate authentication token' });
}
});
export default router;
```
## Child Window (Webview) Implementation
This section is for reference only. The Doshi iframe already implements this functionality.
### Sending PING
```javascript theme={null}
// Inside the webview, send PING to parent when ready
window.parent.postMessage(
JSON.stringify({ type: "PING" }),
"*" // ⚠️ In production: specify parent origin if known
);
```
### Receiving AUTH Data
```javascript theme={null}
// Listen for AUTH response from parent
window.addEventListener("message", (event) => {
// ✅ Verify origin in production
// if (event.origin !== 'https://parent-domain.com') return;
try {
const data = typeof event.data === "string"
? JSON.parse(event.data)
: event.data;
if (data.type === "AUTH") {
// Process authentication data
const {
token,
email,
segment,
branchId,
is2FaEnabled,
dob,
organizationId,
partnerUserId,
firstName,
lastName
} = data;
// Authenticate user with token
await authenticateUser(token);
// If 2FA is enabled, show OTP screen
if (is2FaEnabled) {
await handle2FAFlow({
dob,
organizationId,
partnerUserId,
firstName,
lastName
});
}
// Notify parent of success
window.parent.postMessage(
JSON.stringify({
type: "AUTH_SUCCESS",
userId: user.id
}),
event.origin
);
}
} catch (error) {
console.error("Error processing auth message:", error);
// Notify parent of error
window.parent.postMessage(
JSON.stringify({
type: "AUTH_ERROR",
error: error.message
}),
event.origin
);
}
});
```
## Advanced: Retry Logic
Add retry logic for more robust authentication:
```typescript theme={null}
const WebviewWithRetry: React.FC = ({ userEmail }) => {
const [retryCount, setRetryCount] = useState(0);
const MAX_RETRIES = 3;
const RETRY_DELAY = 2000; // 2 seconds
const handleSendAuth = async () => {
try {
const authData = {
token: authToken,
email: userEmail,
type: "AUTH"
};
const webview = document.querySelector("iframe");
if (webview && webview.contentWindow) {
webview.contentWindow.postMessage(
JSON.stringify(authData),
"https://embed.v2.doshi.app"
);
} else {
throw new Error("Webview not found");
}
} catch (error) {
console.error("Error sending auth:", error);
if (retryCount < MAX_RETRIES) {
console.log(`Retrying... (${retryCount + 1}/${MAX_RETRIES})`);
setTimeout(() => {
setRetryCount(retryCount + 1);
handleSendAuth();
}, RETRY_DELAY);
} else {
setError('Failed to authenticate after multiple attempts');
}
}
};
// ... rest of component
};
```
## Message Types
The message type identifier
**From Iframe to Parent:**
* `PING`: Sent by webview to signal readiness
* `AUTH_SUCCESS`: Authentication completed successfully
* `AUTH_ERROR`: Authentication failed
**From Parent to Iframe:**
* `AUTH`: Sent by parent with authentication data
## Testing postMessage Flow
```javascript theme={null}
// Test the authentication flow
function testPostMessageFlow() {
console.log('Testing postMessage flow...');
// Simulate PING from iframe
window.postMessage(
JSON.stringify({ type: "PING" }),
window.location.origin
);
// Listen for AUTH response
window.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
console.log("Received message:", data);
if (data.type === "AUTH") {
console.log("✅ AUTH message received");
console.log("Token:", data.token);
console.log("Email:", data.email);
}
});
}
```
## Common Issues
**Solutions:**
1. Check that origin validation matches exactly
2. Verify iframe has loaded before sending messages
3. Ensure JSON.stringify is used when sending data
4. Check browser console for errors
**Solutions:**
1. Ensure backend API call completed successfully
2. Check network tab for API response
3. Verify error handling in token fetch logic
4. Check that API key is valid
**Solutions:**
1. Verify `is2FaEnabled` is set to `true`
2. Ensure all required 2FA fields are provided
3. Check that organization has 2FA enabled
4. Verify user phone number is registered
## Next Steps
Review security best practices
Optimize your implementation
# Query Parameter Authentication
Source: https://docs.doshi.app/webview/query-parameters
Implement simple URL-based authentication using query parameters
## Overview
Query parameter authentication is a simpler approach that passes authentication data through the URL. This method is ideal for quick implementation and debugging.
Query parameters are visible in URLs and browser history. While the custom token is short-lived, consider using postMessage for production environments.
## How It Works
Your backend calls the Doshi API to get a custom token
Construct the webview URL with the token and other data as query parameters
Load the iframe with the constructed URL
Webview extracts parameters and authenticates the user, handling 2FA if needed
## React Implementation
### Complete Example with API Call
```tsx theme={null}
import React, { useEffect, useState, useMemo } 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 Props {
userEmail: string;
segment?: string;
branchId?: string;
// 2FA props
is2FaEnabled?: boolean;
dob?: string;
organizationId?: string;
partnerUserId?: string;
firstName?: string;
lastName?: string;
}
const WebviewWithQueryParams: 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: Build URL with token and params
const webviewUrl = useMemo(() => {
if (!authToken) return null;
const baseUrl = "https://embed.v2.doshi.app";
const params = new URLSearchParams();
// Required
params.append('token', authToken);
// Optional parameters
if (userEmail) params.append('email', userEmail);
if (segment) params.append('segment', segment);
if (branchId) params.append('branchId', branchId);
// 2FA parameters
if (is2FaEnabled) {
params.append('is2FaEnabled', 'true');
if (dob) params.append('dob', dob);
if (organizationId) params.append('organizationId', organizationId);
if (partnerUserId) params.append('partnerUserId', partnerUserId);
if (firstName) params.append('firstName', firstName);
if (lastName) params.append('lastName', lastName);
}
return `${baseUrl}?${params.toString()}`;
}, [
authToken,
userEmail,
segment,
branchId,
is2FaEnabled,
dob,
organizationId,
partnerUserId,
firstName,
lastName
]);
if (isLoading) {
return (
Loading...
);
}
if (error) {
return (
{error}
);
}
if (!webviewUrl) {
return
Failed to authenticate
;
}
return (
);
};
export default WebviewWithQueryParams;
```
### Usage Example
```tsx theme={null}
import WebviewWithQueryParams from './WebviewWithQueryParams';
function App() {
return (
My Application
);
}
// With 2FA enabled
function AppWith2FA() {
return (
My Application
);
}
```
## Vanilla JavaScript Implementation
### Parent Window
```javascript theme={null}
// Configuration
const config = {
doshiEmbedUrl: 'https://embed.v2.doshi.app',
backendUrl: '/api/generate-doshi-token'
};
// User data
const userData = {
email: "user@example.com",
segment: "premium",
branchId: "branch_789"
};
// 2FA data (optional)
const twoFactorData = {
is2FaEnabled: true,
dob: "1990-01-15",
organizationId: "org_123",
partnerUserId: "partner_123",
firstName: "John",
lastName: "Doe"
};
// Step 1: Get nonce token from your backend
async function fetchAuthToken() {
try {
const response = await fetch(config.backendUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: userData.email,
branchId: userData.branchId
})
});
if (!response.ok) {
throw new Error('Failed to generate token');
}
const data = await response.json();
return data.token;
} catch (error) {
console.error('Failed to fetch auth token:', error);
showError('Authentication failed. Please try again.');
return null;
}
}
// Step 2: Build URL with query parameters
function buildWebviewUrl(token, userData, twoFactorData) {
const params = new URLSearchParams();
// Required
params.append('token', token);
// Optional parameters
if (userData.email) params.append('email', userData.email);
if (userData.segment) params.append('segment', userData.segment);
if (userData.branchId) params.append('branchId', userData.branchId);
// 2FA parameters
if (twoFactorData.is2FaEnabled) {
params.append('is2FaEnabled', 'true');
if (twoFactorData.dob) params.append('dob', twoFactorData.dob);
if (twoFactorData.organizationId) {
params.append('organizationId', twoFactorData.organizationId);
}
if (twoFactorData.partnerUserId) {
params.append('partnerUserId', twoFactorData.partnerUserId);
}
if (twoFactorData.firstName) {
params.append('firstName', twoFactorData.firstName);
}
if (twoFactorData.lastName) {
params.append('lastName', twoFactorData.lastName);
}
}
return `${config.doshiEmbedUrl}?${params.toString()}`;
}
// Step 3: Create and add webview to the page
async function createDoshiEmbed() {
// Show loading state
showLoading();
// Get token
const token = await fetchAuthToken();
if (!token) return;
// Build URL
const webviewUrl = buildWebviewUrl(token, userData, twoFactorData);
// Create iframe
const webview = document.createElement("iframe");
webview.src = webviewUrl;
webview.className = "h-full w-full";
webview.frameBorder = "0";
webview.allowFullscreen = true;
// Add load event listener
webview.addEventListener('load', () => {
console.log('Doshi embed loaded successfully');
hideLoading();
});
webview.addEventListener('error', () => {
console.error('Failed to load Doshi embed');
showError('Failed to load authentication. Please try again.');
});
// Add to page
const container = document.querySelector("#doshi-container");
container.innerHTML = ''; // Clear any existing content
container.appendChild(webview);
}
// UI Helper functions
function showLoading() {
const container = document.querySelector("#doshi-container");
container.innerHTML = `
Loading...
`;
}
function hideLoading() {
// Loading is automatically hidden when iframe loads
}
function showError(message) {
const container = document.querySelector("#doshi-container");
container.innerHTML = `
${message}
`;
}
// Initialize
createDoshiEmbed();
```
### HTML Structure
```html theme={null}
Doshi Integration
```
## Child Window (Webview) - Reading Query Parameters
This section is for reference only. The Doshi iframe already implements this functionality.
### Reading Parameters
```javascript theme={null}
// Inside the webview, extract query parameters
function getAuthDataFromUrl() {
const params = new URLSearchParams(window.location.search);
return {
token: params.get('token') || '',
email: params.get('email') || '',
segment: params.get('segment') || '',
branchId: params.get('branchId') || '',
// 2FA parameters
is2FaEnabled: params.get('is2FaEnabled') === 'true',
dob: params.get('dob') || '',
organizationId: params.get('organizationId') || '',
partnerUserId: params.get('partnerUserId') || '',
firstName: params.get('firstName') || '',
lastName: params.get('lastName') || ''
};
}
// Use the auth data
const authData = getAuthDataFromUrl();
console.log('Authenticated user:', authData.email);
// Validate required data
if (!authData.token) {
console.error('Missing required authentication token');
showError('Authentication token missing');
}
// If 2FA is enabled, show OTP screen
if (authData.is2FaEnabled) {
handle2FAFlow(authData);
}
```
### Clearing Sensitive Parameters
Clear sensitive parameters from the URL after reading to prevent exposure in browser history
```javascript theme={null}
// Clear sensitive params from URL after reading
function clearUrlParams() {
if (window.history.replaceState) {
const url = new URL(window.location.href);
url.search = ''; // Clear all query params
window.history.replaceState({}, document.title, url.toString());
}
}
// Extract auth data
const authData = getAuthDataFromUrl();
// Store in memory
window.authData = authData;
// Clear URL (optional, for security)
clearUrlParams();
```
## URL Encoding
Properly encode special characters in parameters:
```javascript theme={null}
function buildSecureUrl(baseUrl, data) {
const params = new URLSearchParams();
Object.entries(data).forEach(([key, value]) => {
if (value !== null && value !== undefined && value !== '') {
// URLSearchParams automatically encodes values
params.append(key, String(value));
}
});
const url = `${baseUrl}?${params.toString()}`;
// Check URL length
if (url.length > 2000) {
console.warn('URL exceeds recommended length. Consider using postMessage.');
}
return url;
}
```
## URL Parameter Reference
### Required Parameters
The nonce token received from the Doshi API
### Optional Parameters
User's email address
Used for handling multiple learning paths under the same organization
Branch or location identifier
`pathId` is not a URL parameter. Adding it to the iframe URL 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).
### 2FA Parameters
Set to `true` to enable 2FA flow in the iframe
User's date of birth in YYYY-MM-DD format (required for 2FA)
Your organization ID (required for 2FA)
Your internal user ID (required for 2FA)
User's first name (required for 2FA)
User's last name (required for 2FA)
## Comparison: postMessage vs Query Parameters
| Feature | Query Parameters | postMessage |
| --------------- | ------------------------ | ------------------------------- |
| **Security** | Visible in URLs | Hidden from URLs |
| **Complexity** | Simple | Moderate |
| **Real-time** | No | Yes |
| **URL Length** | Limited (\~2000 chars) | No limit |
| **Debugging** | Easy (visible in URL) | Requires dev tools |
| **2FA Support** | Yes | Yes |
| **Best For** | Simple auth, quick setup | Sensitive data, production apps |
## Common Issues
**Problem:** URL exceeds browser limits (typically 2000 characters).
**Solutions:**
1. Use shorter tokens or references
2. Switch to postMessage method
3. Only include essential parameters
**Problem:** Special characters in parameters causing issues.
**Solution:**
```javascript theme={null}
// Use URLSearchParams for automatic encoding
const params = new URLSearchParams();
params.append('email', 'user+test@example.com'); // Properly encoded
params.append('name', 'John & Jane'); // Properly encoded
```
**Problem:** Query parameters not accessible in the webview.
**Solution:**
```javascript theme={null}
// Debug parameter reading
console.log('Current URL:', window.location.href);
console.log('Search params:', window.location.search);
const params = new URLSearchParams(window.location.search);
console.log('All params:', Object.fromEntries(params));
// Check for specific param
const token = params.get('token');
if (!token) {
console.error('Token parameter missing from URL');
}
```
## Next Steps
Review security considerations
Optimize your implementation
# Quick Start
Source: https://docs.doshi.app/webview/quickstart
Get started with Doshi webview authentication in minutes
## Overview
Doshi webview authentication uses a secure two-step process:
1. **Generate a custom token** by calling the Doshi API with your API key
2. **Pass the token to the iframe** using postMessage or query parameters
**IP Whitelisting Required**: Your server's IP address must be whitelisted to access the Doshi API. Contact [hello@doshi.app](mailto:hello@doshi.app) with your server IP addresses to get started.
Contact [hello@doshi.app](mailto:hello@doshi.app) to:
* Receive your API key
* Whitelist your server IP address(es)
Call the authentication endpoint with your API key from your whitelisted server
Pass the custom token to the Doshi Frontend using postMessage or query parameters
Doshi handles the sign-in process, including 2FA if enabled
These examples use the production hosts. To test against sandbox, use
`https://sandbox.embed.doshi.app` (and `https://sandbox.api.doshi.app` for the
API) — your client token works in both. See [Environments](/webview/environments).
## Authentication Flow
```mermaid theme={null}
sequenceDiagram
participant Client as Your Application
participant API as Doshi API
participant Frontend as Doshi Frontend
participant User as End User
Client->>API: POST /client/auth/custom-token (with API Key)
API->>Client: Returns custom token
Client->>Frontend: Pass token + user data
Frontend->>User: Shows login/2FA screen if needed
User->>Frontend: Completes authentication
Frontend->>Frontend: Session active (1hr + 12hr refresh)
```
## Step 1: Generate Custom Token
Call the authentication endpoint with your API key:
```bash theme={null}
curl -X POST https://api.doshi.app/client/auth/custom-token \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Access-Type: client" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"partnerUserId": "partner_123",
"firstName": "John",
"lastName": "Doe",
"branchId": "branch_789"
}'
```
**Required Headers:**
* `Authorization: Bearer YOUR_API_KEY`
* `Access-Type: client`
* `Content-Type: application/json`
### API Parameters
**Required (at least one):**
* `email` - User's email address
* `partnerUserId` - Your internal user ID
**Optional:**
* `firstName` - User's first name
* `lastName` - User's last name
* `branchId` - Branch or location identifier
You must provide either `email` OR `partnerUserId` (or both). All other fields are optional.
**Response:**
```json theme={null}
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
See the [full API reference](/api-reference/auth/get-custom-authentication-token) for details.
### Setting the Starting Learning Path
By default, a new user picks their own learning path the first time they open Doshi. To skip that screen and drop them straight into a specific set of lessons, pass a `pathId` when you mint the token.
`pathId` is only accepted by `/client/auth/token` — **not** by
`/client/auth/custom-token`, which ignores it. Use `/client/auth/token` when
you need a starting path.
In the dashboard, go to **Paths** → **Create**, then add the courses and lessons you want the user to see.
On the path's card, open the **⋯** menu and choose **Copy ID**. The ID is also the last segment of the path's URL (`/paths/`).
Include the ID as `pathId` in the token request.
```bash theme={null}
curl -X POST https://api.doshi.app/client/auth/token \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Access-Type: client" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"pathId": "YOUR_PATH_ID"
}'
```
The user now lands directly in the lessons on that path, with no path-selection screen.
#### Behaviour and limits
* **Sets the initial path only.** If the user already has a path, `pathId` is ignored — existing users are never moved. It is not a way to switch someone's path later.
* **Don't combine it with `segment`.** The two write to different places, so sending both leaves the path-selection screen in place. Use one or the other.
* **The ID isn't validated.** An incorrect ID is accepted without an error and leaves the user without usable content, so confirm the flow on [sandbox](/webview/environments) before going live.
* Must be 2–50 characters.
* Works with the 2FA flow.
`pathId` decides **which lessons** the user starts on. `segment` is different —
it narrows which paths a user is offered when you run several paths under one
organization, and still lets them choose.
## Step 2: Pass Token to Doshi Frontend
Pass the nonce token to the Doshi Frontend using one of two methods:
Secure, real-time communication for sensitive data
Simple URL-based authentication
### Frontend Parameters
**Required:**
* `token` - The nonce token from the API
**Optional:**
* `email` - User's email address
* `segment` - For handling multiple learning paths
* `branchId` - Branch or location identifier
### 2FA Parameters (Optional)
When 2FA is enabled, pass these additional parameters:
**`All 2FA fields are required when is2FaEnabled is true:`**
* `is2FaEnabled` - Set to `true`
* `dob` - Date of birth (YYYY-MM-DD)
* `organizationId` - Your organization ID
* `partnerUserId` - Your internal user ID
* `firstName` - User's first name
* `lastName` - User's last name
The nonce token received from the API
User's email address (optional)
Used for handling multiple learning paths under the same organization (optional)
Branch or location identifier (optional)
Set to `true` to enable 2FA flow (optional, but if true, all 2FA fields below are required)
User's date of birth - YYYY-MM-DD format (required when is2FaEnabled is true)
Your organization ID (required when is2FaEnabled is true)
Your internal user ID (required when is2FaEnabled is true)
User's first name (required when is2FaEnabled is true)
User's last name (required when is2FaEnabled is true)
When 2FA is enabled, the Doshi Frontend automatically handles the OTP send and verification flow. You don't need to call the OTP endpoints separately.
## Step 3: Session Management
Once authenticated, the Doshi Frontend automatically manages the session:
* **ID Token TTL**: 1 hour
* **Refresh Token TTL**: 12 hours
* **Auto-refresh**: Tokens are refreshed automatically
No additional action required from your application!
## Quick Example
### React with postMessage
```tsx theme={null}
import React, { useEffect, useState } from "react";
const DoshiEmbed = () => {
const [token, setToken] = useState(null);
useEffect(() => {
// Step 1: Get nonce token from API
async function authenticate() {
const response = await fetch('https://api.doshi.app/client/auth/custom-token', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Access-Type': 'client',
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: 'user@example.com', // Required (or partnerUserId)
partnerUserId: 'partner_123', // Optional (but one of email/partnerUserId required)
branchId: 'branch_789' // Optional
})
});
const data = await response.json();
setToken(data.token);
}
authenticate();
}, []);
useEffect(() => {
if (!token) return;
// Step 2: Listen for PING from Doshi Frontend
const handleMessage = (event: MessageEvent) => {
if (event.origin !== 'https://embed.v2.doshi.app') return;
const data = typeof event.data === "string"
? JSON.parse(event.data)
: event.data;
if (data.type === "PING") {
// Step 3: Send token to Doshi Frontend
const iframe = document.querySelector("iframe");
if (iframe?.contentWindow) {
iframe.contentWindow.postMessage(
JSON.stringify({
token, // Required
email: 'user@example.com', // Optional
segment: 'premium', // Optional
branchId: 'branch_789', // Optional
type: "AUTH"
}),
"https://embed.v2.doshi.app"
);
}
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, [token]);
if (!token) {
return
;
}
return (
);
};
export default DoshiEmbed;
```
## With 2FA Enabled
```tsx theme={null}
const authData = {
token: data.token, // Required
email: 'user@example.com', // Optional
segment: 'premium', // Optional
branchId: 'branch_789', // Optional
// All fields below are required when is2FaEnabled is true
is2FaEnabled: true,
dob: '1990-01-15',
organizationId: 'org_123',
partnerUserId: 'partner_123',
firstName: 'John',
lastName: 'Doe'
};
// The Doshi Frontend will automatically show OTP screen and handle verification
```
## Next Steps
Detailed postMessage implementation
URL-based authentication guide
Security best practices
## Need Help?
Get your API key or ask questions: [hello@doshi.app](mailto:hello@doshi.app)
# Security Considerations
Source: https://docs.doshi.app/webview/security
Essential security guidelines for webview authentication
## Overview
Security is paramount when implementing webview authentication. This guide covers critical security considerations for both postMessage and query parameter methods.
## IP Whitelisting
The Doshi API requires IP whitelisting. Contact [hello@doshi.app](mailto:hello@doshi.app) to whitelist your server IP addresses.
To get your server's IP address:
```bash theme={null}
# Run this command on your server
curl ifconfig.me
```
Send this IP address to [hello@doshi.app](mailto:hello@doshi.app) along with your organization name.
## API Key Security
**Never expose your API key in client-side code**. Always call the Doshi API from your backend server.
### Secure API Key Usage
```typescript theme={null}
// ❌ NEVER do this - API key exposed in frontend
const response = await fetch('https://api.doshi.app/client/auth/token', {
headers: {
'Authorization': 'Bearer YOUR_API_KEY', // Exposed to users!
'Access-type': 'client'
}
});
// ✅ Correct - Call your backend which securely stores the API key
const response = await fetch('https://your-backend.com/api/generate-doshi-token', {
method: 'POST',
body: JSON.stringify({ userId: 'user_123' })
});
```
### Backend Implementation Example
```typescript theme={null}
// Your backend server
app.post('/api/generate-doshi-token', async (req, res) => {
// Verify the user is authenticated in your system
if (!req.session.userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Call Doshi API with your API key (stored securely in environment variables)
const response = await fetch('https://api.doshi.app/client/auth/token', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.DOSHI_API_KEY}`,
'Acess-Type': 'client',
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: req.user.email,
partnerUserId: req.user.id
})
});
const data = await response.json();
res.json({ token: data.token });
});
```
## Origin Verification
Always verify the origin of messages in production environments
If you load the iframe from sandbox, the origin you verify here — and the
`targetOrigin` you pass to `postMessage` — must be
`https://sandbox.embed.doshi.app`. A check hardcoded to the production host
will silently reject sandbox messages. Allow both hosts to support either
environment. See [Environments](/webview/environments).
### For postMessage
```javascript theme={null}
window.addEventListener("message", (event) => {
// ✅ Verify origin in production
if (event.origin !== 'https://embed.v2.doshi.app') {
console.warn('Rejected message from unauthorized origin:', event.origin);
return;
}
// Process message
});
```
### Specify Target Origins
```javascript theme={null}
// ❌ Never use in production
webview.contentWindow.postMessage(data, "*");
// ✅ Always specify exact origin in production
webview.contentWindow.postMessage(data, "https://embed.v2.doshi.app");
```
## Token Security
### Token Lifecycle
The custom token received from the API:
* Is single-use for initial authentication
* Has a short expiration time
* Cannot be reused after the user session is established
```typescript theme={null}
// After getting the token, use it immediately
const { token } = await getTokenFromBackend();
// Pass to iframe right away
passTokenToIframe(token);
// Don't store the token for long periods
// Don't reuse the token for multiple sessions
```
### Session Tokens
Once authenticated:
* **ID Token TTL**: 1 hour
* **Refresh Token TTL**: 12 hours
* The iframe manages token refresh automatically
* Sessions are isolated per iframe instance
## Cross-Origin Communication
### postMessage API Security
Always validate the structure and content of messages:
```javascript theme={null}
try {
const data = typeof event.data === "string"
? JSON.parse(event.data)
: event.data;
// Validate message structure
if (!data.type || typeof data.type !== 'string') {
throw new Error('Invalid message structure');
}
// Validate expected message types
const validTypes = ['PING', 'AUTH', 'AUTH_SUCCESS', 'AUTH_ERROR'];
if (!validTypes.includes(data.type)) {
throw new Error(`Unexpected message type: ${data.type}`);
}
// Process valid message
} catch (error) {
console.error("Error processing message:", error);
return;
}
```
Implement comprehensive error handling:
```javascript theme={null}
const handleMessage = (event) => {
try {
// Verify origin
if (event.origin !== 'https://embed.v2.doshi.app') return;
// Parse and validate
const data = JSON.parse(event.data);
// Process message
} catch (error) {
console.error('Message handling failed:', error);
// Log to monitoring service
logError(error);
}
};
```
Always use HTTPS for secure communication:
```javascript theme={null}
// ✅ Secure HTTPS URL
const webviewUrl = "https://embed.v2.doshi.app";
// ❌ Never use HTTP in production
const insecureUrl = "http://embed.v2.doshi.app";
```
## Query Parameter Security
Query parameters are visible in URLs and browser history. The custom token is short-lived but still sensitive.
### Best Practices
1. **Clear Parameters After Use**
```javascript theme={null}
// After iframe loads, clear sensitive params
window.addEventListener('load', () => {
if (window.history.replaceState) {
const url = new URL(window.location.href);
url.search = ''; // Clear all query params
window.history.replaceState({}, document.title, url.toString());
}
});
```
2. **Use Short-Lived Tokens**
The custom token from the API is already short-lived, but you can add additional validation:
```typescript theme={null}
// Backend - add timestamp to token generation
const tokenData = {
email: user.email,
timestamp: Date.now()
};
// Frontend - check token age before using
const isTokenFresh = (timestamp: number) => {
const maxAge = 5 * 60 * 1000; // 5 minutes
return Date.now() - timestamp < maxAge;
};
```
3. **Monitor Token Usage**
```typescript theme={null}
// Log token usage for security monitoring
app.post('/api/generate-doshi-token', async (req, res) => {
const token = await generateDoshiToken(req.user);
// Log token generation
await logSecurityEvent({
event: 'token_generated',
userId: req.user.id,
timestamp: new Date(),
ip: req.ip
});
res.json({ token });
});
```
4. **Respect URL Length Limits**
URLs should generally stay under 2000 characters for maximum browser compatibility
```javascript theme={null}
function buildWebviewUrl(baseUrl, params) {
const urlParams = new URLSearchParams(params);
const fullUrl = `${baseUrl}?${urlParams.toString()}`;
// Check URL length
if (fullUrl.length > 2000) {
console.warn('URL exceeds recommended length. Consider using postMessage.');
}
return fullUrl;
}
```
## 2FA Security
When implementing 2FA:
```typescript theme={null}
// Only pass 2FA parameters when explicitly enabled
const authData = {
token: customToken,
email: user.email,
// Only include these if 2FA is actually enabled
...(is2FaEnabled && {
is2FaEnabled: true,
dob: user.dob,
organizationId: org.id,
partnerUserId: user.id,
firstName: user.firstName,
lastName: user.lastName
})
};
```
### Don't Send Unnecessary Data
```typescript theme={null}
// ❌ Don't send sensitive data if not needed
const authData = {
token: customToken,
ssn: user.ssn, // Never send unless absolutely required
password: user.password // Never send passwords
};
// ✅ Only send required fields
const authData = {
token: customToken,
email: user.email,
segment: 'premium'
};
```
## Security Checklist
Store API key securely in backend environment variables
Never expose API key in client-side code
Call Doshi API only from your backend server
Use HTTPS for all communication
Verify message origins in production
Never use "\*" wildcard for target origins in production
Validate all incoming messages and parameters
Use short-lived tokens
Clear sensitive data from URLs after reading
Implement comprehensive error handling
Log security events for monitoring
Test across different browsers and environments
## Common Security Pitfalls
| Pitfall | Risk | Solution |
| ---------------------------------- | --------------------------------- | ------------------------------- |
| API key in frontend | Anyone can steal your key | Call API from backend only |
| Using `"*"` for postMessage target | Any site can receive messages | Specify exact origin |
| Sensitive tokens in URLs | Exposed in logs/history | Use postMessage or clear params |
| No origin verification | Malicious sites can send messages | Always check `event.origin` |
| Missing input validation | XSS vulnerabilities | Validate all inputs |
| Long-lived tokens | Increased attack window | Use short expiration times |
| Storing tokens in localStorage | XSS attacks can steal tokens | Let iframe manage sessions |
## Environment-Specific Configuration
```typescript theme={null}
// config.ts
const config = {
development: {
doshiApiUrl: 'https://sandbox.api.doshi.app',
doshiEmbedUrl: 'https://staging-embed.doshi.app',
strictOriginCheck: false // For local testing
},
production: {
doshiApiUrl: 'https://api.doshi.app',
doshiEmbedUrl: 'https://embed.v2.doshi.app',
strictOriginCheck: true
}
};
// Use environment-specific config
const env = process.env.NODE_ENV || 'development';
export default config[env];
```
## Monitoring and Logging
```typescript theme={null}
// Log security-relevant events
const logSecurityEvent = async (event: SecurityEvent) => {
await logger.log({
level: 'security',
event: event.type,
userId: event.userId,
ip: event.ip,
timestamp: new Date(),
metadata: event.metadata
});
};
// Monitor for suspicious activity
app.post('/api/generate-doshi-token', async (req, res) => {
// Rate limiting
const recentTokens = await getRecentTokensForUser(req.user.id);
if (recentTokens.length > 10) {
await logSecurityEvent({
type: 'excessive_token_generation',
userId: req.user.id,
ip: req.ip
});
return res.status(429).json({ error: 'Too many requests' });
}
// Generate token...
});
```
## Next Steps
Implement secure real-time authentication
Use URL-based authentication safely
# Troubleshooting
Source: https://docs.doshi.app/webview/troubleshooting
Common issues and solutions for webview authentication
## Common Issues
### Authentication Token Issues
**Problem:** Backend returns an error when calling Doshi API.
**Possible Causes:**
* Invalid API key
* API key not provided in Authorization header
* Invalid request parameters
* Network connectivity issues
**Solutions:**
1. **Verify API key:**
```bash theme={null}
# Test your API key
curl -X POST https://api.doshi.app/client/auth/token \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Access-Type: client" \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com"}'
```
2. **Check environment variables:**
```typescript theme={null}
// Verify API key is loaded
console.log('API Key exists:', !!process.env.DOSHI_API_KEY);
console.log('API Key length:', process.env.DOSHI_API_KEY?.length);
// Don't log the actual key!
```
3. **Validate request parameters:**
```typescript theme={null}
// Ensure you're sending email OR partnerUserId
const requestBody = {
email: user.email || undefined,
partnerUserId: user.id || undefined
};
if (!requestBody.email && !requestBody.partnerUserId) {
throw new Error('Must provide email or partnerUserId');
}
```
4. **Check API response:**
```typescript theme={null}
const response = await fetch(doshiApiUrl, options);
if (!response.ok) {
const errorText = await response.text();
console.error('API Error:', response.status, errorText);
throw new Error(`API returned ${response.status}: ${errorText}`);
}
```
**Problem:** Token is rejected by the iframe.
**Possible Causes:**
* Token was generated too long ago
* Token was already used
* Token format is incorrect
**Solutions:**
1. **Generate token immediately before use:**
```typescript theme={null}
// ✅ Generate token right before passing to iframe
const token = await fetchToken();
passToIframe(token);
// ❌ Don't store tokens for long periods
const token = await fetchToken();
localStorage.setItem('token', token); // Don't do this
```
2. **Don't reuse tokens:**
```typescript theme={null}
// Each iframe load should get a fresh token
useEffect(() => {
const fetchAndSetToken = async () => {
const newToken = await fetchToken();
setToken(newToken);
};
fetchAndSetToken();
}, []); // Fresh token on mount
```
3. **Verify token format:**
```typescript theme={null}
const isValidToken = (token: string) => {
return token &&
typeof token === 'string' &&
token.length > 0 &&
!token.includes(' '); // No spaces
};
if (!isValidToken(token)) {
console.error('Invalid token format');
}
```
**Problem:** API key is visible in client-side code.
**Danger:** Anyone can steal your API key and impersonate your application.
**Solution:**
1. **Move API calls to backend:**
```typescript theme={null}
// ❌ NEVER do this in frontend
const response = await fetch(doshiApiUrl, {
headers: {
'Authorization': `Bearer ${API_KEY}`, // EXPOSED!
'Access-Type': 'client'
}
});
// ✅ Call your backend instead
const response = await fetch('/api/generate-doshi-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ userId: user.id })
});
```
2. **Create backend endpoint:**
```typescript theme={null}
// backend/routes/auth.ts
app.post('/api/generate-doshi-token', async (req, res) => {
// Verify user is authenticated
if (!req.session?.userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Call Doshi API with server-side API key
const response = await fetch(doshiApiUrl, {
headers: {
'Authorization': `Bearer ${process.env.DOSHI_API_KEY}`,
'Access-Type': 'client'
}
});
const data = await response.json();
res.json({ token: data.token });
});
```
**Problem:** You sent a `pathId`, but the user still has to pick a path.
Every cause here fails silently — no error is returned. Check in this order:
1. **Wrong endpoint.** `/client/auth/custom-token` ignores `pathId`. Use `/client/auth/token`.
2. **Sent from the frontend.** `pathId` in the `AUTH` payload or the iframe URL does nothing. It belongs in the token request.
3. **`segment` sent alongside it.** The two are incompatible — send one or the other, not both.
4. **User already has a path.** `pathId` only sets the initial path, so returning users are unaffected. Test with a fresh user.
See [Setting the Starting Learning Path](/webview/quickstart#setting-the-starting-learning-path).
### postMessage Issues
**Problem:** The iframe is not receiving messages from the parent window.
**Solutions:**
1. **Verify origin:**
```javascript theme={null}
window.addEventListener("message", (event) => {
console.log('Message received from:', event.origin);
console.log('Expected origin:', 'https://embed.v2.doshi.app');
// Temporarily disable check for debugging
// if (event.origin !== 'https://embed.v2.doshi.app') return;
console.log('Message data:', event.data);
});
```
2. **Check iframe is loaded:**
```javascript theme={null}
const iframe = document.querySelector('iframe');
iframe.addEventListener('load', () => {
console.log('✅ Iframe loaded');
// Now it's safe to send messages
});
```
3. **Verify message format:**
```javascript theme={null}
// Make sure you're sending proper JSON
const message = JSON.stringify({
token: authToken,
email: userEmail,
type: "AUTH"
});
console.log('Sending message:', message);
iframe.contentWindow.postMessage(message, 'https://embed.v2.doshi.app');
```
4. **Check for JavaScript errors:**
```javascript theme={null}
window.addEventListener('error', (event) => {
console.error('JavaScript error:', event.error);
});
```
**Problem:** Messages are rejected due to origin mismatch.
**Solutions:**
1. **Log actual origins:**
```javascript theme={null}
window.addEventListener("message", (event) => {
console.log('Expected:', 'https://embed.v2.doshi.app');
console.log('Received:', event.origin);
console.log('Match:', event.origin === 'https://embed.v2.doshi.app');
});
```
2. **Check for trailing slashes:**
```javascript theme={null}
// Normalize origin comparison
const normalizeOrigin = (origin) => origin.replace(/\/$/, '');
if (normalizeOrigin(event.origin) !== normalizeOrigin(expectedOrigin)) {
return;
}
```
3. **Handle multiple environments:**
```javascript theme={null}
const allowedOrigins = [
'https://embed.v2.doshi.app', // Production
'https://sandbox.embed.doshi.app', // Sandbox
'http://localhost:3000' // Development only
];
if (!allowedOrigins.includes(event.origin)) {
console.warn('Rejected message from:', event.origin);
return;
}
```
**Problem:** Parent window never receives PING from iframe.
**Solutions:**
1. **Check iframe URL:**
```javascript theme={null}
const iframe = document.querySelector('iframe');
console.log('Iframe src:', iframe?.src);
// Should be: https://embed.v2.doshi.app
```
2. **Wait for iframe to load:**
```javascript theme={null}
const iframe = document.querySelector('iframe');
iframe.addEventListener('load', () => {
console.log('Iframe loaded, waiting for PING...');
});
// Set a timeout to detect if PING never arrives
setTimeout(() => {
console.warn('No PING received after 5 seconds');
}, 5000);
```
3. **Check for errors in iframe:**
```javascript theme={null}
iframe.addEventListener('error', (e) => {
console.error('Iframe failed to load:', e);
});
```
### Query Parameter Issues
**Problem:** Query parameters are missing from the iframe URL.
**Solutions:**
1. **Debug URL construction:**
```javascript theme={null}
const params = new URLSearchParams({
token: authToken,
email: userEmail
});
const fullUrl = `https://embed.v2.doshi.app?${params.toString()}`;
console.log('Constructed URL:', fullUrl);
console.log('URL length:', fullUrl.length);
// Verify iframe gets the URL
const iframe = document.querySelector('iframe');
console.log('Iframe src:', iframe?.src);
```
2. **Check for undefined values:**
```javascript theme={null}
const params = new URLSearchParams();
// Only add defined values
if (authToken) params.append('token', authToken);
if (userEmail) params.append('email', userEmail);
console.log('Params:', Object.fromEntries(params));
```
**Problem:** URL exceeds browser limits (typically 2000 characters).
**Solutions:**
1. **Check URL length:**
```javascript theme={null}
const url = buildWebviewUrl(baseUrl, params);
if (url.length > 2000) {
console.error('URL too long:', url.length, 'characters');
// Switch to postMessage method
}
```
2. **Reduce parameter size:**
```javascript theme={null}
// Only include essential parameters
const minimalParams = {
token: authToken,
email: userEmail
// Fetch other data from API after auth
};
```
3. **Switch to postMessage:**
```typescript theme={null}
// If URL is too long, use postMessage instead
if (estimatedUrlLength > 2000) {
return ;
}
return ;
```
**Problem:** Special characters not encoding properly.
**Solutions:**
1. **Use URLSearchParams:**
```javascript theme={null}
// ✅ Automatic encoding
const params = new URLSearchParams();
params.append('email', 'user+test@example.com');
params.append('name', 'John & Jane');
// ❌ Manual concatenation
const url = `${baseUrl}?email=user+test@example.com`; // Wrong!
```
2. **Verify encoding:**
```javascript theme={null}
const params = new URLSearchParams({ email: 'user+test@example.com' });
console.log('Encoded:', params.toString());
// Should be: email=user%2Btest%40example.com
```
### 2FA Issues
**Problem:** 2FA is enabled but OTP screen doesn't appear.
**Solutions:**
1. **Verify 2FA flag:**
```typescript theme={null}
const authData = {
token: authToken,
is2FaEnabled: true, // Must be boolean true, not string
dob: '1990-01-15',
organizationId: 'org_123',
// ... other required fields
};
console.log('2FA enabled:', authData.is2FaEnabled === true);
```
2. **Check all required fields:**
```typescript theme={null}
const required2FAFields = [
'dob',
'organizationId',
'partnerUserId',
'firstName',
'lastName'
];
const missing = required2FAFields.filter(field => !authData[field]);
if (missing.length > 0) {
console.error('Missing 2FA fields:', missing);
}
```
3. **Verify organization has 2FA enabled:**
```javascript theme={null}
// Check with your account manager if 2FA is enabled
// for your organization in Doshi
```
**Problem:** User doesn't receive the OTP code.
**Solutions:**
1. **Verify phone number:**
```typescript theme={null}
// Ensure phone number is registered in Doshi
// Contact support@doshi.app if needed
```
2. **Check OTP channel:**
```typescript theme={null}
// If SMS not working, user can try different channels
// The iframe will handle this automatically
```
3. **Wait and retry:**
```typescript theme={null}
// OTP may take 30-60 seconds to arrive
// User can request resend from the iframe
```
**Problem:** Correct OTP code is rejected.
**Solutions:**
1. **Check OTP expiration:**
```typescript theme={null}
// OTPs typically expire after 5-10 minutes
// User should request a new OTP
```
2. **Verify user details match:**
```typescript theme={null}
// All user details must match exactly
// (name, DOB, organization ID, partner user ID)
```
### Browser Compatibility
**Problem:** postMessage API not supported.
**Solution:**
```javascript theme={null}
// Check for support
if (typeof window.postMessage === 'undefined') {
console.error('postMessage not supported');
// Fallback to query parameters
return ;
}
```
**Problem:** URLSearchParams not supported in older browsers.
**Solution:**
```javascript theme={null}
// Check for support and provide fallback
function buildQueryString(params) {
if (typeof URLSearchParams !== 'undefined') {
return new URLSearchParams(params).toString();
}
// Fallback: manual encoding
return Object.entries(params)
.map(([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`
)
.join('&');
}
```
**Problem:** Iframe doesn't load on iOS Safari.
**Solutions:**
1. **Check iframe attributes:**
```html theme={null}
```
2. **Ensure HTTPS:**
```javascript theme={null}
// iOS Safari requires HTTPS for iframes
const url = "https://embed.v2.doshi.app"; // Must be HTTPS
```
### Mobile WebView Issues
**Problem:** When keyboard opens, content gets squeezed and becomes not visible.
**Solution:** Handle keyboard visibility in your app wrapper.
**iOS:**
```swift theme={null}
// Adjust WebView constraints when keyboard shows
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
```
**Android:**
```xml theme={null}
```
See [Best Practices - Mobile Integration](/webview/best-practices#mobile-app-integration) for complete code.
**Problem:** Users can pinch-to-zoom and it breaks the layout.
**Solution:** Disable zoom in your WebView.
**Web:**
```html theme={null}
```
**iOS:**
```swift theme={null}
webView.scrollView.isScrollEnabled = false
webView.scrollView.bounces = false
```
**Android:**
```kotlin theme={null}
webView.settings.apply {
setSupportZoom(false)
builtInZoomControls = false
}
```
See [Best Practices - Disable Zoom](/webview/best-practices#disable-zoom) for details.
**Problem:** External links clicked in Doshi Frontend don't open.
**Cause:** Popups are blocked for security reasons.
**Solution:** Implement postMessage callback to handle link clicks.
```typescript theme={null}
window.addEventListener('message', (event) => {
if (event.origin !== 'https://embed.v2.doshi.app') return;
const data = JSON.parse(event.data);
if (data.type === 'EXTERNAL_LINK') {
// Handle the link in your environment
window.open(data.url, '_blank', 'noopener,noreferrer');
}
});
```
See [Best Practices - Handling Link Clicks](/webview/best-practices#handling-link-clicks) for platform-specific implementations.
**Problem:** Doshi Frontend doesn't load in mobile WebView.
**Solutions:**
1. **Enable JavaScript:**
```kotlin theme={null}
// Android
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
```
```swift theme={null}
// iOS
let preferences = WKPreferences()
preferences.javaScriptEnabled = true
webConfiguration.preferences = preferences
```
2. **Check HTTPS:**
* Ensure you're loading `https://embed.v2.doshi.app`
* iOS requires HTTPS by default (App Transport Security)
3. **Check App Permissions:**
```xml theme={null}
```
```xml theme={null}
NSAppTransportSecurityNSAllowsArbitraryLoads
```
### Network Issues
**Problem:** Authentication takes too long.
**Solutions:**
1. **Add timeout:**
```typescript theme={null}
const AUTH_TIMEOUT = 10000; // 10 seconds
const authenticateWithTimeout = () => {
return Promise.race([
authenticate(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), AUTH_TIMEOUT)
)
]);
};
```
2. **Show loading state:**
```tsx theme={null}
This may take a moment...
```
3. **Preconnect to domain:**
```html theme={null}
```
**Problem:** Authentication works sometimes but fails randomly.
**Solutions:**
1. **Implement retry logic:**
```typescript theme={null}
const authenticateWithRetry = async (maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
return await authenticate();
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
};
```
2. **Check network status:**
```javascript theme={null}
if (!navigator.onLine) {
setError('No internet connection');
return;
}
window.addEventListener('online', () => {
console.log('Connection restored');
retry();
});
```
### IP Whitelisting Issues
**Problem:** Receiving 403 Forbidden when calling Doshi API.
**Cause:** Your server IP is not whitelisted.
**Solution:**
1. Find your server IP:
```bash theme={null}
curl ifconfig.me
```
2. Email [hello@doshi.app](mailto:hello@doshi.app) with:
* Your organization name
* Your server IP address
* Whether it's for production or staging
3. Wait for confirmation that your IP is whitelisted
**Problem:** API works in staging but not production (or vice versa).
**Cause:** Different servers have different IPs.
**Solution:**
Get the IP from each server and have all of them whitelisted:
```bash theme={null}
# Run on each server
curl ifconfig.me
```
Contact [hello@doshi.app](mailto:hello@doshi.app) with all IPs.
## Debugging Tools
### Enable Debug Logging
```typescript theme={null}
// config.ts
export const DEBUG = process.env.NODE_ENV === 'development';
// utils/logger.ts
export const logger = {
debug: (...args: any[]) => {
if (DEBUG) {
console.log('[Doshi Debug]', ...args);
}
},
error: (...args: any[]) => {
console.error('[Doshi Error]', ...args);
}
};
// Usage
logger.debug('Sending auth message:', authData);
logger.error('Authentication failed:', error);
```
### Message Inspector
```javascript theme={null}
// Log all messages for debugging
window.addEventListener("message", (event) => {
console.group('📨 Message Received');
console.log('Origin:', event.origin);
console.log('Data:', event.data);
console.log('Type:', typeof event.data);
console.log('Timestamp:', new Date().toISOString());
console.groupEnd();
}, true); // Use capture phase
```
### Network Monitor
```javascript theme={null}
// Monitor all fetch requests
const originalFetch = window.fetch;
window.fetch = async (...args) => {
console.log('🌐 Fetch:', args[0]);
const response = await originalFetch(...args);
console.log('✅ Response:', response.status, args[0]);
return response;
};
```
### Check iframe Status
```javascript theme={null}
function debugIframe() {
const iframe = document.querySelector('iframe');
console.log('Iframe found:', !!iframe);
console.log('Iframe src:', iframe?.src);
console.log('Iframe loaded:', iframe?.contentWindow !== null);
console.log('Can access contentWindow:', {
exists: !!iframe?.contentWindow,
canPostMessage: typeof iframe?.contentWindow?.postMessage === 'function'
});
}
```
## Testing Checklist
Test with valid credentials
Test with invalid credentials
Test with missing token
Test with expired token
Test 2FA flow (if enabled)
Test on Chrome
Test on Firefox
Test on Safari
Test on Edge
Test on mobile Safari (iOS)
Test on mobile Chrome (Android)
Test with slow network (throttling)
Test with offline/online transitions
Test timeout scenarios
Test error recovery
## Getting Help
If you're still experiencing issues after trying these solutions:
Email us at [hello@doshi.app](mailto:hello@doshi.app) with:
* Detailed description of the issue
* Browser and OS information
* Any error messages
* Steps to reproduce
Verify Doshi services are operational
## Quick Fixes Reference
| Issue | Quick Fix |
| --------------------- | --------------------------------------------------- |
| Messages not received | Check `event.origin` validation |
| Token not working | Generate fresh token, don't reuse |
| API key exposed | Move to backend immediately |
| URL too long | Switch to postMessage |
| 2FA not showing | Verify `is2FaEnabled: true` and all required fields |
| Slow loading | Add preconnect, implement timeout |
| Origin mismatch | Log and compare actual vs expected origins |
| CORS errors | Ensure all URLs use HTTPS |
| Iframe not loading | Check src attribute and network tab |
## Next Steps
Review implementation guidelines
Ensure your implementation is secure