Pernoex
FeaturesPricingDocs
Sign InGet Started
FeaturesPricingDocs
Sign InGet Started

Getting Started

  • Floating Mode
  • Embedded Mode
  • Inline Explainer Mode

IDE Integration

  • MCP Setup

Advanced

  • Theme Customization
  • Common Use Cases
  • JavaScript API
  • SSE Events
  • REST API
  • Custom Actions
  • Slack Integration
  • Framework Examples

Resources

  • Identity Verification
  • Webhook Signing
  • Security
  • Troubleshooting

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:

html
<script src="https://cdn.pernoex.com/widget.js"
data-api-key="YOUR_API_KEY"></script>

Custom API URL

For self-hosted instances:

html
<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.

Container Requirements: Your container needs a defined 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

html
<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

AI Assistant

Hi! How can I help you today?

How do I get started?

Great question! You can start by creating a project in your dashboard...

Embedded mode - always visible

Configuration Options

OptionTypeDefaultDescription
apiKeystringrequiredYour project's public API key
mode'floating' | 'embedded' | 'inline''floating'Widget display mode
containerstring | HTMLElementnoneCSS selector or element (required for embedded)
apiUrlstringcdn defaultCustom API endpoint URL
showHeaderbooleantrueShow or hide header in embedded mode
showBrandingbooleanfrom configOverride branding visibility
borderRadiusstring'0'CSS border radius for container
showAvatarsbooleantrueShow or hide user/bot avatars next to messages
themeThemeOverridesnoneCustom color and style overrides (see Theme Customization)
zIndexnumber2147483000Base z-index for the widget layer stack. The widget uses 7 layers above this base value. Adjust if you need to control stacking with other overlays.

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.

Dashboard Controlled Enable or disable the inline explainer from your project's Settings page in the dashboard. When enabled, users can select text and get AI explanations powered by your indexed documentation. The sidebar also supports follow up conversations for deeper understanding.

Basic Setup

html
<!-- 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

javascript
// For programmatic control, you can override dashboard settings
Pernoex.init({
apiKey: 'YOUR_API_KEY',
inline: {
enabled: true, // Override dashboard setting
triggerSelector: 'article, .docs-content', // Only in these areas
excludeSelector: 'nav, .code-block', // Not in these areas
position: 'auto', // 'auto' | 'above' | 'below'
showFollowUp: true, // Allow follow-up questions
theme: 'auto' // Match system theme
}
});

Configuration Options

OptionTypeDefaultDescription
enabledbooleanfalseEnable inline explainer (controlled from dashboard settings by default)
triggerSelectorstringnoneCSS selector to limit where text selection triggers the explainer
excludeSelectorstringnoneCSS selector for areas to exclude from text selection
position'auto' | 'above' | 'below''auto'Preferred tooltip position relative to selection
maxWidthnumber400Maximum width of the explainer sidebar in pixels
showFollowUpbooleanfalseShow input field for follow up questions
theme'light' | 'dark' | 'auto''auto'Color theme for the sidebar

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:

javascript
// 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:

json
// .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):

json
// .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:

bash
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:

json
// 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:

json
// ~/.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_ key

Your 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.

Parameters
question required— The question to answer
context optional— Additional context to refine the answer
json
{
"name": "pernoex_ask",
"arguments": {
"question": "How do I configure SSO?",
"context": "Setting up authentication for enterprise customers"
}
}
pernoex_searchsk_live_ key only

Returns 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.

