Magic Links
Magic links generate short-lived portal session tokens, ideal for:
- Embedding the hosted portal in an iframe without exposing long-lived tokens
- Generating one-click links in emails or dashboards
- Time-boxed portal access for support scenarios
How It Works
- Your backend calls the magic link endpoint with an application ID
- Hookbase returns a URL like
https://www.hookbase.app/portal/whpt_abc123...and the raw token - You redirect your customer to the URL or embed it in an iframe
- The token expires automatically (default: 60 minutes, max: 24 hours)
API Endpoint
POST /api/organizations/:orgId/portal/webhook-applications/:appId/magic-linkRequest Body
| Field | Type | Default | Description |
|---|---|---|---|
expiresInMinutes | number | 60 | Token lifetime (1–1440 minutes) |
scopes | string[] | ['read', 'write'] | Permissions: read, write, or both |
Response
{
"data": {
"url": "https://www.hookbase.app/portal/whpt_abc123...",
"token": "whpt_abc123...",
"expiresAt": "2026-03-14T15:30:00.000Z",
"expiresInMinutes": 60,
"scopes": ["read", "write"]
}
}Server-Side Examples
Node.js / Express
app.get('/api/portal-link', async (req, res) => {
const customer = await getCustomerFromSession(req);
const response = await fetch(
`https://api.hookbase.app/api/organizations/${ORG_ID}/portal/webhook-applications/${customer.appId}/magic-link`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
expiresInMinutes: 60,
scopes: ['read', 'write'],
}),
}
);
const { data } = await response.json();
res.json({ url: data.url });
});Python / Flask
@app.route('/api/portal-link')
def portal_link():
customer = get_customer_from_session()
response = requests.post(
f'https://api.hookbase.app/api/organizations/{ORG_ID}/portal/webhook-applications/{customer.app_id}/magic-link',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {API_KEY}',
},
json={
'expiresInMinutes': 60,
'scopes': ['read', 'write'],
},
)
data = response.json()['data']
return jsonify({'url': data['url']})Using the Token with the npm Package
If you prefer to render the portal in your own React app rather than using the hosted page, pass the magic link token directly to <HookbasePortal>:
function PortalPage() {
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
fetch('/api/portal-link')
.then(res => res.json())
.then(data => {
// Extract token from URL or use the raw token
setToken(data.token);
});
}, []);
if (!token) return <div>Loading...</div>;
return (
<HookbasePortal token={token}>
<EndpointList />
<MessageLog />
</HookbasePortal>
);
}Iframe Embedding
function PortalEmbed() {
const [portalUrl, setPortalUrl] = useState<string | null>(null);
useEffect(() => {
fetch('/api/portal-link')
.then(res => res.json())
.then(data => setPortalUrl(data.url));
}, []);
if (!portalUrl) return <div>Loading...</div>;
return (
<iframe
src={portalUrl}
width="100%"
height="600"
style=
title="Webhook Portal"
/>
);
}Security Best Practices
- Always generate magic links server-side. Never expose your API key to the browser.
- Use the shortest expiration that works. Default 60 minutes is good for most use cases.
- Scope tokens to
readwhen write access isn't needed. Read-only tokens prevent customers from creating or modifying endpoints. - Don't cache or reuse magic link tokens. Generate a fresh one each time the user opens the portal.
- Use HTTPS for iframe embeds. Mixed content will be blocked by browsers.
Long-Lived Tokens vs Magic Links
| Long-Lived Token | Magic Link | |
|---|---|---|
| Lifetime | 1–365 days | 1–1440 minutes |
| Use case | npm package with token stored in your backend | Hosted portal links, iframe embeds |
| Endpoint | POST .../tokens | POST .../magic-link |
| Returns URL | No (token only) | Yes (hookbase.app/portal/{token}) |
| Session token | No | Yes (excluded from token list) |