Trusted Server Connections
This recipe walks through the full trusted-server workflow: minting a token, connecting from a backend service, and handling server-side tool calls.
Before You Start
Make sure you have:
- A running self-hosted stack (see Self-Hosted)
- An API key with the
mint_trusted_sessionscope - A backend service that can open WebSocket connections
For an overview of all connection patterns, see Connection Paradigms.
What Is a Trusted Server Connection?
A trusted server connection lets your backend participate directly in a voice session. Instead of a browser client, your backend opens the WebSocket, sends audio or text, and receives responses. The realtime engine can also forward tool calls to your backend for server-side execution.
This is useful when:
- you need to drive a voice session from code (automation, testing, orchestration)
- your tools require server-side access (databases, internal APIs, order systems)
- you want to run voice workflows without a browser in the loop
Key Concepts
serviceId
A unique string that identifies your backend service. The engine logs it, uses it for analytics, and includes it in tool-call routing. Pick something descriptive, like order-service or ci-test-runner.
serverTools
Tool definitions you declare at token-issuance time. Each tool has a name, description, and JSON Schema parameters. When the LLM decides to call one of these tools, the engine forwards the call to your backend over the WebSocket instead of trying to execute it client-side.
Scope Gating
Trusted-server tokens require the mint_trusted_session scope on the API key. This is a separate scope from mint_ephemeral (used for browser tokens). A key without the right scope will get a 403 response.
Step-by-Step
1. Create an API Key with the Right Scope
Use the Core API or admin UI to create a fixed API key. The key must carry the mint_trusted_session scope.
curl -X POST https://your-core-host/api/api-keys \
-H "Authorization: Bearer ${ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"appId": "your-app-id",
"scopes": ["mint_trusted_session"],
"label": "Trusted server key"
}'Save the returned key. It will look like vkey_....
2. Mint a Token
Your backend calls the token endpoint with connectionType: 'trusted_server'.
const tokenResponse = await fetch('https://your-core-host/v1/realtime/sessions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
connectionType: 'trusted_server',
config: {
serviceId: 'my-service',
provider: 'vowel-prime',
voiceConfig: {
model: 'openai/gpt-oss-120b',
voice: 'Ashley',
},
serverTools: [
{
name: 'lookupOrder',
description: 'Look up an order by ID',
parameters: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'Order ID' },
},
required: ['orderId'],
},
},
],
},
}),
});
const { token, expiresAt } = await tokenResponse.json();The token expires after five minutes. Mint a fresh one for each session.
3. Connect via WebSocket
Open a WebSocket to the realtime engine using the token.
import WebSocket from 'ws';
const ws = new WebSocket('wss://your-engine-host/v1/realtime', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
ws.on('open', () => {
console.log('Connected to realtime engine');
});
ws.on('message', (data) => {
const event = JSON.parse(data.toString());
handleEvent(event);
});4. Handle Server Tool Calls
When the LLM calls a server tool, the engine emits a response.output_item.added event with a function_call item. Your backend executes the tool and sends the result back.
function handleEvent(event: any) {
if (event.type === 'response.output_item.added' && event.item?.type === 'function_call') {
const { call_id, name, arguments: argsJson } = event.item;
const args = JSON.parse(argsJson || '{}');
if (name === 'lookupOrder') {
const result = lookupOrder(args.orderId);
ws.send(JSON.stringify({
type: 'conversation.item.create',
item: {
type: 'function_call_output',
call_id,
output: JSON.stringify(result),
},
}));
}
}
}5. Send Text or Audio
You can drive the conversation by sending text messages or audio buffers.
// Send a text message
ws.send(JSON.stringify({
type: 'conversation.item.create',
item: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'What is the status of order 12345?' }],
},
}));
// Request a response
ws.send(JSON.stringify({ type: 'response.create' }));Security Checklist
- Never expose trusted-server tokens in browser code. They are for backend use only.
- Use separate API keys for trusted-server and browser token minting. The scope separation exists for a reason.
- Rotate keys periodically. If a key is compromised, revoke it and create a new one.
- Validate tool inputs. The engine forwards tool calls as-is. Your backend should validate arguments before executing.
- Log session lifecycle. Track token minting, WebSocket connections, and tool-call results for debugging and auditing.
Troubleshooting
403 Forbidden
The API key does not carry the mint_trusted_session scope. Create a new key with the correct scope.
Token Expired
Ephemeral tokens last five minutes. If your backend takes too long between minting and connecting, mint a fresh token.
Tool Calls Not Arriving
- Verify the tool name in
serverToolsmatches exactly what the LLM sees in its tool list. - Check engine logs for tool-registration messages.
- Make sure the tool is not also registered as a client tool (client tools take priority).
WebSocket Disconnects
- Check the
maxCallDurationMsandmaxIdleDurationMstoken claims. Sessions end when either limit is hit. - Look for error events on the WebSocket. The engine sends
errorevents before closing.
Reference Implementation
If you already use a server-issued token flow for browser clients, the token-minting step is almost identical for trusted-server connections. The main difference is that your backend keeps the WebSocket open itself and handles forwarded tool calls directly.