Agent skill
chatkit-frontend-integration
This skill configures OpenAI ChatKit for React frontends with proper domain security, authentication, and backend integration. It covers ChatKit UI setup, domain allowlist configuration, domain key injection, client secret management, and production vs localhost deployment patterns. Use when integrating ChatKit widget, configuring domain security, or connecting ChatKit UI to backend endpoints.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/chatkit-frontend-integration
SKILL.md
ChatKit Frontend Integration
What This Skill Does
This skill provides comprehensive guidance for integrating OpenAI ChatKit into React applications. It covers:
- ChatKit UI component setup with
@openai/chatkit-react - Domain allowlist security configuration
- Domain key injection for custom backends
- Client secret authentication flow
- Backend endpoint connection patterns
- Production vs localhost deployment differences
When to Use
- When setting up ChatKit widget in a React application
- When configuring domain allowlist for ChatKit security
- When implementing domain key injection for custom integrations
- When connecting ChatKit frontend to a backend endpoint
- When handling authentication with client secrets
- When troubleshooting "blank screen" or domain verification issues
- When deploying ChatKit to production environments
When NOT to Use
- When building backend ChatKit server (use chatkit-python-backend skill)
- When working on non-ChatKit chat implementations
- When using vanilla JavaScript without React
- When specifications don't require ChatKit integration
Required Clarifications
Before implementing ChatKit, clarify:
- Integration Type: Hosted (OpenAI-managed) or Custom/Self-hosted backend?
- Environment: Production domain or localhost development?
- Authentication: Using client secrets or domain keys?
- Backend Framework: FastAPI, Next.js API routes, or other?
Core Concepts
Architecture Overview
ChatKit uses a three-party authentication system:
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Client │ ───► │ Backend │ ───► │ OpenAI API │
│ (ChatKit) │ ◄─── │ Server │ ◄─── │ │
└─────────────┘ └──────────────┘ └─────────────────┘
│ │ │
│ 1. Request secret │ 2. Generate token │
│ ◄────────────────────│ ◄────────────────────│
│ │ │
│ 3. Direct chat │ │
│ ─────────────────────────────────────────► │
- Client requests a short-lived
client_secretfrom your backend - Backend uses your API key to generate the token via OpenAI
- Client uses the token to communicate directly with OpenAI
Integration Types
| Type | Backend | Domain Key | Use Case |
|---|---|---|---|
| Hosted | OpenAI-managed | Not needed | Agent Builder workflows |
| Custom | Self-hosted | Required | Full control, custom logic |
Installation
Package Installation
# React package (recommended)
npm install @openai/chatkit-react
# TypeScript types (optional, for enhanced type support)
npm install @openai/chatkit
CDN Script (Alternative)
<script src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js" async></script>
TypeScript Configuration
Add to tsconfig.json for global type support:
{
"compilerOptions": {
"types": ["@openai/chatkit"]
}
}
Domain Allowlist Configuration (CRITICAL)
Why This Matters
ChatKit will not render on domains not explicitly allowed. This is the most common cause of "blank screen" issues.
Configuration Steps
- Navigate to: https://platform.openai.com/settings/organization/security/domain-allowlist
- Add your production domain (e.g.,
myapp.com) - Wait 20-30 minutes for verification to activate
- For localhost: See Localhost Development
Common Mistakes
// BAD: Will fail silently on unregistered domain
const { control } = useChatKit({
api: { url: '/api/chat' } // Domain not in allowlist
});
// GOOD: After adding domain to allowlist
const { control } = useChatKit({
api: {
async getClientSecret() { /* ... */ }
}
});
Domain Verification Behavior
- ChatKit calls
POST /v1/chatkit/domain_keys/verify_hosted - If domain not in allowlist: silent failure, blank widget
- No console errors shown (security feature)
- Mismatched API key organization: authorization errors
Frontend Integration
Basic Setup (Hosted Backend)
// components/Chat.tsx
import { ChatKit, useChatKit } from '@openai/chatkit-react';
export function Chat() {
const { control } = useChatKit({
api: {
async getClientSecret(existingSecret) {
// Request new or refresh existing token
const endpoint = existingSecret
? '/api/chatkit/refresh'
: '/api/chatkit/session';
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: existingSecret
? JSON.stringify({ currentSecret: existingSecret })
: undefined,
});
if (!res.ok) throw new Error('Failed to get client secret');
const { client_secret } = await res.json();
return client_secret;
},
},
});
return (
<ChatKit
control={control}
className="h-[600px] w-full max-w-[400px]"
/>
);
}
Custom Backend Setup (Domain Key)
// components/Chat.tsx
import { ChatKit, useChatKit } from '@openai/chatkit-react';
export function Chat() {
const { control } = useChatKit({
api: {
url: process.env.NEXT_PUBLIC_CHATKIT_API_URL,
domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY,
},
});
return (
<ChatKit
control={control}
className="h-[600px] w-full max-w-[400px]"
/>
);
}
Full-Featured Implementation
// components/ChatWidget.tsx
import { ChatKit, useChatKit } from '@openai/chatkit-react';
import { useState, useCallback } from 'react';
interface ChatWidgetProps {
userId?: string;
threadId?: string;
onError?: (error: Error) => void;
}
export function ChatWidget({ userId, threadId, onError }: ChatWidgetProps) {
const [isReady, setIsReady] = useState(false);
const handleError = useCallback((event: CustomEvent) => {
console.error('ChatKit error:', event.detail);
onError?.(new Error(event.detail?.message || 'ChatKit error'));
}, [onError]);
const handleReady = useCallback(() => {
setIsReady(true);
}, []);
const { control, ref, setThreadId, sendUserMessage } = useChatKit({
api: {
async getClientSecret(existing) {
const res = await fetch('/api/chatkit/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId,
refreshToken: existing
}),
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.message || 'Session creation failed');
}
const { client_secret } = await res.json();
return client_secret;
},
},
// Event handlers
onError: handleError,
onReady: handleReady,
onResponseStart: () => console.log('Response started'),
onResponseEnd: () => console.log('Response ended'),
onThreadChange: (e) => console.log('Thread changed:', e.detail),
});
// Set initial thread if provided
useEffect(() => {
if (threadId && isReady) {
setThreadId(threadId);
}
}, [threadId, isReady, setThreadId]);
return (
<div className="relative">
{!isReady && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-100">
<span className="animate-pulse">Loading chat...</span>
</div>
)}
<ChatKit
ref={ref}
control={control}
className="h-[600px] w-full rounded-lg shadow-lg"
/>
</div>
);
}
useChatKit Hook API
Configuration Options
interface UseChatKitOptions {
api: {
// For hosted integration (recommended)
getClientSecret?: (existing?: string) => Promise<string>;
// For custom backend
url?: string;
domainKey?: string;
};
// Event handlers
onError?: (event: CustomEvent) => void;
onReady?: () => void;
onResponseStart?: () => void;
onResponseEnd?: () => void;
onThreadChange?: (event: CustomEvent) => void;
onThreadLoadStart?: () => void;
onThreadLoadEnd?: () => void;
}
Returned Methods
const {
control, // Pass to <ChatKit control={control} />
ref, // Optional ref for imperative access
// Imperative methods
focusComposer, // Focus the input field
setThreadId, // Switch to a different thread
sendUserMessage, // Programmatically send a message
setComposerValue, // Set input field value
fetchUpdates, // Fetch latest updates
sendCustomAction, // Send custom widget action
showHistory, // Show conversation history
hideHistory, // Hide conversation history
} = useChatKit(options);
Backend Endpoint Examples
FastAPI (Python)
# app/api/chatkit.py
from fastapi import APIRouter, HTTPException
from openai import OpenAI
from pydantic import BaseModel
router = APIRouter(prefix="/api/chatkit")
client = OpenAI()
class SessionRequest(BaseModel):
userId: str | None = None
refreshToken: str | None = None
class SessionResponse(BaseModel):
client_secret: str
@router.post("/session", response_model=SessionResponse)
async def create_session(request: SessionRequest):
try:
# Create or refresh ChatKit session
session = client.chatkit.sessions.create(
# Configure based on your needs
model="gpt-4o",
tools=[],
)
return SessionResponse(client_secret=session.client_secret)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/refresh", response_model=SessionResponse)
async def refresh_session(request: SessionRequest):
try:
# Refresh existing session
session = client.chatkit.sessions.refresh(
client_secret=request.refreshToken
)
return SessionResponse(client_secret=session.client_secret)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
Next.js API Route
// app/api/chatkit/session/route.ts
import { NextRequest, NextResponse } from 'next/server';
import OpenAI from 'openai';
const openai = new OpenAI();
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Create ChatKit session
const session = await openai.chatkit.sessions.create({
model: 'gpt-4o',
// Add your configuration
});
return NextResponse.json({
client_secret: session.client_secret
});
} catch (error) {
console.error('ChatKit session error:', error);
return NextResponse.json(
{ error: 'Failed to create session' },
{ status: 500 }
);
}
}
Localhost Development
The Challenge
- Localhost (
localhost,127.0.0.1) cannot be added to domain allowlist - Domain verification will fail for local development
- This is a security feature, not a bug
Solutions
Option 1: Ngrok Tunneling (Recommended)
# Install ngrok
npm install -g ngrok
# Start your dev server
npm run dev # Usually http://localhost:3000
# In another terminal, create tunnel
ngrok http 3000
Then add the ngrok domain (e.g., abc123.ngrok.io) to your allowlist.
Option 2: Custom API Integration
Use api.url and api.domainKey instead of hosted integration:
const { control } = useChatKit({
api: {
url: 'http://localhost:8000/api/chatkit',
domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY,
},
});
Option 3: Proxy Configuration
// next.config.js
module.exports = {
async rewrites() {
return [
{
source: '/api/chatkit/:path*',
destination: 'https://api.openai.com/v1/chatkit/:path*',
},
];
},
};
Production Deployment
Environment Variables
# .env.production
NEXT_PUBLIC_CHATKIT_API_URL=https://api.yourdomain.com/chatkit
NEXT_PUBLIC_CHATKIT_DOMAIN_KEY=dk_xxxxxxxxxxxx # For custom integration only
OPENAI_API_KEY=sk_xxxxxxxxxxxx # Server-side only, never expose
Pre-Deployment Checklist
- [ ] Production domain added to OpenAI allowlist
- [ ] Waited 20-30 minutes after adding domain
- [ ] API key matches organization with domain allowlist
- [ ] OPENAI_API_KEY is server-side only (not NEXT_PUBLIC_)
- [ ] Error boundaries implemented around ChatKit
- [ ] Loading states for initial render
- [ ] Token refresh logic implemented
Security Best Practices
// NEVER do this - exposes API key
const { control } = useChatKit({
api: {
apiKey: process.env.OPENAI_API_KEY, // WRONG!
},
});
// ALWAYS do this - use backend for token generation
const { control } = useChatKit({
api: {
async getClientSecret() {
const res = await fetch('/api/chatkit/session', { method: 'POST' });
const { client_secret } = await res.json();
return client_secret;
},
},
});
Troubleshooting
Blank Screen / Widget Not Rendering
- Check domain allowlist at https://platform.openai.com/settings/organization/security/domain-allowlist
- Verify domain matches exactly (including
www.if used) - Wait 20-30 minutes after adding new domain
- Check browser console for network errors
401/403 Authorization Errors
- Verify API key belongs to same organization as allowlist
- Check API key has ChatKit permissions
- Ensure
getClientSecretendpoint is accessible
Token Refresh Failures
// Implement proper refresh logic
async getClientSecret(existing) {
if (!existing) {
// First-time token request
return await createNewSession();
}
try {
// Try to refresh
return await refreshSession(existing);
} catch (error) {
// If refresh fails, create new session
return await createNewSession();
}
}
CORS Issues
# FastAPI CORS configuration
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"https://yourdomain.com",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
File Structure
src/
├── components/
│ └── chat/
│ ├── ChatWidget.tsx # Main ChatKit wrapper
│ ├── ChatError.tsx # Error boundary
│ └── ChatLoading.tsx # Loading state
├── hooks/
│ └── useChatSession.ts # Session management hook
├── lib/
│ └── chatkit/
│ ├── client.ts # API client
│ └── types.ts # Type definitions
└── app/
└── api/
└── chatkit/
└── session/
└── route.ts # Session endpoint
Integration with Other Skills
- frontend-architecture: ChatKit components fit within frontend structure
- fastapi-architecture: Backend endpoints follow FastAPI patterns
- auth-aware-ui: ChatKit integrates with existing auth flow
- api-client-design: Session management follows API patterns
References
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?