Parameters
question required— The search query
context optional— Additional context to improve relevance
code_only optional— Return only code snippets
json
{
"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 Authorization header format is Bearer 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)

javascript
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

javascript
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

PropertyTypeDefaultDescription
backgroundstring#f7f8faMessages area & window background. Use 'transparent' for seamless embedding.
assistantBubbleBgstring#ffffffAssistant message bubble background
assistantTextColorstring#1a1d23Assistant message text color
userBubbleBgstringprimary colorUser message bubble background
userTextColorstring#ffffffUser message text color
linkColorstringprimary colorLink color inside messages
inputBgstring#f7f8faInput textarea background
inputBorderColorstring#e5e7ebInput border color
inputTextColorstring#1a1d23Input text color
inputPlaceholderColorstring#b0b8c1Input placeholder text color
sendButtonBgstringprimary colorSend button background
sendButtonColorstring#ffffffSend button icon color
fontFamilystringsystem defaultGlobal font family override

Common Use Cases

Full-Page Intelligence

Create a dedicated support or chat page:

html
<!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>
AI Assistant

Hi! How can I help you today?

How do I get started?

Great question! You can start by creating a project in your dashboard...

Embedded mode - always visible

Sidebar Integration

Add a persistent intelligence sidebar to your application:

html
<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>
Help

Need help with anything?

Sidebar integration layout

JavaScript API Reference

Control Methods

Available for floating mode. All methods are accessible on the global Pernoex object after init():

javascript
// Open the chat window
Pernoex.open()
// Close the chat window
Pernoex.close()
// Toggle open/closed state
Pernoex.toggle()
// Check if widget is currently open
const isOpen = Pernoex.isOpen()
// Clear all messages and start a new conversation
Pernoex.clearMessages()
// Get explanation for text programmatically
const explanation = await Pernoex.explain("text to explain")
// Destroy the widget and clean up
Pernoex.destroy()

Event Callbacks

Listen to integration lifecycle events:

javascript
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

Copy for AI Agent

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.

Per-Message Copy

Each assistant message has a copy icon that copies just that response to the clipboard, including any source citations.

Code Block Copy

Code blocks in responses include a dedicated copy button. The raw code is copied without any surrounding markdown formatting.

Image Lightbox

Images embedded in source content are clickable. Clicking opens a full-screen lightbox overlay for better viewing.

Host Font Inheritance

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:

text
// SSE event types received during a streamed response
// Connect to: /api/v1/chat/stream
event: 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 returned
event: text // Streamed answer text chunk
event: sources // Source citations array
event: confidence // Confidence score (0-1)
event: actions // Suggested follow-up actions
event: done // Stream complete
EventPayloadDescription
session_contextJSON objectSession metadata including conversation_id for follow-ups
thinkingstringReasoning step indicator (content may be empty)
toolJSON objectCustom action invocation — includes action name and parameters
tool_completeJSON objectCustom action result — includes status and output
textstringStreamed answer text chunk — append to the response
sourcesJSON arraySource citations — each with title, URL, and relevance score
confidencenumber (0–1)Answer confidence score — below 0.5 may indicate a content gap
actionsJSON arraySuggested follow-up actions or related questions
done—Stream complete — close the connection

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:

bash
# 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:

bash
# Search documents without generating an answer
curl -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:

json
// 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

  1. A user asks a question that requires an external lookup (e.g., "What's my order status?")
  2. The AI identifies the relevant action and extracts parameters from the conversation
  3. If confirmation_required is true, the user sees a confirmation prompt before the action executes
  4. Pernoex calls your endpoint with the extracted parameters and authentication headers
  5. 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: true for 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

  1. Go to your project's Surface → Slack page in the dashboard
  2. Click Add to Slack and authorize the bot in your workspace
  3. Invite @Pernoex to any channel where you want it available

Usage

  • Mention: Tag @Pernoex with 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 tagged
  • chat:write — post replies
  • channels:history / groups:history — read thread context
  • files:read — read images shared in threads
  • im:history — support direct messages

Framework Examples

jsx
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.

Important: The identity secret must never appear in client-side code. Compute the HMAC on your server and pass only the result to the widget.

2. Compute the HMAC (Server-Side)

On your server, compute the HMAC using the user's details:

javascript
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');
}
// Usage
const hmac = generatePernoexHmac(
process.env.PERNOEX_IDENTITY_SECRET,
'usr_123',
'[email protected]',
'Jane'
);
python
import hmac, hashlib
def 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()
# Usage
sig = generate_pernoex_hmac(
os.environ["PERNOEX_IDENTITY_SECRET"],
"usr_123",
"[email protected]",
"Jane"
)
go
package main
import (
"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:

javascript
Pernoex.init({
apiKey: 'YOUR_API_KEY',
userIdentity: {
user_id: 'usr_123',
email: '[email protected]', // optional
name: 'Jane', // optional
hmac: '<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:

text
t=1714500000,v1=5d41402abc4b2a76b9719d911017c592

t is the Unix timestamp when the webhook was sent. v1 is the HMAC-SHA256 signature of timestamp.payload.

Verification

javascript
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')
);
}
python
import hmac, hashlib, time
def 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() + payload
expected = 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

Shadow DOM Isolation: The widget renders inside a Shadow DOM boundary, providing full CSS and DOM encapsulation:
  • 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 — no style-src 'unsafe-inline' CSP directive needed
  • Fonts are loaded via @font-face inside the shadow root — no <link> tags are injected into your <head>
Important: Only use your public API key in client-side code. Never expose private/secret keys in HTML or JavaScript.

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. localhost is not automatically allowed once you lock down your project.
Testing locally after locking down? Add 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.

text
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, localhost is no longer automatically permitted
  • Add localhost or localhost:PORT (e.g.localhost:3000) to your allowed domains list for local development
  • Remove localhost from 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_allowed when 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 zIndex option in Pernoex.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 triggerSelector matches 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.

Ready to give your product a mind?

Get started free
Pernoex

AI-native documentation intelligence for SaaS teams.

Product

  • Features
  • Pricing
  • Compare Plans
  • Changelog
  • Security

Resources

  • Documentation
  • API Reference

Company

  • About
  • Contact
© 2026 Pernoex. All rights reserved.
PrivacyTermsCookies