Customer Portal Authentication
Portal access tokens are short-lived JWT tokens that authenticate your customers to the Hook Mesh customer portal. Generate them on-demand to grant secure, time-limited access to webhook management.
Portal Token Model
Portal tokens use a secure authentication model designed for customer-facing interfaces:
Short-Lived
Tokens expire after 1 hour by default (configurable up to 24 hours). This limits exposure if tokens are compromised.
Application-Scoped
Each token grants access to a single application's webhooks. Customers can only manage their own endpoints and event types.
Permission-Based
Choose between read-only (view webhooks) or read-write (full management) permissions when generating tokens.
Stateless JWT
Tokens are signed JWTs that can't be revoked early. They automatically expire after the configured duration.
["read", "write"] (full access). For support scenarios, explicitly set permissions: ["read"] for read-only access.Generating Portal Tokens
Create a portal token by sending a POST request to the portal tokens endpoint:
API Endpoint
POST /v1/applications/{application_id}/portal-tokens
Authorization: Bearer sk_live_abc123...
Content-Type: application/json
{
"expires_in": 3600,
"permissions": ["read", "write"]
}Request Parameters
| Parameter | Type | Description |
|---|---|---|
expires_in | integer | Token lifetime in seconds. Min: 60, Default: 3600 (1 hour), Max: 86400 (24 hours) |
permissions | array | Array of permissions: ["read"] or ["read", "write"]. Default: ["read", "write"] |
Response Format
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBfaWQiOiJhcHBfMTIzIiwicGVybWlzc2lvbnMiOlsicmVhZCIsIndyaXRlIl0sImV4cCI6MTczNzM4NDAwMH0.S2F3YUJhc2U2NEVuY29kZWRTaWduYXR1cmU",
"url": "https://portal.hookmesh.com/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_at": "2026-01-20T16:30:00Z"
}| Field | Description |
|---|---|
token | JWT token string to authenticate the customer |
expires_at | ISO 8601 timestamp when the token expires |
url | Pre-constructed portal URL with token in path (ready to redirect) |
Permission Scopes
Portal tokens support two permission levels to control what customers can do:
Read Permission
View-only access to webhook data:
- View configured endpoints
- View event types and their schemas
- View webhook delivery logs and history
- View retry attempts and failure details
- Cannot create, update, or delete anything
{
"expires_in": 7200,
"permissions": ["read"]
}Write Permission
Full management capabilities (includes read):
- Create and configure webhook endpoints
- Update endpoint URLs and settings
- Enable/disable endpoints
- Subscribe to event types
- Test endpoint connectivity
- Rotate endpoint secrets
- View all read-permission data
{
"expires_in": 3600,
"permissions": ["read", "write"]
}Code Examples
Node.js
async function generatePortalToken(applicationId) {
const response = await fetch(
`https://api.hookmesh.com/v1/applications/${applicationId}/portal-tokens`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
expires_in: 3600, // 1 hour
permissions: ['read', 'write']
})
}
);
if (!response.ok) {
throw new Error(`Failed to generate token: ${response.statusText}`);
}
const data = await response.json();
return data.token;
}
// Usage in Express route
app.get('/webhook-settings', async (req, res) => {
const customerId = req.user.id;
// Get or create application for this customer
const application = await getCustomerApplication(customerId);
// Generate fresh portal token
const result = await generatePortalToken(application.id);
// Redirect to portal (token in URL path)
res.redirect(result.url);
});Python
import os
import requests
from datetime import timedelta
def generate_portal_token(application_id, expires_in=3600, read_only=False):
"""Generate a customer portal access token."""
permissions = ['read'] if read_only else ['read', 'write']
response = requests.post(
f'https://api.hookmesh.com/v1/applications/{application_id}/portal-tokens',
headers={
'Authorization': f'Bearer {os.environ.get("HOOKMESH_API_KEY")}',
'Content-Type': 'application/json'
},
json={
'expires_in': expires_in,
'permissions': permissions
}
)
response.raise_for_status()
data = response.json()
return {
'token': data['token'],
'portal_url': data['portal_url'],
'expires_at': data['expires_at']
}
# Usage in Flask route
@app.route('/webhook-settings')
@login_required
def webhook_settings():
customer_id = current_user.id
# Get application for this customer
application = get_customer_application(customer_id)
# Generate token (1 hour expiry)
result = generate_portal_token(
application.id,
expires_in=3600,
read_only=False
)
# Redirect to pre-constructed portal URL
return redirect(result['url'])Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type PortalTokenRequest struct {
ExpiresIn int `json:"expires_in"`
Permissions []string `json:"permissions"`
}
type PortalTokenResponse struct {
Token string `json:"token"`
ExpiresAt string `json:"expires_at"`
PortalURL string `json:"portal_url"`
}
func generatePortalToken(applicationID string) (*PortalTokenResponse, error) {
url := fmt.Sprintf("https://api.hookmesh.com/v1/applications/%s/portal-tokens", applicationID)
payload := PortalTokenRequest{
ExpiresIn: 3600, // 1 hour
Permissions: []string{"read", "write"},
}
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("HOOKMESH_API_KEY")))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to generate token: %s", body)
}
var result PortalTokenResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return &result, nil
}
// Usage in HTTP handler
func webhookSettingsHandler(w http.ResponseWriter, r *http.Request) {
customerID := getCustomerID(r)
// Get application for this customer
application, err := getCustomerApplication(customerID)
if err != nil {
http.Error(w, "Failed to get application", http.StatusInternalServerError)
return
}
// Generate portal token
result, err := generatePortalToken(application.ID)
if err != nil {
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
return
}
// Redirect to portal (token in URL path)
http.Redirect(w, r, result.PortalURL, http.StatusTemporaryRedirect)
}PHP
<?php
function generatePortalToken($applicationId, $expiresIn = 3600, $readOnly = false) {
$apiKey = getenv('HOOKMESH_API_KEY');
$url = "https://api.hookmesh.com/v1/applications/{$applicationId}/portal-tokens";
$permissions = $readOnly ? ['read'] : ['read', 'write'];
$data = [
'expires_in' => $expiresIn,
'permissions' => $permissions
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json'
]);
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($statusCode !== 200) {
throw new Exception("Failed to generate token: " . $response);
}
return json_decode($response, true);
}
// Usage in Laravel controller
class WebhookSettingsController extends Controller
{
public function show(Request $request)
{
$customerId = auth()->id();
// Get application for this customer
$application = $this->getCustomerApplication($customerId);
// Generate portal token
$result = generatePortalToken(
$application->id,
expiresIn: 3600,
readOnly: false
);
// Redirect to portal
return redirect($result['url']);
}
}Integration Patterns
1. Generate on Login
Generate a token immediately when customers log in to your application:
// After successful authentication
app.post('/login', async (req, res) => {
const user = await authenticateUser(req.body);
// Create session
req.session.user = user;
// Pre-generate portal token for webhook settings page
const application = await getCustomerApplication(user.id);
const token = await generatePortalToken(application.id);
// Store token in session (expires soon anyway)
req.session.portalToken = token;
res.json({ success: true });
});2. Generate on Portal Access
Generate a fresh token only when customers click to access webhook settings:
// When customer clicks "Webhook Settings" button
app.get('/webhook-settings', async (req, res) => {
const user = req.session.user;
const application = await getCustomerApplication(user.id);
// Generate fresh token on demand
const result = await generatePortalToken(application.id);
// Redirect immediately
res.redirect(result.portal_url);
});3. Embed in Dashboard
Embed the portal directly in your application using an iframe:
// API endpoint that returns token for frontend
app.get('/api/portal-token', async (req, res) => {
const user = req.session.user;
const application = await getCustomerApplication(user.id);
const result = await generatePortalToken(application.id);
res.json({
token: result.token,
expires_at: result.expires_at
});
});
// Frontend: Embed portal in iframe
async function loadPortal() {
const response = await fetch('/api/portal-token');
const { token } = await response.json();
const iframe = document.getElementById('portal-iframe');
iframe.src = `https://portal.hookmesh.com/${token}`;
}Token Expiration & Renewal
Handling Expired Tokens
When a token expires, the portal will display an error message. Handle this gracefully:
// Listen for portal iframe messages
window.addEventListener('message', async (event) => {
if (event.origin !== 'https://portal.hookmesh.com') return;
if (event.data.type === 'token_expired') {
// Token expired - generate a new one
const response = await fetch('/api/portal-token');
const { token } = await response.json();
// Reload portal with fresh token
const iframe = document.getElementById('portal-iframe');
iframe.src = `https://portal.hookmesh.com/${token}`;
}
});Automatic Renewal
For long-running portal sessions, refresh tokens before they expire:
let tokenExpiresAt;
async function renewTokenBeforeExpiry() {
// Refresh token 5 minutes before expiry
const renewAt = new Date(tokenExpiresAt).getTime() - (5 * 60 * 1000);
const now = Date.now();
const delay = renewAt - now;
if (delay > 0) {
setTimeout(async () => {
const response = await fetch('/api/portal-token');
const { token, expires_at } = await response.json();
tokenExpiresAt = expires_at;
// Update portal with new token
const iframe = document.getElementById('portal-iframe');
iframe.contentWindow.postMessage(
{ type: 'update_token', token },
'https://portal.hookmesh.com'
);
// Schedule next renewal
renewTokenBeforeExpiry();
}, delay);
}
}Token Refresh
For users who remain in the portal longer than the token lifetime, you need a refresh strategy. Here are three common patterns:
Pattern 1: Automatic Redirect on Expiry
The simplest approach - redirect users to regenerate the token:
// React - Detect expiry and redirect
import { useEffect, useState } from 'react';
function PortalWrapper({ appId, initialToken, expiresAt }) {
const [portalUrl, setPortalUrl] = useState(
`https://portal.hookmesh.com/${initialToken}`
);
useEffect(() => {
const expiryTime = new Date(expiresAt).getTime();
const now = Date.now();
const timeUntilExpiry = expiryTime - now;
if (timeUntilExpiry > 0) {
// Set timer to redirect before expiry
const timer = setTimeout(() => {
// Redirect to your token generation endpoint
window.location.href = `/dashboard/webhooks?app_id=${appId}`;
}, timeUntilExpiry - 60000); // Redirect 1 minute before expiry
return () => clearTimeout(timer);
}
}, [appId, expiresAt]);
return <iframe src={portalUrl} />;
}Pattern 2: Background Token Refresh
Refresh the token without interrupting the user:
// React - Background refresh with iframe reload
import { useEffect, useState, useRef } from 'react';
function PortalWithRefresh({ appId, userId }) {
const [portalUrl, setPortalUrl] = useState('');
const [expiresAt, setExpiresAt] = useState<Date | null>(null);
const iframeRef = useRef<HTMLIFrameElement>(null);
// Initial token generation
useEffect(() => {
generateToken(appId, userId);
}, [appId, userId]);
// Refresh timer
useEffect(() => {
if (!expiresAt) return;
const expiryTime = expiresAt.getTime();
const now = Date.now();
const timeUntilExpiry = expiryTime - now;
if (timeUntilExpiry > 0) {
// Refresh 5 minutes before expiry
const refreshTime = timeUntilExpiry - (5 * 60 * 1000);
const timer = setTimeout(async () => {
await generateToken(appId, userId);
// Reload iframe with new token
if (iframeRef.current) {
iframeRef.current.src = portalUrl;
}
}, refreshTime);
return () => clearTimeout(timer);
}
}, [expiresAt, appId, userId, portalUrl]);
async function generateToken(appId: string, userId: string) {
const response = await fetch('/api/portal-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ appId, userId })
});
const { url, expires_at } = await response.json();
setPortalUrl(url);
setExpiresAt(new Date(expires_at));
}
return (
<iframe
ref={iframeRef}
src={portalUrl}
style={{ width: '100%', height: '600px', border: 'none' }}
/>
);
}Pattern 3: Long-Lived Tokens with Read-Only Access
For support scenarios where users need extended access:
// Node.js - Generate long-lived read-only token
const response = await fetch(
`https://api.hookmesh.com/v1/applications/${appId}/portal-tokens`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
expires_in: 86400, // 24 hours (maximum)
permissions: ['read'] // Read-only for safety
})
}
);
const { url, expires_at } = await response.json();# Python - Long-lived read-only token
response = requests.post(
f'https://api.hookmesh.com/v1/applications/{app_id}/portal-tokens',
headers={
'Authorization': f'Bearer {os.getenv("HOOKMESH_API_KEY")}',
'Content-Type': 'application/json'
},
json={
'expires_in': 86400, # 24 hours (maximum)
'permissions': ['read'] # Read-only for safety
}
)
portal_data = response.json()
url = portal_data['url']Handling Refresh in Backend
Your backend token generation endpoint should track refresh requests:
// Express.js - Token refresh endpoint
app.post('/api/portal-token', async (req, res) => {
const { appId, userId } = req.body;
// Verify user has access to this application
const hasAccess = await checkUserAccess(userId, appId);
if (!hasAccess) {
return res.status(403).json({ error: 'Access denied' });
}
// Generate new token
const response = await fetch(
`https://api.hookmesh.com/v1/applications/${appId}/portal-tokens`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
expires_in: 3600, // 1 hour
permissions: ['read', 'write']
})
}
);
const { url, expires_at } = await response.json();
// Optional: Log refresh for audit trail
await logPortalAccess(userId, appId, 'token_refresh');
res.json({ url, expires_at });
});Best Practices
Do:
- Refresh tokens before they expire (5 minutes buffer)
- Use read-only tokens for long-lived sessions
- Log token generation for security auditing
- Handle network errors during refresh gracefully
Don't:
- Don't wait until token expires to refresh
- Don't generate tokens with write access for 24-hour sessions
- Don't cache tokens in localStorage (security risk)
- Don't refresh more frequently than necessary (rate limits)
Security Best Practices
✅ Generate tokens on-demand
Create fresh tokens when customers access the portal, not in advance. This minimizes the window of exposure if tokens are compromised.
✅ Use short expiration times
Default 1-hour expiration is recommended. Only extend for specific use cases like support sessions.
✅ Always use HTTPS
Portal tokens must be transmitted over HTTPS only. Never send tokens over unencrypted HTTP connections.
✅ Validate application ownership
Always verify that the authenticated customer owns the application before generating tokens. Never generate tokens for other customers' applications.
✅ Use read-only for support
Generate read-only tokens when your support team needs to view customer webhook data to prevent accidental changes.
❌ Don't cache long-term
Never store portal tokens in databases or cache them beyond their expiration time. Generate fresh tokens for each session.
❌ Don't log tokens
Tokens are sensitive credentials. Sanitize logs to prevent token exposure. Treat them like passwords.
❌ Don't share tokens
Each customer should get their own token. Never reuse tokens across multiple customers or sessions.
Multi-Tenant Considerations
If your application serves multiple customers, ensure proper isolation:
async function generateCustomerPortalToken(customerId) {
// 1. Authenticate the requesting user
const requestingUser = await getCurrentUser();
// 2. Verify the requesting user has access to this customer
if (!await hasAccessToCustomer(requestingUser, customerId)) {
throw new Error('Unauthorized: Cannot access this customer');
}
// 3. Get the application for this specific customer
const application = await getCustomerApplication(customerId);
if (!application) {
throw new Error('No application found for customer');
}
// 4. Generate token scoped to this customer's application
const result = await generatePortalToken(application.id);
// 5. Log the access for audit trail
await logPortalAccess({
customerId,
applicationId: application.id,
requestingUserId: requestingUser.id,
expiresAt: result.expires_at
});
return result;
}Testing Portal Access
Local Development
During development, test portal token generation without redirecting:
// Development endpoint to test token generation
if (process.env.NODE_ENV === 'development') {
app.get('/dev/test-portal-token', async (req, res) => {
const testApplicationId = 'app_test_123';
try {
const result = await generatePortalToken(testApplicationId);
res.json({
success: true,
token: result.token,
portal_url: result.portal_url,
expires_at: result.expires_at,
decoded: decodeJWT(result.token) // Show token contents
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
}
function decodeJWT(token) {
const [header, payload, signature] = token.split('.');
return {
header: JSON.parse(Buffer.from(header, 'base64').toString()),
payload: JSON.parse(Buffer.from(payload, 'base64').toString())
};
}Testing with cURL
# Generate a portal token
curl -X POST https://api.hookmesh.com/v1/applications/app_abc123/portal-tokens \
-H "Authorization: Bearer sk_test_abc123..." \
-H "Content-Type: application/json" \
-d '{
"expires_in": 3600,
"permissions": ["read", "write"]
}'
# Response
{
"token": "eyJhbGciOi...",
"url": "https://portal.hookmesh.com/eyJhbGciOi...",
"expires_at": "2026-01-20T16:30:00Z"
}
# Test the portal URL in your browser
open "https://portal.hookmesh.com/eyJhbGciOi..."Revoking Access
Portal tokens cannot be revoked before their expiration time since they're stateless JWTs. To revoke access:
- Wait for expiration: Tokens automatically expire after the configured duration (default 1 hour)
- Use short expiration: Set tokens to expire quickly (60 seconds for one-time use)
- Disable the application: If you need immediate revocation, disable the entire application
- Don't generate new tokens: Stop generating tokens for customers who shouldn't have access
// Generate a token that expires in 60 seconds
// Useful for one-time access links sent via email
async function generateOneTimePortalLink(applicationId) {
const result = await fetch(
`https://api.hookmesh.com/v1/applications/${applicationId}/portal-tokens`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
expires_in: 60, // 1 minute
permissions: ['read', 'write']
})
}
);
const data = await response.json();
return data.url;
}Error Handling
400 Bad Request
Returned when token generation parameters are invalid:
{
"error": {
"code": "invalid_request",
"message": "expires_in must be between 60 and 86400 seconds"
}
}404 Not Found
Returned when the application doesn't exist:
{
"error": {
"code": "not_found",
"message": "Application not found"
}
}429 Rate Limit
Portal token generation is rate limited to 100 requests per minute per organization:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Portal token generation rate limit exceeded. Retry after 60 seconds.",
"retry_after": 60
}
}Next Steps
- Learn how to embed the portal in your application using iframes
- Customize the portal appearance to match your brand
- Read the portal overview to understand all features
- API reference for applications to create customer applications