Skip to main content
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:
1

Frontend requests secret

Your app calls POST /navai/realtime/client-secret with optional session parameters
2

Backend mints secret

Your Express server calls OpenAI’s client secrets endpoint with your API key and returns the ephemeral token
3

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
req
CreateClientSecretRequest
Optional request parameters that override defaults

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:
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:
  1. Backend key always wins - If openaiApiKey is configured, it’s always used
  2. Request key as fallback - Only used if backend key is missing AND allowApiKeyFromRequest is true
  3. 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

Build docs developers (and LLMs) love