Skip to main content
While @navai/voice-backend provides first-class Express support, you can implement the NAVAI HTTP contract in any backend framework.

Supported frameworks

The NAVAI backend protocol is framework-agnostic. Here are examples for popular frameworks:

Express

First-class support with @navai/voice-backend package

Laravel

PHP implementation using Laravel routes and controllers

Django

Python implementation using Django views and REST framework

Ruby on Rails

Ruby implementation using Rails controllers and routes
You can implement NAVAI backend in any language or framework that can make HTTP requests and serve JSON responses.

HTTP contract

The NAVAI backend must implement three endpoints:

1. POST /navai/realtime/client-secret

Mints ephemeral client secrets by proxying to OpenAI’s Realtime API. Request headers:
  • Content-Type: application/json
Request body:
{
  "model": "gpt-realtime",
  "voice": "marin",
  "instructions": "You are a helpful assistant.",
  "language": "Spanish",
  "voiceAccent": "neutral Latin American Spanish",
  "voiceTone": "friendly and professional",
  "apiKey": "sk-..."
}
model
string
OpenAI Realtime model (overrides default)
voice
string
Voice to use (overrides default)
instructions
string
Session instructions (overrides default)
language
string
Response language (appended to instructions)
voiceAccent
string
Voice accent (appended to instructions)
voiceTone
string
Voice tone (appended to instructions)
apiKey
string
Client-provided API key (only if allowed by backend)
Response (200):
{
  "value": "ek_...",
  "expires_at": 1730000000
}
Error responses:
// 400: Invalid TTL
{
  "error": "clientSecretTtlSeconds must be between 10 and 7200. Received: 5"
}

// 401: Missing/invalid API key
{
  "error": "Missing API key. Configure openaiApiKey or send apiKey in request."
}

// 403: Request API key disabled
{
  "error": "Passing apiKey from request is disabled. Set allowApiKeyFromRequest=true to enable it."
}

2. GET /navai/functions

Lists all available backend functions. Response (200):
{
  "items": [
    {
      "name": "get_weather",
      "description": "Call exported function getWeather.",
      "source": "src/ai/functions-modules/weather.ts#getWeather"
    },
    {
      "name": "send_email",
      "description": "Call exported function sendEmail.",
      "source": "src/ai/functions-modules/email.ts#sendEmail"
    }
  ],
  "warnings": [
    "[navai] Renamed duplicated function \"get_data\" to \"get_data_2\"."
  ]
}

3. POST /navai/functions/execute

Executes a backend function by name. Request headers:
  • Content-Type: application/json
Request body:
{
  "function_name": "get_weather",
  "payload": {
    "args": ["San Francisco"]
  }
}
function_name
string
required
Name of the function to execute (normalized snake_case)
payload
object
Arguments and data for the function
Success response (200):
{
  "ok": true,
  "function_name": "get_weather",
  "source": "src/ai/functions-modules/weather.ts#getWeather",
  "result": {
    "temperature": 72,
    "conditions": "sunny"
  }
}
Error responses:
// 400: Missing function name
{
  "error": "function_name is required."
}

// 404: Unknown function
{
  "error": "Unknown or disallowed function.",
  "available_functions": ["get_weather", "send_email"]
}

// 500: Runtime error
{
  "error": "Internal server error"
}

OpenAI client secret endpoint

All implementations must call OpenAI’s client secret API: Endpoint:
POST https://api.openai.com/v1/realtime/client_secrets
Headers:
Authorization: Bearer {your_openai_api_key}
Content-Type: application/json
Request body:
{
  "expires_after": {
    "anchor": "created_at",
    "seconds": 600
  },
  "session": {
    "type": "realtime",
    "model": "gpt-realtime",
    "instructions": "You are a helpful assistant.\nAlways reply in Spanish.",
    "audio": {
      "output": {
        "voice": "marin"
      }
    }
  }
}
Response:
{
  "value": "ek_...",
  "expires_at": 1730000000,
  "session": { ... }
}
From the Express implementation (index.ts:160-205):
const body = {
  expires_after: { anchor: "created_at", seconds: ttl },
  session: {
    type: "realtime",
    model,
    instructions,
    audio: {
      output: { voice }
    }
  }
};

