| name | chatkit-frontend-integration |
| description | 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.
|
| allowed-tools | Read, Grep, Glob, Edit, Write |
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_secret from 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
npm install @openai/chatkit-react
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
const { control } = useChatKit({
api: { url: '/api/chat' }
});
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)
import { ChatKit, useChatKit } from '@openai/chatkit-react';
export function Chat() {
const { control } = useChatKit({
api: {
async getClientSecret(existingSecret) {
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)
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
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;
},
},
onError: handleError,
onReady: handleReady,
onResponseStart: () => console.log('Response started'),
onResponseEnd: () => console.log('Response ended'),
onThreadChange: (e) => console.log('Thread changed:', e.detail),
});
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: {
getClientSecret?: (existing?: string) => Promise<string>;
url?: string;
domainKey?: string;
};
onError?: (event: CustomEvent) => void;
onReady?: () => void;
onResponseStart?: () => void;
onResponseEnd?: () => void;
onThreadChange?: (event: CustomEvent) => void;
onThreadLoadStart?: () => void;
onThreadLoadEnd?: () => void;
}
Returned Methods
const {
control,
ref,
focusComposer,
setThreadId,
sendUserMessage,
setComposerValue,
fetchUpdates,
sendCustomAction,
showHistory,
hideHistory,
} = useChatKit(options);
Backend Endpoint Examples
FastAPI (Python)
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:
session = client.chatkit.sessions.create(
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:
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
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();
const session = await openai.chatkit.sessions.create({
model: 'gpt-4o',
});
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)
npm install -g ngrok
npm run dev
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
module.exports = {
async rewrites() {
return [
{
source: '/api/chatkit/:path*',
destination: 'https://api.openai.com/v1/chatkit/:path*',
},
];
},
};
Production Deployment
Environment Variables
NEXT_PUBLIC_CHATKIT_API_URL=https://api.yourdomain.com/chatkit
NEXT_PUBLIC_CHATKIT_DOMAIN_KEY=dk_xxxxxxxxxxxx
OPENAI_API_KEY=sk_xxxxxxxxxxxx
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
const { control } = useChatKit({
api: {
apiKey: process.env.OPENAI_API_KEY,
},
});
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
getClientSecret endpoint is accessible
Token Refresh Failures
async getClientSecret(existing) {
if (!existing) {
return await createNewSession();
}
try {
return await refreshSession(existing);
} catch (error) {
return await createNewSession();
}
}
CORS Issues
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