Answers your team can verify.
Everything you need to integrate Pernoex into your product. Three UI modes, an IDE bridge, and a server-side API — all answering from your indexed knowledge base, every response grounded in a source your users can open.
Floating mode
The default setup is a chat button in the corner of your page. Add this before the closing </body> tag:
<script
src="https://cdn.pernoex.com/widget.js"
data-api-key="YOUR_API_KEY"
></script>
For self-hosted instances, add data-api-url pointing at your endpoint.
Embedded mode
Put the chat inside a container on your page. This works well for help pages, sidebars and custom layouts. The container needs a defined height.
Pernoex.init({
apiKey: 'YOUR_API_KEY',
mode: 'embedded',
container: '#chat-container',
showHeader: true,
borderRadius: '12px'
});
| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | string | required | Your project's public API key |
| apiUrl | string | Pernoex API | Override the API base URL for a self-hosted endpoint |
| mode | enum | floating | floating · embedded · inline |
| position | enum | bottom-right | bottom-right · bottom-left |
| container | selector | none | Required for embedded mode |
| showHeader | boolean | true | Show or hide the header in embedded mode |
| showBranding | boolean | project setting | Override branding visibility for embedded mode |
| borderRadius | string | '0' | CSS radius for the container |
| showAvatars | boolean | true | Show user/bot avatars next to messages |
| theme | object | none | Color & style overrides (see Theme) |
| zIndex | number | 2147483000 | Base z-index for the widget's layer stack |
| startPills | string[] | none | Override the empty-state suggestion pills |
| launcherEntrance | enum | none | Optional one-shot launcher flourish: can · dice |
| launcherIconColor | string | auto | Optional hex color for the launcher ✻ and close icon |
launcherEntrance when you want the launcher to can-drop or dice-toss into place on a campaign page. Script tags can use data-launcher-entrance. Use launcherIconColor only when the launcher icons should override the automatic contrast color.apiKey: 'YOUR_API_KEY',
launcherEntrance: 'dice', // or 'can'
launcherIconColor: '#f7c948'
});
Inline Explainer
Let users highlight text and ask a follow-up question. Pernoex opens a sidebar, answers from your knowledge base and keeps the original text visible. Turn it on in the dashboard or control it with selectors in code.
apiKey: 'YOUR_API_KEY',
inline: {
enabled: true,
triggerSelector: 'article, .docs-content',
excludeSelector: 'nav, .code-block',
maxWidth: 420,
theme: 'auto'
}
});
| Option | Type | Default | Description |
|---|---|---|---|
| enabled | boolean | false | Override the dashboard setting |
| triggerSelector | string | none | Limit where text selection triggers the explainer |
| excludeSelector | string | none | Areas to exclude from selection |
| maxWidth | number | 400 | Maximum explainer sidebar width in pixels |
| theme | enum | auto | light · dark · auto |
Suggestion pills
On the empty state, the widget can show three suggested questions based on the current page. A visitor on /pricing can see pricing questions. Pass startPills to set your own questions. A non-empty array is used verbatim, up to three items are shown, and the widget skips its page-context /chat/suggestions request. It does not change how the eventual answer is generated from your indexed knowledge base.
MCP setup (IDE)Growth+
Connect your docs to AI code editors with the Model Context Protocol. Pernoex works with VS Code, Cursor, Claude Code, JetBrains and Windsurf. Generate a developer key (dk_live_) from Channels → API & MCP in your project. The default limit is 200 requests/day per IP, and a project admin can change it there.
{
"servers": {
"pernoex": {
"url": "https://mcp.pernoex.com/mcp",
"headers": { "Authorization": "Bearer dk_live_…" }
}
}
}
Each IDE has its own config file. The server URL and Bearer header stay the same.
| IDE | Config location | Transport |
|---|---|---|
| VS Code | .vscode/mcp.json | HTTP /mcp |
| Cursor | .cursor/mcp.json | HTTP /mcp |
| Claude Code | claude mcp add … | HTTP /mcp |
| JetBrains | Settings → MCP Servers | HTTP /mcp |
| Windsurf | ~/.codeium/windsurf/… | HTTP /mcp |
Sessions have a 30-minute idle timeout and reconnect automatically; each developer key supports up to 10 concurrent sessions.
MCP tools
| Tool | Key | Arguments | Returns |
|---|---|---|---|
| pernoex_ask | dk_live_ / sk_live_ | question · optional context | A written answer from your indexed docs |
| pernoex_search | sk_live_ | question · optional context, code_only | Ranked source passages with relevance scores, without an AI-written answer |
Theme customization
Pass a theme object with any combination of color overrides — all optional. Set background: 'transparent' for seamless embedding, or fontFamily: 'inherit'to use your page's font.
background: '#1a1a2e',
assistantBubbleBg: '#16213e',
userBubbleBg: '#0f172a',
linkColor: '#818cf8',
fontFamily: '"Inter", sans-serif'
}
| Property | Applies to |
|---|---|
| background | Messages area & window — use 'transparent' to blend in |
| assistantBubbleBg / TextColor | Assistant message bubble and text |
| userBubbleBg / TextColor | User message bubble and text |
| linkColor | Links inside messages |
| inputBg / inputBorderColor | Input field background and border |
| sendButtonBg / Color | Send button background and icon |
| fontFamily | Global font — pass 'inherit' for your page's stack |
JavaScript API
After init(), control methods are on the global Pernoex object, and lifecycle callbacks can be passed to init.
Pernoex.isOpen() · Pernoex.clearMessages()
await Pernoex.explain("text to explain")
// callbacks
{ onReady, onOpen, onClose, onMessage, onError }
- ✻Copy for AI Agent — formats the full thread (question, answer, sources) for pasting into Cursor, VS Code or Claude Code.
- ✻Per-message & code-block copy — copy any single response, or just a code block without surrounding markdown.
- ✻Image lightbox — images in source content open full-screen on click.
- ✻Host font inheritance — set
fontFamily: 'inherit'to match your page's type.
SSE events
Streamed responses connect over Server-Sent Events. For custom integrations, these are the event types you'll receive from /api/v1/chat/stream:
| Event | Payload | Description |
|---|---|---|
| session_context | object | Session metadata including conversation_id |
| tool / tool_complete | object | Custom action invoked, and its result |
| text | string | Streamed answer chunk — append to the response |
| sources | array | Source citations, each with title, URL and score |
| done | — | Stream complete — close the connection |
REST APIScale
Query your knowledge base server-side with a server key (srv_live_) created in Channels → API & MCP. Never expose server keys in client-side code. The production route is limited to 60 requests/minute per key.
curl -X POST https://api.pernoex.com/v1/query \
-H "Authorization: Bearer srv_live_YOUR_SERVER_KEY" \
-H "Content-Type: application/json" \
-d '{ "message": "How do I set up webhooks?" }'
A successful response contains conversation_id, answer, tools_used, andsources[]. Each source carries document_id, document_title, source_url, and cited_at. Render those citations with the answer; no confidence score is defined.
Custom ActionsGrowth+
Let the AI call your APIs during a conversation. It can check an order, reset a password or look up an account across chat, Slack and voice. Auth headers are stored server-side and never exposed; every call is logged in your audit trail. Use confirmation_required: true for write operations. Growth: up to 5 per project. Scale: up to 25.
"name": "check_order_status",
"endpoint": "https://api.yourapp.com/orders/lookup",
"method": "POST",
"parameters": [{ "name": "order_id", "required": true }],
"confirmation_required": false
}
Slack
Add Pernoex to your workspace so your team or customers can tag @Pernoex in any thread and get answers from your docs, with clickable source links. The bot reads full thread context, including images, and supports DMs. On Scale, use your own Slack app with a custom name, avatar and description.
Framework examples
Load the script and call init() on mount. Clean up with Pernoex.destroy() on unmount.
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://cdn.pernoex.com/widget.js';
script.setAttribute('data-manual-init', 'true');
script.onload = () => window.Pernoex.init({
apiKey,
mode: 'embedded',
container: containerRef.current
});
document.body.appendChild(script);
return () => {
window.Pernoex?.destroy();
script.remove();
};
}, []);
'use client';
export default function ChatWidget() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://cdn.pernoex.com/widget.js';
script.dataset.manualInit = 'true';
script.onload = () => window.Pernoex.init({
apiKey: process.env.NEXT_PUBLIC_PERNOEX_KEY,
mode: 'embedded',
container: containerRef.current
});
document.body.appendChild(script);
return () => { window.Pernoex?.destroy(); script.remove(); };
}, []);
return <div ref={containerRef} style={{ height: '600px' }} />;
}
<template>
<div ref="chatContainer" style="height: 600px"></div>
</template>
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue';
const chatContainer = ref(null);
onMounted(() => {
const script = document.createElement('script');
script.src = 'https://cdn.pernoex.com/widget.js';
script.dataset.manualInit = 'true';
script.onload = () => window.Pernoex.init({
apiKey: 'YOUR_API_KEY',
mode: 'embedded',
container: chatContainer.value
});
document.body.appendChild(script);
});
onBeforeUnmount(() => window.Pernoex?.destroy());
</script>
Signed user contextGrowth+
Let your backend identify the signed-in user without exposing app sessions, access tokens or private keys. Sign the short-lived context on your server with HMAC-SHA256 and pass the resulting { version, payload, signature } object to the widget. The identity secret must never appear in client-side code.
const now = Math.floor(Date.now() / 1000);
const payload = Buffer.from(JSON.stringify({
version: 'v1',
project_key: process.env.PERNOEX_PROJECT_KEY,
external_user_id: user.id,
external_account_id: user.accountId,
email: user.email,
name: user.name,
iat: now,
exp: now + 600,
nonce: crypto.randomUUID()
})).toString('base64url');
const signature = crypto.createHmac('sha256', process.env.PERNOEX_IDENTITY_SECRET)
.update('pernoex-user-context.v1.' + payload)
.digest('base64url');
return { version: 'v1', payload, signature };
Cookie-based apps can give the widget same-origin session and grant endpoints. Those are routes on your app, not Pernoex API routes. Token-based apps should use sessionProvider and grantProvider functions instead.
credentials: 'include',
cache: 'no-store'
}).then(response => response.json());
Pernoex.init({
apiKey: 'pk_live_…',
userContext: session.user_context,
actionAuth: {
sessionEndpoint: '/api/pernoex/session',
grantEndpoint: '/api/pernoex/action-grants'
}
});
Webhook signingScale
Event webhooks cover conversation creation/messages, escalations and handoff replies, detected gaps, completed crawls, and channel message/health events. Copy the endpoint secret when the webhook is created; it is shown once. Each request carries an X-Webhook-Signature header containing the hex HMAC-SHA256 of the raw request body. Verify the raw bytes before parsing JSON and compare signatures in constant time.
function verifyWebhook(rawBody, signature, secret) {
if (!signature || !secret) return false;
const expected = crypto.createHmac('sha256', secret)
.update(rawBody)
.digest();
const received = Buffer.from(signature, 'hex');
return received.length === expected.length
&& crypto.timingSafeEqual(received, expected);
}
const valid = verifyWebhook(
req.body,
req.get('X-Webhook-Signature'),
process.env.PERNOEX_WEBHOOK_SECRET
);
CSP & domain restrictions
The widget renders inside a Shadow DOMboundary — your CSS can't affect it and its styles can't leak into your page. Styles inject via adoptedStyleSheets, so style-src 'unsafe-inline' is not required. You can also choose which domains may load the widget from your project settings. Once an allowlist exists, unlisted origins are rejected.
script-src 'self' https://cdn.pernoex.com;
connect-src 'self' https://api.pernoex.com;
style-src 'self'; font-src 'self' data: https://fonts.gstatic.com;
Troubleshooting
| Symptom | Fix |
|---|---|
| origin_not_allowed | Add the domain (or localhost:PORT) to your allowed domains list |
| Widget does not appear | Confirm the public key, script request and allowed-domain state in the browser console and Network panel |
| Embedded container empty | Set an explicit height; confirm the element exists before init() |
| Widget behind another overlay | Set the zIndex base high enough for the host page's layer stack |
| Inline Explainer absent | Confirm it is enabled and that triggerSelector matches a selectable element not excluded by excludeSelector |
| 401 on MCP | Check the dk_live_ key and the Bearer header format |
| 429 Too Many Requests | The default is 200 requests/day per IP; a project admin can change the daily limit in API & MCP |
| Generic explanations | Ensure the knowledge base is indexed and has sufficient credit balance |