const response = await fetch(OPENAI_CLIENT_SECRETS_URL, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify(body)
});

Laravel implementation

Here’s a complete Laravel implementation:
1

Install dependencies

composer require guzzlehttp/guzzle
2

Create routes

Add to routes/api.php:
use App\Http\Controllers\NavaiController;

Route::post('/navai/realtime/client-secret', [NavaiController::class, 'clientSecret']);
Route::get('/navai/functions', [NavaiController::class, 'listFunctions']);
Route::post('/navai/functions/execute', [NavaiController::class, 'executeFunction']);
3

Create controller

Create app/Http/Controllers/NavaiController.php:
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class NavaiController extends Controller
{
    private const OPENAI_CLIENT_SECRETS_URL = 'https://api.openai.com/v1/realtime/client_secrets';
    private const MIN_TTL_SECONDS = 10;
    private const MAX_TTL_SECONDS = 7200;

    public function clientSecret(Request $request)
    {
        $apiKey = $this->resolveApiKey($request);
        $ttl = (int) env('OPENAI_REALTIME_CLIENT_SECRET_TTL', 600);

        if ($ttl < self::MIN_TTL_SECONDS || $ttl > self::MAX_TTL_SECONDS) {
            return response()->json([
                'error' => "clientSecretTtlSeconds must be between " . self::MIN_TTL_SECONDS . " and " . self::MAX_TTL_SECONDS
            ], 400);
        }

        $model = $request->input('model', env('OPENAI_REALTIME_MODEL', 'gpt-realtime'));
        $voice = $request->input('voice', env('OPENAI_REALTIME_VOICE', 'marin'));
        $instructions = $this->buildInstructions($request);

        $response = Http::withHeaders([
            'Authorization' => 'Bearer ' . $apiKey,
            'Content-Type' => 'application/json'
        ])->post(self::OPENAI_CLIENT_SECRETS_URL, [
            'expires_after' => [
                'anchor' => 'created_at',
                'seconds' => $ttl
            ],
            'session' => [
                'type' => 'realtime',
                'model' => $model,
                'instructions' => $instructions,
                'audio' => [
                    'output' => ['voice' => $voice]
                ]
            ]
        ]);

        if (!$response->successful()) {
            return response()->json([
                'error' => 'OpenAI client_secrets failed: ' . $response->body()
            ], $response->status());
        }

        $data = $response->json();
        return response()->json([
            'value' => $data['value'],
            'expires_at' => $data['expires_at']
        ]);
    }

    public function listFunctions()
    {
        // Implement your function discovery logic
        $functions = $this->discoverFunctions();
        
        return response()->json([
            'items' => $functions,
            'warnings' => []
        ]);
    }

    public function executeFunction(Request $request)
    {
        $functionName = $request->input('function_name');
        $payload = $request->input('payload', []);

        if (empty($functionName)) {
            return response()->json(['error' => 'function_name is required.'], 400);
        }

        // Implement your function execution logic
        $result = $this->runFunction($functionName, $payload, $request);

        if ($result === null) {
            return response()->json([
                'error' => 'Unknown or disallowed function.',
                'available_functions' => array_column($this->discoverFunctions(), 'name')
            ], 404);
        }

        return response()->json([
            'ok' => true,
            'function_name' => $functionName,
            'source' => 'php',
            'result' => $result
        ]);
    }

    private function resolveApiKey(Request $request): string
    {
        $backendKey = env('OPENAI_API_KEY');
        if (!empty($backendKey)) {
            return $backendKey;
        }

        $requestKey = $request->input('apiKey');
        $allowRequestKey = env('NAVAI_ALLOW_FRONTEND_API_KEY', false);

        if (!empty($requestKey) && $allowRequestKey) {
            return $requestKey;
        }

        abort(401, 'Missing API key.');
    }

    private function buildInstructions(Request $request): string
    {
        $lines = [
            $request->input('instructions', env('OPENAI_REALTIME_INSTRUCTIONS', 'You are a helpful assistant.'))
        ];

        $language = $request->input('language', env('OPENAI_REALTIME_LANGUAGE'));
        if (!empty($language)) {
            $lines[] = "Always reply in {$language}.";
        }

        $accent = $request->input('voiceAccent', env('OPENAI_REALTIME_VOICE_ACCENT'));
        if (!empty($accent)) {
            $lines[] = "Use a {$accent} accent while speaking.";
        }

        $tone = $request->input('voiceTone', env('OPENAI_REALTIME_VOICE_TONE'));
        if (!empty($tone)) {
            $lines[] = "Use a {$tone} tone while speaking.";
        }

        return implode("\n", $lines);
    }

    private function discoverFunctions(): array
    {
        // Implement your function discovery
        return [
            [
                'name' => 'get_weather',
                'description' => 'Get weather for a location',
                'source' => 'app/Navai/Functions/Weather.php'
            ]
        ];
    }

    private function runFunction(string $name, array $payload, Request $request)
    {
        // Implement your function execution
        return match($name) {
            'get_weather' => $this->getWeather($payload['args'][0] ?? null),
            default => null
        };
    }

    private function getWeather(?string $location)
    {
        if (empty($location)) {
            return ['error' => 'Location required'];
        }
        return ['temperature' => 72, 'conditions' => 'sunny', 'location' => $location];
    }
}
4

