@pineconesai/chat-sdk
v0.9.0
Published
Embeddable AI chat widget for the Pineconesai platform
Readme
Pineconesai Chat Widget — Developer Guide
Table of Contents
- Quick start
- Installation options
- Boot configuration
- API reference
- Events
- User identity & verification
- Theming
- Custom launcher
- Single-page apps
- TypeScript usage
1. Quick start
Paste this snippet before the closing </body> tag on every page where you want the widget to appear:
<script>
(function () {
window.PineconesAI = function () {
(window.PineconesAI.q = window.PineconesAI.q || []).push(arguments);
};
var s = document.createElement('script');
s.src = 'https://cdn.pineconesai.com/widget.js';
s.async = true;
document.head.appendChild(s);
})();
PineconesAI('boot', {
apiKey: 'pk_live_YOUR_KEY',
});
</script>That's it. A floating chat button will appear in the bottom-right corner of the page.
2. Installation options
Option A — Script snippet (recommended for most sites)
Use the snippet above. The main bundle loads asynchronously after your page is interactive. Any PineconesAI(...) calls made before the bundle loads are queued and replayed automatically.
Option B — npm package
npm install @pineconesai/chat-sdkimport '@pineconesai/chat-sdk' // auto-boots and flushes the command queue
window.PineconesAI('boot', {
apiKey: 'pk_live_YOUR_KEY',
})3. Boot configuration
boot() accepts a single configuration object:
PineconesAI('boot', {
// ── Required ───────────────────────────────────────────────────────────
apiKey: 'pk_live_YOUR_KEY', // Your workspace public API key
// ── API base URL (optional) ────────────────────────────────────────────
baseUrl: 'https://api.pineconesai.com/api/v2', // Override the API host (local dev)
// ── User identity (optional) ───────────────────────────────────────────
user: {
id: 'usr_42', // Your app's user ID
name: 'Jane Doe',
email: '[email protected]',
jwt: 'eyJhbGci...', // Customer-signed identity JWT (see section 6)
attributes: { // Any custom key/value pairs
plan: 'pro',
company: 'Acme Corp',
},
},
// ── AI behaviour (optional) ────────────────────────────────────────────
ai: {
context: 'You are a helpful assistant for Acme Corp, a project-management SaaS.',
suggestedMessages: [ // Shown on the home screen when chat is empty
'How do I get started?',
'What does your pricing look like?',
'Can I import data from another tool?',
],
agentName: 'Acme Assistant', // Name shown next to AI replies.
// Defaults to "AI agent" ("סוכן AI" in Hebrew).
},
// ── Theme (optional) ───────────────────────────────────────────────────
theme: {
primaryColor: '#5b4af7', // Hex color for buttons, bubbles, header
position: 'bottom-right', // 'bottom-right' | 'bottom-left'
layout: 'panel', // 'panel' (floating card) | 'drawer' (full-height) | 'inline' (mount in a host element)
launcherIcon: 'chat', // 'chat' | 'robot' | 'https://…/icon.png'
suggestedButtonStyle: 'filled', // 'filled' (default) | 'outlined'
headerStyle: 'solid', // 'solid' (filled primary bar) | 'surface' (light header)
// ── Design tokens (optional) — each maps to a --pcai-* CSS variable ──
fontFamily: 'Inter, system-ui, sans-serif', // Base font for all widget text
radius: 16, // Corner radius (number = px) for panel, bubbles, controls
backgroundColor: '#ffffff', // Panel background
surfaceColor: '#f5f5f7', // Secondary surface — AI bubbles & home cards
borderColor: '#e5e5ea', // Border & divider color
textColor: '#1c1c1e', // Primary text color
mutedTextColor: '#8e8e93', // Muted / secondary text color
userBubbleColor: '#5b4af7', // User message bubble (defaults to primaryColor)
aiBubbleColor: '#f5f5f7', // AI message bubble (defaults to surfaceColor)
shadow: '0 12px 32px rgba(0,0,0,.16)', // Panel drop shadow (CSS box-shadow)
},
// ── Branding (optional) ────────────────────────────────────────────────
// Every field is optional. Omitted fields fall back to the localized default,
// so leaving `branding` out keeps the current look unchanged.
branding: {
headerTitle: 'Support Chat', // Static title; replaces the "Hi, Jane 👋" greeting
headerSubtitle: 'How can we help?', // Defaults to the localized subtitle
headerLogoUrl: 'https://…/logo.png', // Small logo shown in the header
showRefreshButton: true, // Header icon to start a new conversation
showCloseButton: true, // In-panel close (X) button — shown by default; set false to hide
inputPlaceholder: 'Ask us…', // Composer placeholder (default: "Type a message…")
logoAlt: 'Acme', // Alt text for both logo images
home: { // Hero card on the home screen (omit to hide)
logoUrl: 'https://…/logo.png',
title: 'Support Chat',
subtitle: 'How can we help you?',
welcomeMessage: 'Have a question? Just ask us or choose an option below.',
},
},
// ── Recent conversations (optional) ────────────────────────────────────
recentConversationsLimit: 5, // How many to show in "Recent" (0–20). 0 hides the section.
// ── Language (optional) ────────────────────────────────────────────────
language: 'en', // 'en' | 'he' — Hebrew enables RTL layout
// ── Launcher (optional) ────────────────────────────────────────────────
hideDefaultLauncher: false, // Hide the built-in floating button
customLauncherSelector: '#my-btn', // CSS selector for your own trigger element
// ── Inline mount (optional) ────────────────────────────────────────────
container: '#chat-root', // CSS selector or HTMLElement to mount into (use with theme.layout: 'inline')
})Full option reference
Every option boot() accepts. apiKey is required — every other field is
optional and falls back to the default shown.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| apiKey | string | — (required) | Your workspace public API key (pk_live_… / pk_test_…). |
| baseUrl | string | https://api.pineconesai.com/api/v2 | Override the API base URL. Useful for local development. |
| user.id | string | — | Your app's user ID. |
| user.name | string | — | User's display name. Drives the "Hi, Jane 👋" greeting. |
| user.email | string | — | User's email address. |
| user.jwt | string | — | Customer-signed identity JWT (HS256, sub = user ID, with exp), minted server-side with your integration secret. Verifies a logged-in user (see §6). |
| user.attributes | object | — | Any custom key/value pairs (e.g. { plan: 'pro', company: 'Acme' }). |
| ai.context | string | — | System context describing your business, steering the AI's replies. |
| ai.suggestedMessages | string[] | — | Prompts shown on the home screen when the chat is empty. |
| ai.agentName | string | "AI agent" ("סוכן AI" in Hebrew) | Name shown next to AI replies. |
| theme.primaryColor | string (hex) | brand default | Color for buttons, bubbles, and the header. |
| theme.position | 'bottom-right' \| 'bottom-left' | 'bottom-right' | Which corner the launcher and panel anchor to. |
| theme.layout | 'panel' \| 'drawer' \| 'inline' | 'panel' | panel = floating card; drawer = full-height side panel; inline = mount inside a host element (see container). |
| theme.launcherIcon | 'chat' \| 'robot' \| string (url) | 'chat' | Built-in icon or a URL to your own image (28×28px). |
| theme.suggestedButtonStyle | 'filled' \| 'outlined' | 'filled' | Style of the suggested-message buttons. |
| theme.headerStyle | 'solid' \| 'surface' | 'solid' | solid = filled primary-color bar with white text; surface = light header that blends into host chrome. |
| theme.fontFamily | string | system UI stack | Base font-family for all widget text (--pcai-font). |
| theme.radius | string \| number | 16 | Corner radius for panel, bubbles, and controls. A number is treated as px. |
| theme.backgroundColor | string | '#ffffff' | Panel background color. |
| theme.surfaceColor | string | '#f5f5f7' | Secondary surface color — AI bubbles and home cards. |
| theme.borderColor | string | '#e5e5ea' | Border and divider color. |
| theme.textColor | string | '#1c1c1e' | Primary text color. |
| theme.mutedTextColor | string | '#8e8e93' | Muted / secondary text color. |
| theme.userBubbleColor | string | primaryColor | User message bubble color. |
| theme.aiBubbleColor | string | surfaceColor | AI message bubble color. |
| theme.shadow | string (CSS box-shadow) | subtle floating shadow | Panel drop shadow. |
| branding.headerTitle | string | dynamic greeting | Static header title; replaces the "Hi, Jane 👋" greeting. |
| branding.headerSubtitle | string | localized default | Subtitle under the header title. |
| branding.headerLogoUrl | string (url) | — | Small square logo shown in the header (≥112×112px). |
| branding.showRefreshButton | boolean | false | Show a header icon to start a new conversation. |
| branding.showCloseButton | boolean | true | Show the in-panel close (X) button. |
| branding.inputPlaceholder | string | "Type a message…" | Composer placeholder text. |
| branding.logoAlt | string | "" | Alt text for both logo images. |
| branding.home.logoUrl | string (url) | — | Logo on the home-screen hero card. |
| branding.home.title | string | — | Title on the home-screen hero card. |
| branding.home.subtitle | string | — | Subtitle on the home-screen hero card. |
| branding.home.welcomeMessage | string | — | Welcome message on the home-screen hero card. Omit the whole home object to hide the card. |
| recentConversationsLimit | number (0–20) | 5 | How many recent conversations to list. 0 hides the section. |
| language | 'en' \| 'he' | 'en' | UI language. 'he' enables a right-to-left layout. |
| hideDefaultLauncher | boolean | false | Hide the built-in floating button. |
| customLauncherSelector | string (CSS selector) | — | CSS selector for your own trigger element. |
| container | string (CSS selector) \| HTMLElement | document.body | Host element to mount the widget into. Intended for theme.layout: 'inline'. Falls back to document.body if not found. |
4. API reference
All methods follow the same call signature: PineconesAI('methodName', ...args).
Lifecycle
| Method | Description |
|--------|-------------|
| PineconesAI('boot', config) | Initialize the widget. Must be called first. |
| PineconesAI('shutdown') | Tear down the widget and clear the session. Call on logout. |
UI control
| Method | Description |
|--------|-------------|
| PineconesAI('show') | Open the chat panel. |
| PineconesAI('hide') | Close the chat panel. |
| PineconesAI('openNewConversation') | Open the panel and start a new conversation. |
| PineconesAI('openNewConversation', 'prefill text') | Same, with text pre-filled in the composer. |
User & data
| Method | Description |
|--------|-------------|
| PineconesAI('update', user) | Update user identity after login or profile change. The header greeting updates automatically — "Hi, Jane 👋" when a name is supplied, "Hi there 👋" for anonymous visitors. |
| PineconesAI('trackEvent', name, metadata?) | Send a custom analytics event. |
File attachments
The widget has built-in file attachment support. A paperclip button appears in the message composer — users can attach up to 5 files per message, each up to 10 MB. Images are shown as thumbnails; other files appear as downloadable chips. No configuration is required to enable this feature.
Attached files are included in message.attachments when you receive messages via the onMessageReceived event (see Section 5).
Events
| Method | Description |
|--------|-------------|
| PineconesAI('onShow', callback) | Fires when the panel opens. Callback receives no arguments. |
| PineconesAI('onHide', callback) | Fires when the panel closes. Callback receives no arguments. |
| PineconesAI('onUnreadCountChange', callback) | Fires on unread count changes. Also fires immediately on registration. Callback receives the new count (number). |
| PineconesAI('onMessageReceived', callback) | Fires when an incoming message is fully received (AI reply or a human rep). Callback receives a Message. |
| PineconesAI('onMessageSent', callback) | Fires when the visitor sends a message. Callback receives a SentMessage (a Message without id). |
| PineconesAI('onConversationCreated', callback) | Fires when a new conversation is created on the server (on the visitor's first message in a draft). Callback receives a Conversation. |
5. Events
Subscribe to widget events to keep your own UI in sync:
// Fires every time the chat panel opens
PineconesAI('onShow', function () {
console.log('Chat opened')
})
// Fires every time the chat panel closes
PineconesAI('onHide', function () {
console.log('Chat closed')
})
// Fires immediately with current count, then on every change
PineconesAI('onUnreadCountChange', function (count) {
document.getElementById('my-badge').textContent = count > 0 ? count : ''
})
// Fires when an incoming message is fully received (AI reply or human rep)
PineconesAI('onMessageReceived', function (message) {
console.log('New message:', message.id, message.content)
// Distinguish the AI agent from a human support rep
if (message.senderType === 'human') {
console.log('Reply from', message.senderName)
}
// message.attachments is an array of { id, name, url, mimeType, size }
if (message.attachments?.length) {
console.log('Attachments:', message.attachments)
}
})
// Fires when the visitor sends a message (no id — the persisted id is not
// returned to the SDK)
PineconesAI('onMessageSent', function (message) {
console.log('Visitor sent:', message.content)
})
// Fires when a new conversation is created on the server (on the first message
// of a draft). On a brand-new conversation this fires before onMessageSent.
PineconesAI('onConversationCreated', function (conversation) {
console.log('New conversation:', conversation.id, conversation.title)
})Event payloads
| Event | Callback argument |
|-------|-------------------|
| onShow | none |
| onHide | none |
| onUnreadCountChange | count: number |
| onMessageReceived | message: Message (shape below) |
| onMessageSent | message: SentMessage — a Message without id |
| onConversationCreated | conversation: Conversation ({ id, title, lastMessage, unreadCount, updatedAt }) |
Message shape:
| Field | Type | Description |
|-------|------|-------------|
| id | string | Unique message ID. |
| role | 'user' \| 'assistant' | Who the message is from. Incoming messages are 'assistant'. |
| content | string | The message text. |
| createdAt | number | Creation timestamp (epoch ms). |
| attachments | Attachment[] (optional) | Files attached to the message. |
| senderType | 'ai' \| 'human' (optional) | Only on assistant messages — distinguishes the AI agent from a human rep. |
| senderName | string (optional) | First name of the human representative (when senderType === 'human'). |
Attachment shape:
| Field | Type | Description |
|-------|------|-------------|
| id | string | Unique attachment ID. |
| name | string | Original file name. |
| url | string | Download URL. |
| mimeType | string | MIME type (e.g. image/png). |
| size | number | File size in bytes. |
6. User identity & verification
The widget can run in one of two modes. Pick based on whether the visitor is logged into your app:
| Mode | When to use | What you pass |
|------|-------------|---------------|
| Unauthenticated (anonymous) | Logged-out visitors, marketing pages | Just apiKey — no user |
| Authenticated (verified user) | Logged-in users whose chat history must be protected | apiKey + user with a server-signed user.jwt |
Both modes always boot with your public apiKey (pk_live_… / pk_test_…),
which is safe to embed in the browser. Your integration secret is only ever
used on your server — never ship it to the client.
Option 1 — Unauthenticated (anonymous)
If you don't pass a user object, the widget generates a random visitorId
stored in localStorage. Conversation history persists across page reloads for
the same browser, but is not tied to any account.
PineconesAI('boot', {
apiKey: 'pk_live_YOUR_KEY',
})Anonymous sessions are allowed only when the integration does not enforce
Identity Verification. With enforcement on, a session without a valid user.jwt
— including anonymous visitors — is rejected (see Enforcing verification below).
Option 2 — Authenticated (verified user)
Pass a user object once the visitor is logged into your app. Because a browser
can claim any user.id, include a server-signed JWT (user.jwt) so
visitors can't impersonate one another and read each other's history.
Mint a short-lived JWT on your server, signed with your integration
app_secret, and pass it as user.jwt. The identity lives in the token —
put it in a user_id claim (sub is also accepted). Your secret never reaches
the browser — only the per-user token does.
Server side (Node.js example):
const jwt = require('jsonwebtoken')
const token = jwt.sign(
{ user_id: user.id }, // the identity — no need to pass it separately
process.env.PINECONESAI_APP_SECRET, // integration secret — server only
{ algorithm: 'HS256', expiresIn: '1h' }, // exp is required (recommend ≥ 5 min)
)Client side:
PineconesAI('boot', {
apiKey: 'pk_live_YOUR_KEY',
user: {
name: user.name, // unprotected fields — for display / low-fidelity data
email: user.email,
jwt: token, // from your server — carries the verified identity
},
})The backend verifies the signature against your app_secret and checks exp,
then takes the identity from the token's user_id (or sub) claim. You don't
need to pass user.id separately when a jwt is supplied.
Never expose your integration secret in client-side code. Always generate the
jwtserver-side.
Protecting individual fields ("authenticated only")
By default the widget still trusts name/email/attributes sent in the
boot() body — convenient, but a determined visitor could pass someone else's
email. To lock a field down, mark it Protected ("authenticated only") in
the admin app under Chat integration → User Context (toggle the Protected
checkbox next to email, name, or any custom attribute). For every protected
field, the backend uses the value from the verified JWT claim and ignores
whatever the body sends; if the token doesn't carry it, the field is dropped (no
unauthenticated fallback).
Put each protected field in the JWT as a top-level claim (Fin-style):
const token = jwt.sign(
{
user_id: user.id, // the identity
email: user.email, // protected → trusted from here, body ignored
name: user.name,
plan: user.plan, // any custom attribute can be protected too
},
process.env.PINECONESAI_APP_SECRET,
{ algorithm: 'HS256', expiresIn: '1h' },
)Fields not marked protected keep working from the boot() body, so this is
opt-in and backward-compatible. user.name in the body is still used for the
client-side greeting ("Hi, Jane 👋") regardless — it's display-only and never
trusted for identity when name is protected.
Updating identity mid-session
If the visitor logs in after the widget has booted, call update() with the
same fields (including the proof) instead of re-booting:
PineconesAI('update', {
name: currentUser.displayName,
email: currentUser.email,
jwt: token, // from your server — carries the verified identity
})Call shutdown() on logout so the session and local state are cleared:
PineconesAI('shutdown')Enforcing verification per integration
An integration can require Identity Verification (toggle it in the admin
panel). When enabled, every session must carry a valid user.jwt — the
widget always boots with the public apiKey plus a verified user (Option 2):
- A session without a valid
user.jwt— including an anonymous/logged-out visitor — is rejected with403 identity_verification_required. - An invalid or expired token is rejected with
401 identity_verification_failed.
So on an integration with verification enabled, only logged-in users (for whom
your server can mint a user.jwt) can open the widget. Leave verification off
if you need anonymous/logged-out visitors to chat.
7. Theming
Customise the widget's appearance via the theme option in boot():
PineconesAI('boot', {
apiKey: 'pk_live_YOUR_KEY',
theme: {
primaryColor: '#e85d04', // Your brand color
position: 'bottom-left', // Move the launcher to the left
layout: 'drawer', // Full-height side drawer instead of a floating card
launcherIcon: 'robot', // Built-in robot icon
},
})Layout options:
| Value | Result |
|-------|--------|
| 'panel' (default) | Floating card anchored above the launcher |
| 'drawer' | Full-height panel flush to the screen edge, sliding in from the side set by position (fills the screen on narrow viewports) |
| 'inline' | Panel mounted inside a host element (see container) — always open, no floating launcher |
Launcher icon options:
| Value | Result |
|-------|--------|
| 'chat' (default) | Speech bubble icon |
| 'robot' | Robot face icon |
| 'https://…/icon.png' | Your own image (28×28px recommended) |
Header style
theme.headerStyle controls how the header bar looks:
| Value | Result |
|-------|--------|
| 'solid' (default) | Filled primary-color bar with white text |
| 'surface' | Light header using the background/text/border tokens — blends into a host app's chrome |
Design tokens
For finer control, theme exposes a set of design tokens that each map to a
--pcai-* CSS variable. All are optional; any token you omit keeps its built-in
default, so existing integrations are unaffected. These are especially useful for
matching the widget to a host application in the inline layout.
PineconesAI('boot', {
apiKey: 'pk_live_YOUR_KEY',
theme: {
fontFamily: 'Inter, system-ui, sans-serif',
radius: 12, // number → px
backgroundColor: '#ffffff',
surfaceColor: '#f5f5f7',
borderColor: '#e5e5ea',
textColor: '#1c1c1e',
mutedTextColor: '#8e8e93',
userBubbleColor: '#5b4af7',
aiBubbleColor: '#f5f5f7',
shadow: '0 12px 32px rgba(0,0,0,.16)',
},
})| Token | Default | Description |
|-------|---------|-------------|
| fontFamily | system UI stack | Base font-family for all widget text. |
| radius | 16 | Corner radius (number = px) for panel, bubbles, controls. |
| backgroundColor | #ffffff | Panel background color. |
| surfaceColor | #f5f5f7 | Secondary surface — AI bubbles and home cards. |
| borderColor | #e5e5ea | Border and divider color. |
| textColor | #1c1c1e | Primary text color. |
| mutedTextColor | #8e8e93 | Muted / secondary text color. |
| userBubbleColor | primaryColor | User message bubble color. |
| aiBubbleColor | surfaceColor | AI message bubble color. |
| shadow | subtle floating shadow | Panel drop shadow (CSS box-shadow). |
Inline layout
Set theme.layout: 'inline' with a container to embed the widget inside your
own page element instead of floating over it. There's no launcher — the panel
fills the host element and stays open.
PineconesAI('boot', {
apiKey: 'pk_live_YOUR_KEY',
container: '#chat-root', // CSS selector or HTMLElement
theme: { layout: 'inline', headerStyle: 'surface' },
})<div id="chat-root" style="width: 400px; height: 600px;"></div>If the container element isn't found, the widget falls back to document.body.
Branding & home screen
The branding option restyles the widget's text and imagery to match your product —
a static header title, your logo, a hero/welcome card on the home screen, and a custom
input placeholder:
PineconesAI('boot', {
apiKey: 'pk_live_YOUR_KEY',
theme: { primaryColor: '#2563eb', suggestedButtonStyle: 'outlined' },
branding: {
headerTitle: 'Support Chat',
headerLogoUrl: 'https://acme.com/logo.png',
showRefreshButton: true,
showCloseButton: true,
inputPlaceholder: 'Ask us…',
home: {
logoUrl: 'https://acme.com/logo.png',
title: 'Support Chat',
subtitle: 'How can we help you?',
welcomeMessage: 'Have a question? Just ask us or choose an option below.',
},
},
})Every branding field is optional — any field you omit falls back to the
localized default (or, for headerTitle, the dynamic "Hi, Jane 👋" greeting). So
an empty/absent branding block leaves the widget looking exactly as it does today.
Logo images (headerLogoUrl, home.logoUrl) are URLs. Use a square image —
at least 112×112px for crisp rendering on retina displays. Set branding.logoAlt for
the images' alt text (defaults to empty/decorative).
8. Custom launcher
Hide the default button and use your own
PineconesAI('boot', {
apiKey: 'pk_live_YOUR_KEY',
hideDefaultLauncher: true,
customLauncherSelector: '#open-chat-btn',
})<button id="open-chat-btn">Talk to us</button>Clicking the element matching the selector will toggle the widget open/closed automatically.
Programmatic control
You can also open and close the widget from anywhere in your own code:
// Open on a specific user action
document.getElementById('help-link').addEventListener('click', () => {
PineconesAI('show')
})
// Open with a pre-filled message
document.getElementById('upgrade-btn').addEventListener('click', () => {
PineconesAI('openNewConversation', 'I want to upgrade my plan.')
})9. Single-page apps
React
Boot once when your app mounts and shut down on unmount:
import { useEffect } from 'react'
export function ChatWidget({ user }) {
useEffect(() => {
PineconesAI('boot', {
apiKey: 'pk_live_YOUR_KEY',
// `user.jwt` is minted on your server (see §6). Omit the whole `user`
// object for anonymous visitors.
user: { id: user.id, name: user.name, email: user.email, jwt: user.chatJwt },
})
return () => {
PineconesAI('shutdown')
}
}, [])
// Update identity when the user object changes
useEffect(() => {
if (user) {
PineconesAI('update', { id: user.id, name: user.name, email: user.email, jwt: user.chatJwt })
}
}, [user.id])
return null
}Vue
// main.js or a plugin
app.mixin({
mounted() {
if (this.$options.name === 'App') {
PineconesAI('boot', { apiKey: 'pk_live_YOUR_KEY' })
}
},
beforeUnmount() {
if (this.$options.name === 'App') {
PineconesAI('shutdown')
}
},
})Route changes
Call update() on each navigation to refresh page context:
router.afterEach((to) => {
PineconesAI('update', {}) // pings the server and checks for new messages
})10. TypeScript usage
When importing via npm, all public types are exported:
import type { BootConfig, UserConfig, ThemeConfig, BrandingConfig, AIConfig, Message, Conversation, Session } from '@pineconesai/chat-sdk'
const config: BootConfig = {
apiKey: 'pk_live_YOUR_KEY',
user: {
id: '42',
email: '[email protected]',
},
theme: {
primaryColor: '#5b4af7',
},
}
PineconesAI('boot', config)
PineconesAI('onMessageReceived', (message: Message) => {
console.log(message.content)
// message.attachments is typed on the Message; message.senderType is 'ai' | 'human'
})API keys
| Key format | Use |
|------------|-----|
| pk_live_… | Production — real users, real data |
| pk_test_… | Development & testing |
Get your keys from the Pineconesai Admin Panel.
