Secure API Calls
Updated
On this page
Your Xirsys identifier and secret authorize privileged API operations. Keep them in a trusted backend and expose only narrow application endpoints that return temporary TURN or signaling values.
XIRSYS_SECRETglobal.xirsys.netDo not build a generic pass-through proxy that accepts an upstream path from the client. Define fixed routes, authenticate your own user, authorize the requested channel, validate inputs, rate-limit issuance, and redact credentials from logs.
Store Secrets on the Server
Use a deployment secret manager or protected environment variables:
XIRSYS_IDENT=your-account-ident
XIRSYS_SECRET=your-api-secret
XIRSYS_CHANNEL=your-channelDo not commit an .env file, print the Basic authorization header, include credentials in a URL, or return the Xirsys secret to a client. Rotate the secret immediately if it is exposed.
Node.js 18+ Backend Pattern
The following excerpts use Express and the built-in Node.js fetch. The application-specific authentication middleware must populate request.user. If geo routing is enabled, trusted proxy middleware must separately populate request.authenticatedClientIp from a verified network boundary.
const express = require('express');
const net = require('node:net');
const app = express();
const baseUrl = 'https://global.xirsys.net';
const { XIRSYS_IDENT, XIRSYS_SECRET, XIRSYS_CHANNEL } = process.env;
if (!XIRSYS_IDENT || !XIRSYS_SECRET || !XIRSYS_CHANNEL) {
throw new Error('Missing Xirsys server configuration');
}
const authorization = `Basic ${Buffer.from(
`${XIRSYS_IDENT}:${XIRSYS_SECRET}`
).toString('base64')}`;
const encodedChannel = encodeURIComponent(XIRSYS_CHANNEL);
async function xirsysValue(response, operation) {
const data = await response.json();
if (!response.ok || data.s !== 'ok') {
throw new Error(`${operation} failed (${response.status})`);
}
return data.v;
}
function trustedPublicIp(request) {
const candidate = request.authenticatedClientIp;
return typeof candidate === 'string' && net.isIP(candidate)
? candidate
: null;
}
function requireAppUser(request, response, next) {
// Replace with your session/JWT authentication and authorization.
if (!request.user) return response.sendStatus(401);
next();
}
app.use(requireAppUser);Do not trust X-Forwarded-For by default. Configure your framework with the exact proxies you operate, discard untrusted forwarded headers at the network edge, and normalize IPv4/IPv6 before treating an address as authenticated context.
Fixed TURN Route
app.get('/api/ice', async (request, response) => {
try {
const userIp = trustedPublicIp(request);
const query = new URLSearchParams({ expire: '60', webrtc: '1' });
const body = {};
if (userIp) {
query.set('geo', '1');
body.user_ip = userIp;
}
const upstream = await fetch(
`${baseUrl}/_turn/${encodedChannel}?${query}`,
{
method: 'PUT',
headers: {
Authorization: authorization,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
}
);
const value = await xirsysValue(upstream, 'TURN request');
response.set('Cache-Control', 'no-store');
response.json({ iceServers: value.iceServers });
} catch (error) {
console.error(error.message);
response.status(502).json({ error: 'Unable to obtain ICE servers' });
}
});The webrtc=1 flag returns v.iceServers in the standard WebRTC array schema, so it can be passed directly to RTCPeerConnection without conversion.
When no trusted public IP is available, this route makes a normal TURN request. It does not accept user_ip from a query parameter or request body.
End-user IP privacy: The user_ip value is strictly transient. Xirsys uses it only in memory to select a TURN region for the current request, then discards it. It is not persisted, stored, cached, or logged anywhere within the Xirsys service, and it is not included in forwarded request bodies.
Fixed Signaling Route
Assign the peer ID from authenticated application state instead of accepting an arbitrary identity from the client:
app.get('/api/signal-config', async (request, response) => {
const peerId = String(request.user.id);
if (!/^[A-Za-z0-9-]{1,64}$/.test(peerId)) {
return response.status(400).json({ error: 'Invalid peer identity' });
}
try {
const headers = { Authorization: authorization };
const [hostResponse, tokenResponse] = await Promise.all([
fetch(`${baseUrl}/_host?type=signal`, { headers }),
fetch(
`${baseUrl}/_token/${encodedChannel}` +
`?k=${encodeURIComponent(peerId)}&expire=120`,
{ method: 'PUT', headers }
)
]);
const [host, token] = await Promise.all([
xirsysValue(hostResponse, 'Host request'),
xirsysValue(tokenResponse, 'Token request')
]);
response.set('Cache-Control', 'no-store');
response.json({ host, token });
} catch (error) {
console.error(error.message);
response.status(502).json({ error: 'Unable to configure signaling' });
}
});The browser connects to ${host}/v2/${encodeURIComponent(token)}. On reconnect, it should call this backend route again rather than reuse stale configuration.
Production Checklist
- Serve the application and API over HTTPS with a certificate trusted by clients.
- Authenticate users before issuing ICE credentials or signaling tokens.
- Authorize channel membership and assign peer IDs on the server.
- Apply per-user and per-IP rate limits and monitor issuance failures.
- Set
Cache-Control: no-storeon responses containing temporary credentials. - Use short expiration periods and retrieve fresh values for new connections.
- Redact Basic auth, TURN credentials, signaling tokens, and SDP from routine logs.
- Configure trusted proxy handling explicitly before using a forwarded client IP.
- Keep development, staging, and production channels and secrets separate.
Continue with the Quick Start for a runnable local example, then review the detailed TURN and Signaling references.