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:

Portal URL Format
https://portal.hookmesh.com/{portal_token}?return_url={return_url}
ParameterRequiredDescription
portal_tokenYesSigned token from the Portal Tokens API
return_urlNoURL to redirect to after user exits portal
sectionNoDeep link to specific section (endpoints, subscriptions, logs)
Generate Portal URL
// 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}
Token expiration: Portal tokens expire after the specified duration (default: 1 hour, min: 60 seconds, max: 24 hours). Generate a new token each time a user accesses the portal. Tokens grant full read-write access by default—use 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

HTML Structure
<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:

Auto-resize Listener
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

WebhookPortalEmbed.tsx
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:

PermissionPurpose
allow-scriptsEnable JavaScript for portal functionality
allow-same-originAllow portal to access its own storage and cookies
allow-formsEnable form submission for endpoint management
allow-popupsAllow opening links in new tabs (optional)

Standalone Redirect

For simpler integrations, redirect users to the standalone portal. They'll return to your application when finished.

Redirect Implementation
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:

Track Portal 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:

SectionDescriptionExample URL
endpointsEndpoint management page?section=endpoints
subscriptionsEvent type subscriptions?section=subscriptions
logsDelivery logs and history?section=logs
settingsAccount settings?section=settings
Deep Link Example
// 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:

Resource Deep Links
// 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 Forms
// 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:

Action Triggers
// 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:

TypeScript - Deep Link Builder
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'
  })
};
Python - Deep Link Builder
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

ParameterTypeDescriptionExample
sectionstringPortal section to displayendpoints
endpoint_idstringSpecific endpoint IDep_abc123
job_idstringSpecific webhook job IDjob_xyz789
actionstringAction to triggercreate, test
urlstringPre-fill endpoint URLhttps://api.customer.com/webhooks
event_typesstringComma-separated event typesuser.created,user.updated
return_urlstringURL to return after closehttps://yourapp.com/dashboard

Use Cases

Practical examples of deep linking in real-world scenarios:

Customer Support
// Link support tickets directly to specific endpoints
const supportLink = buildPortalDeepLink(token, {
  section: 'endpoints',
  endpointId: customerEndpointId,
  returnUrl: `https://support.yourapp.com/tickets/${ticketId}`
});
Onboarding Flow
// 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']
  }
});
Debugging
// Link from error logs to specific failed jobs
const debugLink = buildPortalDeepLink(token, {
  section: 'logs',
  jobId: failedJobId
});
Deep Linking Best Practice: Use resource-specific deep links in your application to provide contextual access to the portal, reducing clicks and improving user experience.

Message Passing API

The portal communicates with your application via postMessage. All messages follow this structure:

Message Format
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 TypePayloadDescription
hookmesh:readyPortal 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:

Loading State Component
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
Mobile-Responsive Container
/* 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:

Comprehensive Error Handling
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);
  }
}
ErrorCauseSolution
Token expiredToken TTL exceeded or already usedGenerate new token
Invalid tokenMalformed or incorrect tokenCheck token generation
Iframe blockedCSP or browser securityUpdate CSP headers or use redirect
Loading timeoutNetwork issuesShow 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
Production tip: Cache the customer's application ID in your backend to avoid extra API calls when generating portal tokens.

Complete Example

Here's a complete implementation combining all best practices:

Full Implementation (React + Next.js)
'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>
  );
}

Next Steps