Client secrets are ephemeral tokens that allow frontend clients to establish secure WebRTC connections with OpenAI’s Realtime API without exposing your API key.
What are client secrets?
A client secret is a short-lived credential minted by OpenAI’s /v1/realtime/client_secrets endpoint. It allows a frontend application to:
Connect directly to OpenAI’s Realtime API via WebRTC
Establish voice sessions without knowing the server’s API key
Access only the permissions and session config you specify
Client secrets are ephemeral by design. They expire after a configurable TTL (time-to-live) between 10 seconds and 2 hours.
Why use client secrets?
Client secrets solve a critical security problem:
Without client secrets
Frontend needs your OpenAI API key
Key can be stolen from network traffic
Full API access exposed to users
No way to limit session duration
With client secrets
API key stays secure on backend
Frontend gets temporary token
Token expires automatically
Session config controlled by server
How it works
The flow involves three parties:
Frontend requests secret
Your app calls POST /navai/realtime/client-secret with optional session parameters
Backend mints secret
Your Express server calls OpenAI’s client secrets endpoint with your API key and returns the ephemeral token
Frontend connects
Your app uses the client secret to establish a WebRTC connection with OpenAI’s Realtime API
Creating client secrets
The createRealtimeClientSecret function handles the entire client secret lifecycle:
import { createRealtimeClientSecret } from "@navai/voice-backend" ;
const options = {
openaiApiKey: process . env . OPENAI_API_KEY ,
defaultModel: "gpt-realtime" ,
defaultVoice: "marin" ,
defaultInstructions: "You are a helpful assistant." ,
clientSecretTtlSeconds: 600
};
const secret = await createRealtimeClientSecret ( options , {
model: "gpt-realtime" ,
voice: "marin" ,
language: "Spanish"
});
console . log ( secret );
// {
// value: "ek_...",
// expires_at: 1730000000
// }
Function signature
From index.ts:160-205:
export async function createRealtimeClientSecret (
opts : NavaiVoiceBackendOptions ,
req ?: CreateClientSecretRequest
) : Promise < OpenAIRealtimeClientSecretResponse >
opts
NavaiVoiceBackendOptions
required
Backend configuration options Show NavaiVoiceBackendOptions properties
defaultModel
string
default: "gpt-realtime"
Default Realtime model
defaultInstructions
string
default: "You are a helpful assistant."
Default session instructions
Default response language
Client secret TTL (10-7200 seconds)
Allow clients to pass API key in request
req
CreateClientSecretRequest
Optional request parameters that override defaults Show CreateClientSecretRequest properties
Override model for this session
Override voice for this session
Override instructions for this session
Override language for this session
Override accent for this session
Override tone for this session
Client-provided API key (only if allowApiKeyFromRequest=true)
Return type
export type OpenAIRealtimeClientSecretResponse = {
value : string ; // The ephemeral client secret token
expires_at : number ; // Unix timestamp when token expires
session ?: unknown ; // Optional session metadata from OpenAI
};
TTL configuration
Client secret TTL (time-to-live) must be between 10 and 7200 seconds:
Short-lived (10 seconds)
Medium-lived (10 minutes)
Long-lived (2 hours)
const options = {
openaiApiKey: process . env . OPENAI_API_KEY ,
clientSecretTtlSeconds: 10 // Minimum
};
If you try to use a TTL outside the 10-7200 range, the function will throw: clientSecretTtlSeconds must be between 10 and 7200. Received: {value}
Validation happens in index.ts:71-83:
function validateOptions ( opts : NavaiVoiceBackendOptions ) : void {
const hasBackendApiKey = Boolean ( opts . openaiApiKey ?. trim ());
if ( ! hasBackendApiKey && ! opts . allowApiKeyFromRequest ) {
throw new Error ( "Missing openaiApiKey in NavaiVoiceBackendOptions." );
}
const ttl = opts . clientSecretTtlSeconds ?? 600 ;
if ( ttl < MIN_TTL_SECONDS || ttl > MAX_TTL_SECONDS ) {
throw new Error (
`clientSecretTtlSeconds must be between ${ MIN_TTL_SECONDS } and ${ MAX_TTL_SECONDS } . Received: ${ ttl } `
);
}
}
Instructions building
The createRealtimeClientSecret function automatically builds session instructions by combining base instructions with language/accent/tone parameters.
From index.ts:134-158:
function buildSessionInstructions ( input : {
baseInstructions : string ;
language ?: string ;
voiceAccent ?: string ;
voiceTone ?: string ;
}) : string {
const lines = [ input . baseInstructions . trim ()];
const language = readOptional ( input . language );
const voiceAccent = readOptional ( input . voiceAccent );
const voiceTone = readOptional ( input . voiceTone );
if ( language ) {
lines . push ( `Always reply in ${ language } .` );
}
if ( voiceAccent ) {
lines . push ( `Use a ${ voiceAccent } accent while speaking.` );
}
if ( voiceTone ) {
lines . push ( `Use a ${ voiceTone } tone while speaking.` );
}
return lines . join ( " \n " );
}
Example
Given these parameters:
const request = {
instructions: "You are a navigation assistant." ,
language: "Spanish" ,
voiceAccent: "neutral Latin American Spanish" ,
voiceTone: "friendly and professional"
};
The final instructions sent to OpenAI will be:
You are a navigation assistant.
Always reply in Spanish.
Use a neutral Latin American Spanish accent while speaking.
Use a friendly and professional tone while speaking.
Express handler
The createExpressClientSecretHandler function creates an Express route handler:
import { createExpressClientSecretHandler } from "@navai/voice-backend" ;
const options = {
openaiApiKey: process . env . OPENAI_API_KEY ,
clientSecretTtlSeconds: 600
};
app . post ( "/api/voice/secret" , createExpressClientSecretHandler ( options ));
From index.ts:207-219:
export function createExpressClientSecretHandler ( opts : NavaiVoiceBackendOptions ) {
validateOptions ( opts );
return async ( req : Request , res : Response , next : NextFunction ) => {
try {
const input = req . body as CreateClientSecretRequest | undefined ;
const data = await createRealtimeClientSecret ( opts , input );
res . json ({ value: data . value , expires_at: data . expires_at });
} catch ( error ) {
next ( error );
}
};
}
The handler automatically passes errors to Express’s next() function, so you can use standard Express error middleware.
Security considerations
API key policy
The backend enforces a strict priority for API keys:
Backend key always wins - If openaiApiKey is configured, it’s always used
Request key as fallback - Only used if backend key is missing AND allowApiKeyFromRequest is true
Secure by default - Request keys are rejected when backend key exists (unless explicitly allowed)
// ✅ Secure: Backend key always used
const options = {
openaiApiKey: process . env . OPENAI_API_KEY ,
allowApiKeyFromRequest: false // Default
};
// ⚠️ Development only: Allow client keys as fallback
const options = {
openaiApiKey: undefined ,
allowApiKeyFromRequest: true
};
// ❌ Insecure: Never do this in production
const options = {
openaiApiKey: undefined ,
allowApiKeyFromRequest: false // Will throw error
};
Production best practices
Follow these security practices:
Always set OPENAI_API_KEY on the backend in production
Never set NAVAI_ALLOW_FRONTEND_API_KEY=true in production
Use HTTPS for all client secret requests
Set appropriate CORS origins to prevent unauthorized access
Monitor for unusual client secret request patterns
Consider implementing rate limiting on the client secret endpoint
Use shorter TTLs (e.g., 300-600 seconds) for better security
Rotate your OpenAI API key periodically
Environment-based configuration
You can load backend options from environment variables using getNavaiVoiceBackendOptionsFromEnv:
import { getNavaiVoiceBackendOptionsFromEnv } from "@navai/voice-backend" ;
const options = getNavaiVoiceBackendOptionsFromEnv ();
// Reads from process.env:
// - OPENAI_API_KEY
// - OPENAI_REALTIME_MODEL
// - OPENAI_REALTIME_VOICE
// - OPENAI_REALTIME_INSTRUCTIONS
// - OPENAI_REALTIME_LANGUAGE
// - OPENAI_REALTIME_VOICE_ACCENT
// - OPENAI_REALTIME_VOICE_TONE
// - OPENAI_REALTIME_CLIENT_SECRET_TTL
// - NAVAI_ALLOW_FRONTEND_API_KEY
const secret = await createRealtimeClientSecret ( options );
From index.ts:114-132:
export function getNavaiVoiceBackendOptionsFromEnv (
env : NavaiBackendEnv = process . env as NavaiBackendEnv
) : NavaiVoiceBackendOptions {
const hasBackendApiKey = Boolean ( env . OPENAI_API_KEY ?. trim ());
const allowFrontendApiKeyFromEnv = ( env . NAVAI_ALLOW_FRONTEND_API_KEY ?? "false" ). toLowerCase () === "true" ;
const allowFrontendApiKey = allowFrontendApiKeyFromEnv || ! hasBackendApiKey ;
return {
openaiApiKey: env . OPENAI_API_KEY ,
defaultModel: env . OPENAI_REALTIME_MODEL ,
defaultVoice: env . OPENAI_REALTIME_VOICE ,
defaultInstructions: env . OPENAI_REALTIME_INSTRUCTIONS ,
defaultLanguage: env . OPENAI_REALTIME_LANGUAGE ,
defaultVoiceAccent: env . OPENAI_REALTIME_VOICE_ACCENT ,
defaultVoiceTone: env . OPENAI_REALTIME_VOICE_TONE ,
clientSecretTtlSeconds: Number ( env . OPENAI_REALTIME_CLIENT_SECRET_TTL ?? "600" ),
allowApiKeyFromRequest: allowFrontendApiKey
};
}
Common errors
Here are the most common errors and how to fix them:
Missing API key
Missing openaiApiKey in NavaiVoiceBackendOptions.
Solution: Set OPENAI_API_KEY environment variable or pass openaiApiKey in options.
Request API key disabled
Passing apiKey from request is disabled. Set allowApiKeyFromRequest=true to enable it.
Solution: Either configure backend API key or set allowApiKeyFromRequest: true (development only).
Invalid TTL
clientSecretTtlSeconds must be between 10 and 7200. Received: 5
Solution: Use a TTL between 10 and 7200 seconds.
OpenAI API error
OpenAI client_secrets failed (401): Incorrect API key provided
Solution: Verify your OPENAI_API_KEY is valid and has access to the Realtime API.
Next steps
Functions Learn how to create backend functions that the AI agent can invoke