Embedding the Portal
The Hook Mesh customer portal can be integrated into your application in two ways: as an embedded iframe for a seamless in-app experience, or as a standalone page that users are redirected to. Choose the approach that best fits your application architecture.
Integration Approaches
Iframe Embedding
Embed the portal directly in your application for a seamless experience. Best for applications that want full control over navigation and styling.
- ✓Seamless user experience without leaving your app
- ✓Full control over page layout and navigation
- ✓Ability to handle portal events in your application
- ×Requires postMessage integration for height adjustment
Standalone Redirect
Redirect users to a standalone portal page. Best for simple integrations or when you want users to focus solely on webhook management.
- ✓Simple implementation with just a redirect
- ✓No iframe security considerations
- ✓Automatic return to your app after completion
- ×Users leave your application temporarily
Portal URL Structure
Before embedding or redirecting, you need to generate a signed portal URL for your customer. The URL structure is:
https://portal.hookmesh.com/{portal_token}?return_url={return_url}| Parameter | Required | Description |
|---|---|---|
portal_token | Yes | Signed token from the Portal Tokens API |
return_url | No | URL to redirect to after user exits portal |
section | No | Deep link to specific section (endpoints, subscriptions, logs) |
// Generate a portal token for the customer
const response = await fetch(
'https://api.hookmesh.com/v1/applications/app_abc123/portal-tokens',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
expires_in: 3600, // Token valid for 1 hour (default)
permissions: ['read', 'write'] // Full access (default)
})
}
);
const { url, token } = await response.json();
// Use the pre-constructed URL (token already in path)
const returnUrl = encodeURIComponent('https://yourapp.com/settings/webhooks');
const portalUrl = `${url}?return_url=${returnUrl}`;
// Or manually construct: https://portal.hookmesh.com/{token}?return_url={returnUrl}permissions: ["read"] for read-only access in support scenarios.Iframe Embedding
Embedding the portal in an iframe provides a seamless experience. The portal automatically adjusts its height and communicates with your application via postMessage.
Basic Implementation
<div id="webhook-portal-container">
<iframe
id="webhook-portal"
src="https://portal.hookmesh.com/pt_abc123"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
style="width: 100%; border: none; min-height: 600px;"
title="Webhook Management Portal"
></iframe>
</div>Responsive Height Adjustment
The portal sends height update messages to automatically resize the iframe as content changes. Implement a message listener to handle these events:
window.addEventListener('message', (event) => {
// Verify the message is from Hook Mesh
if (event.origin !== 'https://portal.hookmesh.com') {
return;
}
const { type, payload } = event.data;
switch (type) {
case 'hookmesh:resize':
// Adjust iframe height
const iframe = document.getElementById('webhook-portal');
if (iframe && payload.height) {
iframe.style.height = `${payload.height}px`;
}
break;
case 'hookmesh:navigate':
// Handle navigation events (optional)
console.log('User navigated to:', payload.section);
break;
case 'hookmesh:close':
// User clicked "Done" or similar
window.location.href = '/settings/webhooks';
break;
}
});React Component Example
import { useEffect, useRef, useState } from 'react';
interface PortalEmbedProps {
portalToken: string;
onClose?: () => void;
onNavigate?: (section: string) => void;
}
export function WebhookPortalEmbed({
portalToken,
onClose,
onNavigate
}: PortalEmbedProps) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [height, setHeight] = useState(600);
const [loading, setLoading] = useState(true);
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
// Security: verify origin
if (event.origin !== 'https://portal.hookmesh.com') {
return;
}
const { type, payload } = event.data;
switch (type) {
case 'hookmesh:resize':
if (payload.height) {
setHeight(payload.height);
}
break;
case 'hookmesh:ready':
setLoading(false);
break;
case 'hookmesh:navigate':
onNavigate?.(payload.section);
break;
case 'hookmesh:close':
onClose?.();
break;
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [onClose, onNavigate]);
return (
<div className="relative">
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-background">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
</div>
)}
<iframe
ref={iframeRef}
src={`https://portal.hookmesh.com/${portalToken}`}
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
style={{
width: '100%',
height: `${height}px`,
border: 'none',
display: loading ? 'none' : 'block',
}}
title="Webhook Management Portal"
/>
</div>
);
}Iframe Security
The sandbox attribute restricts iframe capabilities for security. Hook Mesh portal requires these permissions:
| Permission | Purpose |
|---|---|
allow-scripts | Enable JavaScript for portal functionality |
allow-same-origin | Allow portal to access its own storage and cookies |
allow-forms | Enable form submission for endpoint management |
allow-popups | Allow opening links in new tabs (optional) |
frame-src https://portal.hookmesh.com to allow iframe embedding.Standalone Redirect
For simpler integrations, redirect users to the standalone portal. They'll return to your application when finished.
async function openWebhookPortal() {
try {
// Generate portal token
const response = await fetch('/api/portal-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customer_id: currentUser.id
})
});
const { url } = await response.json();
// Redirect to portal with return URL
const returnUrl = encodeURIComponent(window.location.href);
window.location.href = `${url}?return_url=${returnUrl}`;
} catch (error) {
console.error('Failed to open portal:', error);
alert('Unable to open webhook portal. Please try again.');
}
}
// Usage in your UI
document.getElementById('manage-webhooks-btn').addEventListener('click', openWebhookPortal);
// Backend endpoint (Node.js)
app.post('/api/portal-token', async (req, res) => {
const customerId = req.user.id;
const application = await getCustomerApplication(customerId);
const response = await fetch(
`https://api.hookmesh.com/v1/applications/${application.id}/portal-tokens`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ expires_in: 3600 })
}
);
const data = await response.json();
res.json(data);
});Return URL Handling
When users complete their portal session, they're redirected back to the return_url. You can append query parameters to track completion:
// When opening portal, include a state parameter
const returnUrl = encodeURIComponent(
`${window.location.href}?portal_completed=true`
);
const portalUrl = `https://portal.hookmesh.com/${token}?return_url=${returnUrl}`;
// Or use the pre-constructed URL from API response:
// const portalUrl = `${apiResponse.url}?return_url=${returnUrl}`;
// After redirect back, check for completion
if (new URLSearchParams(window.location.search).get('portal_completed') === 'true') {
// Show success message
showNotification('Webhook settings updated successfully!');
// Clean up URL
window.history.replaceState({}, '', window.location.pathname);
}Deep Linking
Direct users to specific sections of the portal using the section parameter:
| Section | Description | Example URL |
|---|---|---|
endpoints | Endpoint management page | ?section=endpoints |
subscriptions | Event type subscriptions | ?section=subscriptions |
logs | Delivery logs and history | ?section=logs |
settings | Account settings | ?section=settings |
// Direct users to the logs page
const portalUrl = `https://portal.hookmesh.com/${token}?section=logs`;
// Or combine with return URL
const returnUrl = encodeURIComponent('https://yourapp.com/webhooks');
const portalUrl = `https://portal.hookmesh.com/${token}?section=endpoints&return_url=${returnUrl}`;Resource-Specific Deep Links
Link directly to specific resources within the portal:
// Link to a specific endpoint
const endpointUrl = `https://portal.hookmesh.com/${token}?section=endpoints&endpoint_id=ep_abc123`;
// Link to endpoint logs
const logsUrl = `https://portal.hookmesh.com/${token}?section=logs&endpoint_id=ep_abc123`;
// Link to a specific webhook job
const jobUrl = `https://portal.hookmesh.com/${token}?section=logs&job_id=job_xyz789`;Pre-Filled Forms
Deep link with pre-populated data for quick setup:
// Pre-fill new endpoint form
const newEndpointUrl = new URL(`https://portal.hookmesh.com/${token}`);
newEndpointUrl.searchParams.set('section', 'endpoints');
newEndpointUrl.searchParams.set('action', 'create');
newEndpointUrl.searchParams.set('url', 'https://api.customer.com/webhooks');
newEndpointUrl.searchParams.set('event_types', 'user.created,user.updated');
// Pre-fill subscription form
const subscribeUrl = new URL(`https://portal.hookmesh.com/${token}`);
subscribeUrl.searchParams.set('section', 'subscriptions');
subscribeUrl.searchParams.set('endpoint_id', 'ep_abc123');
subscribeUrl.searchParams.set('event_types', 'payment.succeeded,payment.failed');Action Triggers
Trigger specific actions directly:
// Open test webhook modal for specific endpoint
const testUrl = `https://portal.hookmesh.com/${token}?section=endpoints&endpoint_id=ep_abc123&action=test`;
// Trigger secret rotation
const rotateUrl = `https://portal.hookmesh.com/${token}?section=endpoints&endpoint_id=ep_abc123&action=rotate_secret`;
// Show endpoint creation modal
const createUrl = `https://portal.hookmesh.com/${token}?section=endpoints&action=create`;Complete Deep Link Builder
Helper function to build deep links:
type PortalSection = 'endpoints' | 'subscriptions' | 'logs' | 'settings';
type PortalAction = 'create' | 'edit' | 'test' | 'rotate_secret' | 'delete';
interface DeepLinkOptions {
section: PortalSection;
endpointId?: string;
jobId?: string;
action?: PortalAction;
prefill?: Record<string, string | string[]>;
returnUrl?: string;
}
function buildPortalDeepLink(token: string, options: DeepLinkOptions): string {
const url = new URL(`https://portal.hookmesh.com/${token}`);
// Add section
url.searchParams.set('section', options.section);
// Add resource IDs
if (options.endpointId) {
url.searchParams.set('endpoint_id', options.endpointId);
}
if (options.jobId) {
url.searchParams.set('job_id', options.jobId);
}
// Add action
if (options.action) {
url.searchParams.set('action', options.action);
}
// Add prefill data
if (options.prefill) {
Object.entries(options.prefill).forEach(([key, value]) => {
if (Array.isArray(value)) {
url.searchParams.set(key, value.join(','));
} else {
url.searchParams.set(key, value);
}
});
}
// Add return URL
if (options.returnUrl) {
url.searchParams.set('return_url', options.returnUrl);
}
return url.toString();
}
// Usage examples
const links = {
viewEndpoint: buildPortalDeepLink(token, {
section: 'endpoints',
endpointId: 'ep_abc123'
}),
createEndpoint: buildPortalDeepLink(token, {
section: 'endpoints',
action: 'create',
prefill: {
url: 'https://api.customer.com/webhooks',
event_types: ['user.created', 'user.updated']
}
}),
testWebhook: buildPortalDeepLink(token, {
section: 'endpoints',
endpointId: 'ep_abc123',
action: 'test'
}),
viewLogs: buildPortalDeepLink(token, {
section: 'logs',
endpointId: 'ep_abc123',
returnUrl: 'https://yourapp.com/dashboard'
})
};from typing import Optional, Dict, List, Union
from urllib.parse import urlencode, quote
def build_portal_deep_link(
token: str,
section: str,
endpoint_id: Optional[str] = None,
job_id: Optional[str] = None,
action: Optional[str] = None,
prefill: Optional[Dict[str, Union[str, List[str]]]] = None,
return_url: Optional[str] = None
) -> str:
"""Build a deep link to Hook Mesh Customer Portal."""
params = {'section': section}
if endpoint_id:
params['endpoint_id'] = endpoint_id
if job_id:
params['job_id'] = job_id
if action:
params['action'] = action
if return_url:
params['return_url'] = return_url
if prefill:
for key, value in prefill.items():
if isinstance(value, list):
params[key] = ','.join(value)
else:
params[key] = value
query = urlencode(params, quote_via=quote)
return f'https://portal.hookmesh.com/{token}?{query}'
# Usage
links = {
'view_endpoint': build_portal_deep_link(
token,
section='endpoints',
endpoint_id='ep_abc123'
),
'create_endpoint': build_portal_deep_link(
token,
section='endpoints',
action='create',
prefill={
'url': 'https://api.customer.com/webhooks',
'event_types': ['user.created', 'user.updated']
}
)
}Supported Parameters Reference
| Parameter | Type | Description | Example |
|---|---|---|---|
section | string | Portal section to display | endpoints |
endpoint_id | string | Specific endpoint ID | ep_abc123 |
job_id | string | Specific webhook job ID | job_xyz789 |
action | string | Action to trigger | create, test |
url | string | Pre-fill endpoint URL | https://api.customer.com/webhooks |
event_types | string | Comma-separated event types | user.created,user.updated |
return_url | string | URL to return after close | https://yourapp.com/dashboard |
Use Cases
Practical examples of deep linking in real-world scenarios:
// Link support tickets directly to specific endpoints
const supportLink = buildPortalDeepLink(token, {
section: 'endpoints',
endpointId: customerEndpointId,
returnUrl: `https://support.yourapp.com/tickets/${ticketId}`
});// Guide new customers through endpoint setup
const onboardingLink = buildPortalDeepLink(token, {
section: 'endpoints',
action: 'create',
prefill: {
url: 'https://api.customer.com/webhooks',
event_types: ['order.created']
}
});// Link from error logs to specific failed jobs
const debugLink = buildPortalDeepLink(token, {
section: 'logs',
jobId: failedJobId
});Message Passing API
The portal communicates with your application via postMessage. All messages follow this structure:
interface HookMeshMessage {
type: string;
payload: Record<string, any>;
}
// Example messages:
{
type: 'hookmesh:resize',
payload: { height: 850 }
}
{
type: 'hookmesh:navigate',
payload: { section: 'endpoints', path: '/endpoints/ep_123' }
}
{
type: 'hookmesh:close',
payload: { reason: 'user_action' }
}| Message Type | Payload | Description |
|---|---|---|
hookmesh:ready | Portal loaded and ready | |
hookmesh:resize | { height } | Content height changed |
hookmesh:navigate | { section, path } | User navigated to section |
hookmesh:close | { reason } | User requested to close portal |
hookmesh:error | { code, message } | Error occurred in portal |
Loading States
Provide visual feedback while the portal loads:
function WebhookPortalWithLoading({ portalToken }: { portalToken: string }) {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.origin !== 'https://portal.hookmesh.com') return;
const { type, payload } = event.data;
if (type === 'hookmesh:ready') {
setIsLoading(false);
} else if (type === 'hookmesh:error') {
setError(payload.message);
setIsLoading(false);
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, []);
if (error) {
return (
<div className="rounded-lg border border-red-500/50 bg-red-500/10 p-6">
<h3 className="text-lg font-medium text-red-400 mb-2">
Failed to Load Portal
</h3>
<p className="text-sm text-red-300">{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-red-500 text-white rounded-lg"
>
Retry
</button>
</div>
);
}
return (
<div className="relative min-h-[600px]">
{isLoading && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-background">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mb-4" />
<p className="text-foreground-secondary">Loading webhook portal...</p>
</div>
)}
<iframe
src={`https://portal.hookmesh.com/${portalToken}`}
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
className="w-full h-full border-none"
style={{ display: isLoading ? 'none' : 'block' }}
title="Webhook Management Portal"
/>
</div>
);
}Mobile Responsiveness
The portal is fully responsive and adapts to mobile screens. For the best mobile experience:
- Iframe embedding: Ensure your container has proper mobile styles
- Standalone redirect: Preferred on mobile for full-screen experience
- Touch targets: Portal uses 44px minimum touch targets for accessibility
- Viewport meta tag: Ensure your page includes proper viewport configuration
/* Desktop: Fixed width with padding */
.portal-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
/* Tablet: Reduce padding */
@media (max-width: 768px) {
.portal-container {
padding: 1rem;
}
}
/* Mobile: Full width, minimal padding */
@media (max-width: 640px) {
.portal-container {
padding: 0.5rem;
max-width: 100%;
}
/* Consider redirect instead of iframe on mobile */
.portal-container.use-redirect-on-mobile iframe {
display: none;
}
}Error Handling
Handle common error scenarios gracefully:
async function openPortal() {
try {
// Show loading state
setLoading(true);
// Generate portal token
const response = await fetch('/api/portal-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customer_id: currentUser.id })
});
if (!response.ok) {
throw new Error(`Failed to generate portal token: ${response.status}`);
}
const { token } = await response.json();
if (!token) {
throw new Error('No token returned from API');
}
// Open portal
setPortalToken(token);
setError(null);
} catch (error) {
console.error('Portal error:', error);
// User-friendly error message
setError(
'Unable to open webhook portal. Please try again or contact support if the problem persists.'
);
// Optional: Track error in monitoring service
trackError('portal_open_failed', error);
} finally {
setLoading(false);
}
}| Error | Cause | Solution |
|---|---|---|
| Token expired | Token TTL exceeded or already used | Generate new token |
| Invalid token | Malformed or incorrect token | Check token generation |
| Iframe blocked | CSP or browser security | Update CSP headers or use redirect |
| Loading timeout | Network issues | Show retry button after 10s |
Best Practices
- Preload tokens: Generate tokens just before opening the portal to minimize latency
- Short expiration: Use 1-hour token expiration for security (default)
- Loading states: Always show loading indicators while portal initializes
- Error handling: Provide clear error messages and retry mechanisms
- Test on mobile: Verify portal works on various mobile devices and browsers
- Monitor usage: Track portal opens and errors in your analytics
- Return URLs: Always provide return URLs for better user experience
- Deep links: Use section parameters to guide users to specific actions
Complete Example
Here's a complete implementation combining all best practices:
'use client';
import { useEffect, useRef, useState } from 'react';
interface WebhookPortalProps {
customerId: string;
onComplete?: () => void;
defaultSection?: 'endpoints' | 'subscriptions' | 'logs' | 'settings';
}
export function WebhookPortal({
customerId,
onComplete,
defaultSection
}: WebhookPortalProps) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [portalUrl, setPortalUrl] = useState<string | null>(null);
const [height, setHeight] = useState(600);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Generate portal URL on mount
useEffect(() => {
generatePortalUrl();
}, [customerId, defaultSection]);
// Handle postMessage events
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.origin !== 'https://portal.hookmesh.com') return;
const { type, payload } = event.data;
switch (type) {
case 'hookmesh:ready':
setLoading(false);
break;
case 'hookmesh:resize':
setHeight(payload.height || 600);
break;
case 'hookmesh:close':
onComplete?.();
break;
case 'hookmesh:error':
setError(payload.message);
setLoading(false);
break;
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [onComplete]);
async function generatePortalUrl() {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/webhooks/portal-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customer_id: customerId })
});
if (!response.ok) {
throw new Error('Failed to generate portal token');
}
const { token } = await response.json();
let url = `https://portal.hookmesh.com/${token}`;
if (defaultSection) {
url += `?section=${defaultSection}`;
}
setPortalUrl(url);
} catch (err) {
setError('Unable to load webhook portal. Please try again.');
setLoading(false);
}
}
if (error) {
return (
<div className="rounded-lg border border-red-500/50 bg-red-500/10 p-8 text-center">
<h3 className="text-lg font-medium text-red-400 mb-2">
Failed to Load Portal
</h3>
<p className="text-sm text-red-300 mb-4">{error}</p>
<button
onClick={generatePortalUrl}
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
>
Try Again
</button>
</div>
);
}
return (
<div className="relative" style={{ minHeight: `${height}px` }}>
{loading && (
<div className="absolute inset-0 flex flex-col items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4" />
<p className="text-gray-400">Loading webhook portal...</p>
</div>
)}
{portalUrl && (
<iframe
ref={iframeRef}
src={portalUrl}
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
style={{
width: '100%',
height: `${height}px`,
border: 'none',
display: loading ? 'none' : 'block',
}}
title="Webhook Management Portal"
/>
)}
</div>
);
}
// Usage in your application
export default function WebhooksPage() {
const [showPortal, setShowPortal] = useState(false);
return (
<div className="container mx-auto py-8">
<h2 className="text-3xl font-bold mb-6">Webhook Management</h2>
{showPortal ? (
<WebhookPortal
customerId="cust_123"
defaultSection="endpoints"
onComplete={() => {
setShowPortal(false);
alert('Webhook settings updated!');
}}
/>
) : (
<button
onClick={() => setShowPortal(true)}
className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
>
Manage Webhooks
</button>
)}
</div>
);
}