Configure environment

Add to .env:
OPENAI_API_KEY=sk-...
OPENAI_REALTIME_MODEL=gpt-realtime
OPENAI_REALTIME_VOICE=marin
OPENAI_REALTIME_INSTRUCTIONS="You are a helpful assistant."
OPENAI_REALTIME_CLIENT_SECRET_TTL=600
NAVAI_ALLOW_FRONTEND_API_KEY=false

Django implementation

Here’s a Django implementation:
1

Create views

Create navai/views.py:
import os
import requests
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
import json

OPENAI_CLIENT_SECRETS_URL = "https://api.openai.com/v1/realtime/client_secrets"
MIN_TTL_SECONDS = 10
MAX_TTL_SECONDS = 7200

def resolve_api_key(request_data):
    backend_key = os.getenv('OPENAI_API_KEY', '').strip()
    if backend_key:
        return backend_key
    
    request_key = request_data.get('apiKey', '').strip()
    allow_request_key = os.getenv('NAVAI_ALLOW_FRONTEND_API_KEY', 'false').lower() == 'true'
    
    if request_key and allow_request_key:
        return request_key
    
    raise ValueError('Missing API key')

def build_instructions(request_data):
    lines = [
        request_data.get('instructions', os.getenv('OPENAI_REALTIME_INSTRUCTIONS', 'You are a helpful assistant.'))
    ]
    
    language = request_data.get('language', os.getenv('OPENAI_REALTIME_LANGUAGE', ''))
    if language:
        lines.append(f"Always reply in {language}.")
    
    accent = request_data.get('voiceAccent', os.getenv('OPENAI_REALTIME_VOICE_ACCENT', ''))
    if accent:
        lines.append(f"Use a {accent} accent while speaking.")
    
    tone = request_data.get('voiceTone', os.getenv('OPENAI_REALTIME_VOICE_TONE', ''))
    if tone:
        lines.append(f"Use a {tone} tone while speaking.")
    
    return "\n".join(lines)

