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.How It Works
1
Generate Custom Token
Your backend calls the Doshi API to generate a custom token
2
Webview Sends PING
The embedded webview sends a PING message to signal it’s ready
3
Parent Receives PING
Parent window listens for the PING and prepares authentication data
4
Parent Sends AUTH
Parent sends the custom token and user data back to the webview
5
Webview Processes AUTH
Webview receives and processes the authentication, handling 2FA if needed
TypeScript Interfaces
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;
}
React Implementation
Complete Example with API Call
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<Props> = ({
userEmail,
segment,
branchId,
is2FaEnabled = false,
dob,
organizationId,
partnerUserId,
firstName,
lastName
}) => {
const [authToken, setAuthToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<div className="flex items-center justify-center h-screen">
<div className="text-center">
<div className="spinner mb-4"></div>
<p>Loading...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-screen">
<div className="text-center text-red-600">
<p>{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
>
Retry
</button>
</div>
</div>
);
}
if (!authToken) {
return <div>Failed to authenticate</div>;
}
return (
<div className="webview-container h-screen w-full">
<iframe
src="https://embed.v2.doshi.app"
className="h-full w-full"
frameBorder="0"
allowFullScreen
/>
</div>
);
};
export default WebviewComponent;
Usage Example
import WebviewComponent from './WebviewComponent';
function App() {
return (
<div className="app">
<h1>My Application</h1>
<WebviewComponent
userEmail="user@example.com"
segment="premium"
branchId="branch_789"
/>
</div>
);
}
// With 2FA enabled
function AppWith2FA() {
return (
<div className="app">
<h1>My Application</h1>
<WebviewComponent
userEmail="user@example.com"
segment="premium"
branchId="branch_789"
is2FaEnabled={true}
dob="1990-01-15"
organizationId="org_123"
partnerUserId="partner_123"
firstName="John"
lastName="Doe"
/>
</div>
);
}
Vanilla JavaScript Implementation
Parent Window
// 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 = `
<div class="error-message">
<p>${message}</p>
<button onclick="window.location.reload()">Retry</button>
</div>
`;
}
// Initialize
fetchAuthToken();
HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Doshi Integration</title>
<style>
#doshi-container {
width: 100%;
height: 100vh;
position: relative;
}
.error-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
text-align: center;
}
.error-message button {
margin-top: 1rem;
padding: 0.5rem 1rem;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 0.375rem;
cursor: pointer;
}
</style>
</head>
<body>
<div id="doshi-container"></div>
<script src="doshi-integration.js"></script>
</body>
</html>
Backend Implementation
Your backend should handle the API key securely:// 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
// 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
// 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:const WebviewWithRetry: React.FC<Props> = ({ 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
Show Available Types
Show Available Types
From Iframe to Parent:
PING: Sent by webview to signal readinessAUTH_SUCCESS: Authentication completed successfullyAUTH_ERROR: Authentication failed
AUTH: Sent by parent with authentication data
Testing postMessage Flow
// 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
Messages not being received
Messages not being received
Solutions:
- Check that origin validation matches exactly
- Verify iframe has loaded before sending messages
- Ensure JSON.stringify is used when sending data
- Check browser console for errors
Token not available
Token not available
Solutions:
- Ensure backend API call completed successfully
- Check network tab for API response
- Verify error handling in token fetch logic
- Check that API key is valid
2FA not triggering
2FA not triggering
Solutions:
- Verify
is2FaEnabledis set totrue - Ensure all required 2FA fields are provided
- Check that organization has 2FA enabled
- Verify user phone number is registered
Next Steps
Security Guidelines
Review security best practices
Best Practices
Optimize your implementation
