Developers / Integration guide

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.

Three UI modes: Floating (a chat bubble), Embedded (always-visible, in your layout), and Inline Explainer (highlight text, “Ask AI”). For IDE setup, see MCP setup.

Floating mode

The default setup is a chat button in the corner of your page. Add this before the closing </body> tag:

<!-- before </body> -->
<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.

<div id="chat-container" style="height: 600px;"></div>
Pernoex.init({
  apiKey: 'YOUR_API_KEY',
  mode: 'embedded',
  container: '#chat-container',
  showHeader: true,
  borderRadius: '12px'
});
Configuration options
OptionTypeDefaultDescription
apiKeystringrequiredYour project's public API key
apiUrlstringPernoex APIOverride the API base URL for a self-hosted endpoint
modeenumfloatingfloating · embedded · inline
positionenumbottom-rightbottom-right · bottom-left
containerselectornoneRequired for embedded mode
showHeaderbooleantrueShow or hide the header in embedded mode
showBrandingbooleanproject settingOverride branding visibility for embedded mode
borderRadiusstring'0'CSS radius for the container
showAvatarsbooleantrueShow user/bot avatars next to messages
themeobjectnoneColor & style overrides (see Theme)
zIndexnumber2147483000Base z-index for the widget's layer stack
startPillsstring[]noneOverride the empty-state suggestion pills
launcherEntranceenumnoneOptional one-shot launcher flourish: can · dice
launcherIconColorstringautoOptional hex color for the launcher ✻ and close icon
Hidden flourish: pass 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.
Pernoex.init({
  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.

Pernoex.init({
  apiKey: 'YOUR_API_KEY',
  inline: {
    enabled: true,
    triggerSelector: 'article, .docs-content',
    excludeSelector: 'nav, .code-block',
    maxWidth: 420,
    theme: 'auto'
  }
});
OptionTypeDefaultDescription
enabledbooleanfalseOverride the dashboard setting
triggerSelectorstringnoneLimit where text selection triggers the explainer
excludeSelectorstringnoneAreas to exclude from selection
maxWidthnumber400Maximum explainer sidebar width in pixels
themeenumautolight · 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.

// .vscode/mcp.json — or .cursor/mcp.json
{
  "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.

IDEConfig locationTransport
VS Code.vscode/mcp.jsonHTTP /mcp
Cursor.cursor/mcp.jsonHTTP /mcp
Claude Codeclaude mcp add …HTTP /mcp
JetBrainsSettings → MCP ServersHTTP /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

ToolKeyArgumentsReturns
pernoex_askdk_live_ / sk_live_question · optional contextA written answer from your indexed docs
pernoex_searchsk_live_question · optional context, code_onlyRanked 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.

theme: {
  background: '#1a1a2e',
  assistantBubbleBg: '#16213e',
  userBubbleBg: '#0f172a',
  linkColor: '#818cf8',
  fontFamily: '"Inter", sans-serif'
}
PropertyApplies to
backgroundMessages area & window — use 'transparent' to blend in
assistantBubbleBg / TextColorAssistant message bubble and text
userBubbleBg / TextColorUser message bubble and text
linkColorLinks inside messages
inputBg / inputBorderColorInput field background and border
sendButtonBg / ColorSend button background and icon
fontFamilyGlobal 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.open() · Pernoex.close() · Pernoex.toggle()
Pernoex.isOpen() · Pernoex.clearMessages()
await Pernoex.explain("text to explain")

// callbacks
{ onReady, onOpen, onClose, onMessage, onError }
Widget features
  • 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:

EventPayloadDescription
session_contextobjectSession metadata including conversation_id
tool / tool_completeobjectCustom action invoked, and its result
textstringStreamed answer chunk — append to the response
sourcesarraySource citations, each with title, URL and score
doneStream 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.

# ask a question with a server API 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.

// React
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();
  };
}, []);

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.

// server-side (Node) — return with Cache-Control: no-store
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 };
Browser bridge

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.

const session = await fetch('/api/pernoex/session', {
  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'
  }
});
Signed identity does not grant authority by itself. For delegated actions, your backend must check the user, account, action and resource before issuing a short-lived, one-time grant; the final proxy must verify and consume that grant before running business logic.

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.

// req.body must be the unparsed raw request body
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.

# Content Security Policy
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;
With no allowed domains configured, the public widget key accepts any origin, including localhost. As soon as one domain is added, enforcement becomes strict. Add your localhost host or port explicitly while developing, then remove it before production if it should not remain allowed.

Troubleshooting

SymptomFix
origin_not_allowedAdd the domain (or localhost:PORT) to your allowed domains list
Widget does not appearConfirm the public key, script request and allowed-domain state in the browser console and Network panel
Embedded container emptySet an explicit height; confirm the element exists before init()
Widget behind another overlaySet the zIndex base high enough for the host page's layer stack
Inline Explainer absentConfirm it is enabled and that triggerSelector matches a selectable element not excluded by excludeSelector
401 on MCPCheck the dk_live_ key and the Bearer header format
429 Too Many RequestsThe default is 200 requests/day per IP; a project admin can change the daily limit in API & MCP
Generic explanationsEnsure the knowledge base is indexed and has sufficient credit balance