@csrf_exempt
@require_http_methods(["POST"])
def client_secret(request):
    try:
        request_data = json.loads(request.body)
    except json.JSONDecodeError:
        request_data = {}
    
    try:
        api_key = resolve_api_key(request_data)
    except ValueError as e:
        return JsonResponse({'error': str(e)}, status=401)
    
    ttl = int(os.getenv('OPENAI_REALTIME_CLIENT_SECRET_TTL', 600))
    if ttl < MIN_TTL_SECONDS or ttl > MAX_TTL_SECONDS:
        return JsonResponse({
            'error': f'clientSecretTtlSeconds must be between {MIN_TTL_SECONDS} and {MAX_TTL_SECONDS}'
        }, status=400)
    
    model = request_data.get('model', os.getenv('OPENAI_REALTIME_MODEL', 'gpt-realtime'))
    voice = request_data.get('voice', os.getenv('OPENAI_REALTIME_VOICE', 'marin'))
    instructions = build_instructions(request_data)
    
    response = requests.post(
        OPENAI_CLIENT_SECRETS_URL,
        headers={
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json'
        },
        json={
            'expires_after': {'anchor': 'created_at', 'seconds': ttl},
            'session': {
                'type': 'realtime',
                'model': model,
                'instructions': instructions,
                'audio': {'output': {'voice': voice}}
            }
        }
    )
    
    if not response.ok:
        return JsonResponse({
            'error': f'OpenAI client_secrets failed: {response.text}'
        }, status=response.status_code)
    
    data = response.json()
    return JsonResponse({
        'value': data['value'],
        'expires_at': data['expires_at']
    })

@require_http_methods(["GET"])
def list_functions(request):
    # Implement your function discovery
    functions = [
        {
            'name': 'get_weather',
            'description': 'Get weather for a location',
            'source': 'navai/functions/weather.py'
        }
    ]
    return JsonResponse({'items': functions, 'warnings': []})

@csrf_exempt
@require_http_methods(["POST"])
def execute_function(request):
    try:
        request_data = json.loads(request.body)
    except json.JSONDecodeError:
        return JsonResponse({'error': 'Invalid JSON'}, status=400)
    
    function_name = request_data.get('function_name', '').strip()
    if not function_name:
        return JsonResponse({'error': 'function_name is required.'}, status=400)
    
    payload = request_data.get('payload', {})
    
    # Implement your function execution
    if function_name == 'get_weather':
        location = payload.get('args', [None])[0]
        result = {'temperature': 72, 'conditions': 'sunny', 'location': location}
        return JsonResponse({
            'ok': True,
            'function_name': function_name,
            'source': 'python',
            'result': result
        })
    
    return JsonResponse({
        'error': 'Unknown or disallowed function.',
        'available_functions': ['get_weather']
    }, status=404)
2

Add routes

Add to urls.py:
from django.urls import path
from navai import views

urlpatterns = [
    path('navai/realtime/client-secret', views.client_secret),
    path('navai/functions', views.list_functions),
    path('navai/functions/execute', views.execute_function),
]

Rails implementation

Here’s a Ruby on Rails implementation:
1

Create controller

Create app/controllers/navai_controller.rb:
require 'net/http'
require 'json'

