Integration Guide
Everything you need to integrate the Pernoex cognition layer into your product
Pernoex offers three UI integration modes: Floating for a chat bubble experience,Embedded for custom layouts where you want the intelligence always visible, and Inline Explainer for contextual "Ask AI" functionality on text selection that opens an AI sidebar with full conversational support. For IDE integration, see MCP Setup.
Floating Mode
The default integration. A floating interface appears in the corner of your page that users can click to open and close.
Basic Setup
Add this before the closing </body> tag:
<script src="https://cdn.pernoex.com/widget.js"data-api-key="YOUR_API_KEY"></script>
Custom API URL
For self-hosted instances:
<script src="https://cdn.pernoex.com/widget.js"data-api-key="YOUR_API_KEY"data-api-url="https://your-api.example.com/api/v1"></script>
Live Preview
Click the chat bubble to preview
Embedded Mode
Render the intelligence inside a specific container. Perfect for dedicated help pages, sidebars, or custom layouts.
height (or flex/grid sizing). The widget will automatically set position: relative on it if needed. If the container has zero height, a console warning will help you diagnose the issue.Setup
<div id="chat-container" style="height: 600px;"></div><script src="https://cdn.pernoex.com/widget.js"data-manual-init="true"></script><script>Pernoex.init({apiKey: 'YOUR_API_KEY',mode: 'embedded',container: '#chat-container',showHeader: true,borderRadius: '12px'});</script>
Live Preview
Embedded mode - always visible
Configuration Options
Inline Explainer Mode
Add contextual AI explanations to any text in your product. Users can highlight text and click "Ask AI" to get explanations grounded in your knowledge base in a sidebar that pushes page content, keeping the reading context visible.
Basic Setup
<!-- Inline explainer is controlled from your dashboard settings --><!-- Once enabled, it works automatically with text selection --><script src="https://cdn.pernoex.com/widget.js"data-api-key="YOUR_API_KEY"></script>
How It Works
Our RAG pipeline ensures accurate responses grounded in your documentation.
Click "RAG pipeline" above to preview
Advanced Configuration
// For programmatic control, you can override dashboard settingsPernoex.init({apiKey: 'YOUR_API_KEY',inline: {enabled: true, // Override dashboard settingtriggerSelector: 'article, .docs-content', // Only in these areasexcludeSelector: 'nav, .code-block', // Not in these areasposition: 'auto', // 'auto' | 'above' | 'below'showFollowUp: true, // Allow follow-up questionstheme: 'auto' // Match system theme}});
Configuration Options
Combined with Floating Integration
The inline explainer works alongside your main floating integration automatically. When enabled from the dashboard, both the floating interface and text selection explainer are available to your users:
// Inline explainer works alongside the floating widget automatically// Just enable it in your dashboard settings, or override via init:Pernoex.init({apiKey: 'YOUR_API_KEY',inline: {enabled: true // Works alongside the floating chat widget}});
MCP Setup (IDE Integration)
Connect your documentation directly to AI-powered IDEs using the Model Context Protocol (MCP). Developers on your team can query your docs from VS Code, Cursor, Claude Code, JetBrains, and any other MCP-compatible client — without leaving their editor.
Prerequisites
- A Pernoex account on the Growth plan or higher
- At least one project with crawled documentation
- A developer key (
dk_live_) generated from your dashboard
Step 1: Get Your Developer Key
Open your project in the Pernoex dashboard, go to the Surface page, and find the Developer IDE Access section. Enable it and copy your developer key. The key starts with dk_live_ and is used to authenticate MCP connections.
You can regenerate the key at any time — the old key stops working immediately. The default rate limit is 200 requests per day per IP address.
Step 2: Configure Your IDE
Choose your IDE and add the configuration below. Replace dk_live_YOUR_KEY_HERE with your actual developer key.
VS Code
Create a .vscode/mcp.json file in your project root:
// .vscode/mcp.json{"servers": {"pernoex": {"url": "https://mcp.pernoex.com/mcp","headers": {"Authorization": "Bearer dk_live_YOUR_KEY_HERE"}}}}
Cursor
Create .cursor/mcp.json in your project root (or ~/.cursor/mcp.json for global access):
// .cursor/mcp.json (project) or ~/.cursor/mcp.json (global){"mcpServers": {"pernoex": {"url": "https://mcp.pernoex.com/mcp","headers": {"Authorization": "Bearer dk_live_YOUR_KEY_HERE"}}}}
Claude Code
Run this command in your terminal:
claude mcp add pernoex "https://mcp.pernoex.com/mcp" \--header "Authorization: Bearer dk_live_YOUR_KEY_HERE"
JetBrains IDEs
Go to Settings → Tools → MCP Servers and add:
// Settings → Tools → MCP Servers{"mcpServers": {"pernoex": {"url": "https://mcp.pernoex.com/mcp","headers": {"Authorization": "Bearer dk_live_YOUR_KEY_HERE"}}}}
Windsurf
Edit ~/.codeium/windsurf/mcp_config.json. Windsurf uses the SSE transport:
// ~/.codeium/windsurf/mcp_config.json{"mcpServers": {"pernoex": {"serverUrl": "https://mcp.pernoex.com/sse","headers": {"Authorization": "Bearer dk_live_YOUR_KEY_HERE"}}}}
Available Tools
Once connected, your IDE will have access to the following MCP tools:
pernoex_askdk_live_ keyYour AI agent asks a natural-language question and receives a fully written answer sourced from your indexed documentation. Behind the scenes Pernoex finds the most relevant pages, ranks them, and synthesizes a single coherent response — exactly like asking a teammate who has read every page of your docs.
question required— The question to answercontext optional— Additional context to refine the answer{"name": "pernoex_ask","arguments": {"question": "How do I configure SSO?","context": "Setting up authentication for enterprise customers"}}
pernoex_searchsk_live_ key onlyReturns the most relevant pages and passages from your documentation along with relevance scores — without generating an AI answer. Ideal when your agent needs to inspect the source material directly or when you want full control over how results are presented.
question required— The search querycontext optional— Additional context to improve relevancecode_only optional— Return only code snippets{"name": "pernoex_search","arguments": {"question": "SSO configuration","code_only": false}}
How It Works
Pernoex uses the MCP Streamable HTTP transport (protocol version 2025-03-26). When your IDE connects, it establishes a session and can call tools to query your documentation. The AI agent in your IDE uses these tools automatically when it needs information from your docs.
Sessions have a 30-minute idle timeout. If the connection drops, the IDE will automatically reconnect. Each developer key supports up to 10 concurrent sessions.
Troubleshooting MCP
Connection Refused or 401 Unauthorized
- Check that your
dk_live_key is correct and hasn't been regenerated - Ensure the
Authorizationheader format isBearer dk_live_... - Verify MCP is enabled in your project's Surface page
429 Too Many Requests
- Developer keys are limited to 200 requests per day per IP address
- Wait until the next day, or contact support to increase your limit
Tools Not Appearing in IDE
- Restart your IDE after saving the MCP config file
- Ensure the JSON is valid (no trailing commas)
- Verify the URL ends with
/mcp
Token Cap Reached
- Your project's monthly MCP token cap has been exceeded
- Adjust it in Project Settings → MCP Usage Cap
MCP Not Available
- MCP requires a Growth plan or higher
- Check your subscription tier in the dashboard
Theme Customization
Fully customize the widget's visual appearance to match your brand. Pass a theme object with any combination of color overrides. All properties are optional — only specify what you want to change.
Example: Transparent Background (No Avatars)
Pernoex.init({apiKey: 'YOUR_API_KEY',showAvatars: false,theme: {background: 'transparent',assistantBubbleBg: '#f0f1f3',assistantTextColor: '#1a1d23',userBubbleBg: '#0f172a',userTextColor: '#ffffff',linkColor: '#0f172a',inputBg: '#ffffff',inputBorderColor: '#e5e7eb',inputTextColor: '#1a1d23',inputPlaceholderColor: '#9ca3af',sendButtonBg: '#0f172a',sendButtonColor: '#ffffff',fontFamily: '"Inter", sans-serif'}});
Example: Dark Theme
Pernoex.init({apiKey: 'YOUR_API_KEY',theme: {background: '#1a1a2e',assistantBubbleBg: '#16213e',assistantTextColor: '#e2e8f0',userBubbleBg: '#0f172a',userTextColor: '#ffffff',linkColor: '#818cf8',inputBg: '#16213e',inputBorderColor: '#2a2a4a',inputTextColor: '#e2e8f0',inputPlaceholderColor: '#64748b',sendButtonBg: '#0f172a',sendButtonColor: '#ffffff'}});
Theme Properties
Common Use Cases
Full-Page Intelligence
Create a dedicated support or chat page:
<!DOCTYPE html><html><head><style>body, html { margin: 0; height: 100%; }#chat-container { height: 100vh; width: 100%; }</style></head><body><div id="chat-container"></div><script src="https://cdn.pernoex.com/widget.js"data-manual-init="true"></script><script>Pernoex.init({apiKey: 'YOUR_API_KEY',mode: 'embedded',container: '#chat-container'});</script></body></html>
Embedded mode - always visible
Sidebar Integration
Add a persistent intelligence sidebar to your application:
<div style="display: flex; height: 100vh;"><main style="flex: 1;"><!-- Your main content --></main><aside id="chat-sidebar"style="width: 400px; border-left: 1px solid #e5e7eb;"></aside></div><script src="https://cdn.pernoex.com/widget.js"data-manual-init="true"></script><script>Pernoex.init({apiKey: 'YOUR_API_KEY',mode: 'embedded',container: '#chat-sidebar',showHeader: true});</script>
Sidebar integration layout
JavaScript API Reference
Control Methods
Available for floating mode. All methods are accessible on the global Pernoex object after init():
// Open the chat windowPernoex.open()// Close the chat windowPernoex.close()// Toggle open/closed statePernoex.toggle()// Check if widget is currently openconst isOpen = Pernoex.isOpen()// Clear all messages and start a new conversationPernoex.clearMessages()// Get explanation for text programmaticallyconst explanation = await Pernoex.explain("text to explain")// Destroy the widget and clean upPernoex.destroy()
Event Callbacks
Listen to integration lifecycle events:
Pernoex.init({apiKey: 'YOUR_API_KEY',onReady: () => {console.log('Widget is ready');},onOpen: () => {console.log('Widget opened');},onClose: () => {console.log('Widget closed');},onMessage: (message) => {console.log('New message:', message);},onError: (error) => {console.error('Widget error:', error);},onExplain: (text, explanation) => {console.log('Explained:', text, explanation);},onExplainError: (error) => {console.error('Explain error:', error);}});
Widget Features
Every conversation includes a "Copy for AI Agent" button that formats the full thread (question, answer, and sources) for pasting into Cursor, VS Code, or Claude Code. The format is optimized for AI agents to continue the conversation.
Each assistant message has a copy icon that copies just that response to the clipboard, including any source citations.
Code blocks in responses include a dedicated copy button. The raw code is copied without any surrounding markdown formatting.
Images embedded in source content are clickable. Clicking opens a full-screen lightbox overlay for better viewing.
Set fontFamily in the theme config to override the widget's default font. Pass 'inherit' to use your page's font stack.
SSE Event Types
When streaming responses, the widget connects to the chat endpoint via Server-Sent Events. If you're building a custom integration, these are the event types you'll receive:
// SSE event types received during a streamed response// Connect to: /api/v1/chat/streamevent: session_context // Session metadata (conversation_id, etc.)event: thinking // AI is reasoning (optional, may be empty)event: tool // Custom action invoked (name, parameters)event: tool_complete // Custom action result returnedevent: text // Streamed answer text chunkevent: sources // Source citations arrayevent: confidence // Confidence score (0-1)event: actions // Suggested follow-up actionsevent: done // Stream complete
REST API
The REST API lets you query your knowledge base server-side. Requires a Scale plan and a secret key (sk_live_) generated from your dashboard. Never expose secret keys in client-side code.
Query
Ask a question and receive a generated answer with source citations:
# Query your knowledge base (Scale plan required)curl -X POST https://api.pernoex.com/v1/query \-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \-H "Content-Type: application/json" \-d '{"query": "How do I set up webhooks?","conversation_id": "optional-for-follow-ups"}'
Search
Search your indexed documents without generating an AI answer:
# Search documents without generating an answercurl -X POST https://api.pernoex.com/v1/search \-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \-H "Content-Type: application/json" \-d '{"query": "webhook configuration","code_only": false,"limit": 5}'
Rate Limits
- Scale plan: 1,000 requests per minute
- Rate limit headers (
X-RateLimit-Remaining,X-RateLimit-Reset) are included in every response - Exceeding the limit returns
429 Too Many Requests
Custom Actions
Custom Actions let your AI call external APIs mid-conversation — check order status, reset passwords, look up accounts, and more. Actions work across all surfaces: chat widget, Slack, and voice.
Defining an Action
Create actions in your dashboard under Settings → Custom Actions, or define them via the API:
// Define an action in your dashboard (Settings → Custom Actions)// Or via the API:{"name": "check_order_status","description": "Look up the current status of a customer order","endpoint": "https://api.yourapp.com/orders/lookup","method": "POST","headers": {"Authorization": "Bearer YOUR_INTERNAL_API_KEY"},"parameters": [{"name": "order_id","type": "string","description": "The order ID to look up","required": true}],"confirmation_required": false}
How It Works
- A user asks a question that requires an external lookup (e.g., "What's my order status?")
- The AI identifies the relevant action and extracts parameters from the conversation
- If
confirmation_requiredistrue, the user sees a confirmation prompt before the action executes - Pernoex calls your endpoint with the extracted parameters and authentication headers
- The result is incorporated into the AI's response
Security
- Authentication headers are stored server-side and never exposed to the browser
- All action invocations are logged in your project's audit trail
- Use
confirmation_required: truefor any write operations
Plan Limits
- Growth: up to 5 custom actions per project
- Scale: up to 25 custom actions per project
Slack Integration
Add Pernoex to your Slack workspace so your team (or customers) can tag @Pernoex in any thread and get answers grounded in your documentation.
Setup
- Go to your project's Surface → Slack page in the dashboard
- Click Add to Slack and authorize the bot in your workspace
- Invite
@Pernoexto any channel where you want it available
Usage
- Mention: Tag
@Pernoexwith a question in any channel or thread - Thread context: The bot reads the full thread history, including images, to provide context-aware answers
- DMs: Message the bot directly for private queries
- Source citations: Responses include clickable links back to your documentation
Custom Slack Bot (Scale)
On the Scale plan, you can use your own Slack app instead of the shared Pernoex bot. This lets you set a custom bot name, avatar, and description that matches your brand. Configure it in Surface → Slack → Custom Bot.
Permissions Required
app_mentions:read— detect when the bot is taggedchat:write— post replieschannels:history/groups:history— read thread contextfiles:read— read images shared in threadsim:history— support direct messages
Framework Examples
import { useEffect, useRef } from 'react';function ChatWidget() {const containerRef = useRef(null);useEffect(() => {const script = document.createElement('script');script.src = 'https://cdn.pernoex.com/widget.js';script.setAttribute('data-manual-init', 'true');document.body.appendChild(script);script.onload = () => {window.Pernoex.init({apiKey: 'YOUR_API_KEY',mode: 'embedded',container: containerRef.current,showHeader: true});};return () => {window.Pernoex?.destroy();document.body.removeChild(script);};}, []);return <div ref={containerRef} style={{ height: '600px' }} />;}
Identity Verification (HMAC)
Identity verification ensures that the user interacting with your widget is who your server says they are. When enabled, Pernoex validates a server-generated HMAC signature before allowing identified sessions. Available on Growth and Scale plans.
1. Generate Your Secret
Go to your project's Settings → Identity Verification page and click Generate Secret. Copy the secret — it will only be shown once. Store it securely in your environment variables.
2. Compute the HMAC (Server-Side)
On your server, compute the HMAC using the user's details:
const crypto = require('crypto');function generatePernoexHmac(secret, userId, email, name) {const payload = [userId, email || '', name || ''].join(':');return crypto.createHmac('sha256', secret).update(payload).digest('hex');}// Usageconst hmac = generatePernoexHmac(process.env.PERNOEX_IDENTITY_SECRET,'usr_123','Jane');
import hmac, hashlibdef generate_pernoex_hmac(secret: str, user_id: str, email: str = '', name: str = '') -> str:payload = f"{user_id}:{email}:{name}"return hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()# Usagesig = generate_pernoex_hmac(os.environ["PERNOEX_IDENTITY_SECRET"],"usr_123","Jane")
package mainimport ("crypto/hmac""crypto/sha256""encoding/hex""fmt""strings")func generatePernoexHmac(secret, userID, email, name string) string {payload := strings.Join([]string{userID, email, name}, ":")mac := hmac.New(sha256.New, []byte(secret))mac.Write([]byte(payload))return hex.EncodeToString(mac.Sum(nil))}
3. Pass Identity to the Widget
Send the computed HMAC to the widget via the userIdentity option:
Pernoex.init({apiKey: 'YOUR_API_KEY',userIdentity: {user_id: 'usr_123',email: '[email protected]', // optionalname: 'Jane', // optionalhmac: '<computed-server-side>'}});
Secret Rotation
You can rotate the secret at any time from Settings → Identity Verification → Rotate. The old secret is immediately invalidated. Update your server-side code before rotating.
Webhook Signing
When you configure event webhooks (Scale plan), Pernoex sends notifications to your server when events occur — conversations completed, handoff triggered, content gaps detected, and more. These payloads are signed so you can verify they originated from Pernoex.
Signature Format
Every webhook request includes a X-Pernoex-Signature header in the format:
t=1714500000,v1=5d41402abc4b2a76b9719d911017c592
t is the Unix timestamp when the webhook was sent. v1 is the HMAC-SHA256 signature of timestamp.payload.
Verification
const crypto = require('crypto');function verifyWebhookSignature(payload, signatureHeader, secret) {const [tPart, v1Part] = signatureHeader.split(',');const timestamp = tPart.replace('t=', '');const signature = v1Part.replace('v1=', '');const signedPayload = timestamp + '.' + payload;const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');return crypto.timingSafeEqual(Buffer.from(signature, 'hex'),Buffer.from(expected, 'hex'));}
import hmac, hashlib, timedef verify_webhook_signature(payload: bytes, signature_header: str, secret: str) -> bool:parts = dict(p.split("=", 1) for p in signature_header.split(","))timestamp = parts["t"]signature = parts["v1"]signed_payload = f"{timestamp}.".encode() + payloadexpected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()return hmac.compare_digest(signature, expected)
Generating the Secret
Go to Settings → Webhook Signing Secret → Generate. The webhook signing secret is separate from the identity verification secret.
Security & Best Practices
- Your page's CSS cannot affect the widget, and widget styles cannot leak into your page
- Host-page scripts cannot access widget DOM internals (e.g. conversation messages)
- Styles are injected via
adoptedStyleSheets— nostyle-src 'unsafe-inline'CSP directive needed - Fonts are loaded via
@font-faceinside the shadow root — no<link>tags are injected into your<head>
Domain Restrictions
Domain restrictions control which sites are allowed to load your widget. The behavior depends on whether you have configured any allowed domains yet:
- No domains configured (default): The API key is open — any origin is accepted, including
localhost. This lets you develop and test locally without any extra setup, and allows third-party integrators to get started immediately. - At least one domain configured: Enforcement becomes strict. Only the exact domains you have listed are accepted.
localhostis not automatically allowed once you lock down your project.
localhost or your specific local port (e.g. localhost:3000) to your project's allowed domains list. Remove it before going to production.Content Security Policy
If you use CSP headers, allow the following. Note that style-src 'unsafe-inline' is not required — the widget uses Shadow DOM with adoptedStyleSheets for CSP-compliant style injection.
script-src 'self' https://cdn.pernoex.com;connect-src 'self' https://api.pernoex.com;style-src 'self';img-src 'self' https:;font-src 'self' data: https://fonts.gstatic.com;frame-src 'none';object-src 'none';base-uri 'self';
Troubleshooting
Widget Blocked After Adding Domain Restrictions
- Once you add at least one allowed domain,
localhostis no longer automatically permitted - Add
localhostorlocalhost:PORT(e.g.localhost:3000) to your allowed domains list for local development - Remove
localhostfrom your allowed domains before deploying to production if you want to restrict access to your live domains only - The browser console error will show
origin_not_allowedwhen this occurs
Integration Not Appearing
- Verify your API key is correct
- If you have configured allowed domains, check that the current domain is in the list
- If no domains are configured yet, the widget accepts all origins automatically
- Open browser console for errors
- Ensure the script tag is loaded (check the Network tab)
Embedded Mode Container Empty
- Confirm container element exists before calling
Pernoex.init() - Set an explicit height on the container element
- Check console for initialization errors
- Verify
data-manual-init="true"is set on the script tag
Styling Issues
- The widget renders inside a Shadow DOM boundary, so host page styles cannot affect it and widget styles cannot leak into your page
- Ensure the container has defined dimensions for embedded mode
- If the widget appears behind other elements, use the
zIndexoption inPernoex.init()to adjust the stacking order
Inline Explainer Not Appearing
- Verify inline explainer is enabled in your project's Settings page on the dashboard
- Check
triggerSelectormatches elements on your page - Ensure selection is at least 3 characters
- Check console for JavaScript errors
- Verify the selected area is not excluded by
excludeSelector
Explanations Are Generic
- Ensure your knowledge base is indexed (check Crawl status in dashboard)
- The selected text should relate to content in your indexed pages
- Try selecting more specific terms from your documentation
- Check that your project has sufficient credit balance
Sidebar Not Pushing Content
- The sidebar pushes page content to the left when it opens
- Ensure your page layout can accommodate the sidebar width
- Check for fixed positioning elements that may not respond to the push
Need Help?
Have questions about integration? Reach out to our support team at [email protected] or explore more examples in your dashboard.