class NavaiController < ApplicationController
  skip_before_action :verify_authenticity_token

  OPENAI_CLIENT_SECRETS_URL = 'https://api.openai.com/v1/realtime/client_secrets'.freeze
  MIN_TTL_SECONDS = 10
  MAX_TTL_SECONDS = 7200

  def client_secret
    api_key = resolve_api_key
    ttl = (ENV['OPENAI_REALTIME_CLIENT_SECRET_TTL'] || '600').to_i

    if ttl < MIN_TTL_SECONDS || ttl > MAX_TTL_SECONDS
      return render json: {
        error: "clientSecretTtlSeconds must be between #{MIN_TTL_SECONDS} and #{MAX_TTL_SECONDS}"
      }, status: :bad_request
    end

    model = params[:model] || ENV['OPENAI_REALTIME_MODEL'] || 'gpt-realtime'
    voice = params[:voice] || ENV['OPENAI_REALTIME_VOICE'] || 'marin'
    instructions = build_instructions

    uri = URI(OPENAI_CLIENT_SECRETS_URL)
    request = Net::HTTP::Post.new(uri)
    request['Authorization'] = "Bearer #{api_key}"
    request['Content-Type'] = 'application/json'
    request.body = {
      expires_after: { anchor: 'created_at', seconds: ttl },
      session: {
        type: 'realtime',
        model: model,
        instructions: instructions,
        audio: { output: { voice: voice } }
      }
    }.to_json

    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
      http.request(request)
    end

    unless response.is_a?(Net::HTTPSuccess)
      return render json: {
        error: "OpenAI client_secrets failed: #{response.body}"
      }, status: response.code.to_i
    end

    data = JSON.parse(response.body)
    render json: { value: data['value'], expires_at: data['expires_at'] }
  rescue StandardError => e
    render json: { error: e.message }, status: :internal_server_error
  end

  def list_functions
    functions = discover_functions
    render json: { items: functions, warnings: [] }
  end

  def execute_function
    function_name = params[:function_name]&.strip
    unless function_name
      return render json: { error: 'function_name is required.' }, status: :bad_request
    end

    payload = params[:payload] || {}
    result = run_function(function_name, payload)

    if result.nil?
      return render json: {
        error: 'Unknown or disallowed function.',
        available_functions: discover_functions.map { |f| f[:name] }
      }, status: :not_found
    end

    render json: {
      ok: true,
      function_name: function_name,
      source: 'ruby',
      result: result
    }
  end

  private

  def resolve_api_key
    backend_key = ENV['OPENAI_API_KEY']&.strip
    return backend_key if backend_key.present?

    request_key = params[:apiKey]&.strip
    allow_request_key = ENV['NAVAI_ALLOW_FRONTEND_API_KEY']&.downcase == 'true'

    if request_key.present? && allow_request_key
      return request_key
    end

    raise 'Missing API key'
  end

  def build_instructions
    lines = [
      params[:instructions] || ENV['OPENAI_REALTIME_INSTRUCTIONS'] || 'You are a helpful assistant.'
    ]

    language = params[:language] || ENV['OPENAI_REALTIME_LANGUAGE']
    lines << "Always reply in #{language}." if language.present?

    accent = params[:voiceAccent] || ENV['OPENAI_REALTIME_VOICE_ACCENT']
    lines << "Use a #{accent} accent while speaking." if accent.present?

    tone = params[:voiceTone] || ENV['OPENAI_REALTIME_VOICE_TONE']
    lines << "Use a #{tone} tone while speaking." if tone.present?

    lines.join("\n")
  end

  def discover_functions
    [
      {
        name: 'get_weather',
        description: 'Get weather for a location',
        source: 'app/navai/functions/weather.rb'
      }
    ]
  end

  def run_function(name, payload)
    case name
    when 'get_weather'
      location = payload.dig('args', 0)
      { temperature: 72, conditions: 'sunny', location: location }
    else
      nil
    end
  end
end
2

Add routes

Add to config/routes.rb:
Rails.application.routes.draw do
  post 'navai/realtime/client-secret', to: 'navai#client_secret'
  get 'navai/functions', to: 'navai#list_functions'
  post 'navai/functions/execute', to: 'navai#execute_function'
end

Testing your implementation

Use these curl commands to test your backend:
curl -X POST http://localhost:3000/navai/realtime/client-secret \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-realtime",
    "voice": "marin",
    "language": "Spanish"
  }'

Security checklist

Ensure your custom implementation follows these security practices:
  • Store API key in environment variables, never hardcode
  • Validate TTL is between 10 and 7200 seconds
  • Implement API key priority: backend key > request key (if allowed)
  • Reject request API keys when backend key exists (unless explicitly allowed)
  • Use HTTPS in production
  • Implement CORS with specific origin whitelist
  • Validate all input parameters
  • Sanitize function names to prevent injection
  • Implement rate limiting on client secret endpoint
  • Log all function executions for audit trails
  • Add authentication/authorization if needed
  • Handle OpenAI API errors gracefully

Next steps

Express setup

See the complete Express implementation for reference

Client secrets

Deep dive into client secret generation and security

Functions

Learn about the backend function system in detail

Frontend

Connect your custom backend to NAVAI frontend

Build docs developers (and LLMs) love