Docs

WebSocket Signaling

Updated

On this page

Xirsys signaling carries application messages such as WebRTC offers, answers, and ICE candidates between peers on the same channel. Your application still owns the WebRTC state machine, peer roles, authorization, and message payloads.

A signaling connection requires two temporary values from your backend: a selected WebSocket host and a peer token. Do not put your Xirsys account secret in client code.

1. Select a Signaling Host

GET/_host?type=signal

Call GET /_host?type=signal with HTTP Basic authentication. The endpoint selects a healthy signaling API host; it does not use a channel or peer k parameter.

RequestShell
curl --user "$XIRSYS_IDENT:$XIRSYS_SECRET" \
  "https://global.xirsys.net/_host?type=signal"
ResponseJSON
{
  "s": "ok",
  "v": "wss://signaling-host.example:443/ws"
}

Treat v as the complete WebSocket base URL. Do not reconstruct its scheme, port, or /ws path.

2. Create a Peer Token

PUT/_token/{channel}?k={peer-id}&expire={seconds}

Call PUT /_token/{channel}?k={peer-id}&expire={seconds}. The token authorizes one peer ID on the specified channel.

RequestShell
curl --request PUT \
  --user "$XIRSYS_IDENT:$XIRSYS_SECRET" \
  "https://global.xirsys.net/_token/myChannel?k=peer-123&expire=120"
ResponseJSON
{
  "s": "ok",
  "v": "eyJhbGciOi..."
}

The default token lifetime is 60 seconds. expire=0 creates a long-lived but still finite token; it does not mean literal non-expiration. Prefer short-lived tokens and create a fresh token for a reconnect. The peer ID must be unique among peers connected to that channel at the same time.

3. Connect

WSS{host}/v2/{token}

Append /v2/{token} to the host returned by /_host and wait for the WebSocket open event before sending.

JavaScriptJS
const socket = new WebSocket(
  `${signalingHost}/v2/${encodeURIComponent(token)}`
);

socket.addEventListener('open', () => {
  console.log('Xirsys signaling connected');
});

socket.addEventListener('close', () => {
  // Reconnect with backoff after retrieving a fresh host and token.
});

Connection lifecycle messages can include peers, peer_connected, and peer_removed. Use them as presence signals, but design your application to tolerate disconnects, reconnects, and simultaneous state changes.

4. Send User Messages

A user message requires top-level t, m, and p fields:

JavaScriptJS
socket.send(JSON.stringify({
  t: 'u',
  m: {
    f: myPeerId,
    // t: remotePeerId, // [Optional] Direct messages only.
    o: 'offer'
  },
  p: {
    sdp: peerConnection.localDescription
  }
}));
Field Meaning
tUse "u" for an application user message.
m.fRequired sender metadata supplied by your application. It does not select the channel.
m.oRequired application operation, such as offer, answer, or candidate.
m.t[Optional] Add a remote peer ID for a direct message.
pApplication-defined payload.

The token determines channel membership. When m.t is omitted, Xirsys publishes the message to every other connected subscriber on the channel; the sender does not receive an echo. Use targeted messages for one-to-one WebRTC negotiation.

JavaScriptJS
socket.addEventListener('message', async event => {
  const message = JSON.parse(event.data);

  if (message.t !== 'u') return;

  switch (message.m?.o) {
    case 'offer':
      await handleOffer(message.p);
      break;
    case 'answer':
      await handleAnswer(message.p);
      break;
    case 'candidate':
      await handleCandidate(message.p);
      break;
  }
});

5. Ping/Pong Heartbeats

Use an application-level JSON ping to keep a signaling connection healthy. The server returns a pong only to the same socket:

HeartbeatText
client -> {"t":"ping","ts":1721846400000}
server -> {"t":"pong","ts":1721846400000}

The server echoes ts, which lets the client match the reply and measure round-trip time. The pong is a direct socket response and is not broadcast as a "u" message. Do not include m or p in heartbeat packets. Raw text ping or PING is also accepted and returns {"t":"pong"}, but timestamped JSON is recommended.

JavaScriptJS
const pingEveryMs = 30_000;
const pongTimeoutMs = 10_000;
const maxMissedPongs = 2;
let pendingPing = null;
let missedPongs = 0;
let pongTimer;

function sendPing() {
  if (socket.readyState !== WebSocket.OPEN || pendingPing !== null) return;

  pendingPing = Date.now();
  socket.send(JSON.stringify({ t: 'ping', ts: pendingPing }));

  pongTimer = setTimeout(() => {
    pendingPing = null;
    missedPongs += 1;
    if (missedPongs >= maxMissedPongs) {
      socket.close(4000, 'Heartbeat timeout');
    }
  }, pongTimeoutMs);
}

const pingTimer = setInterval(sendPing, pingEveryMs);

socket.addEventListener('message', event => {
  let message;
  try {
    message = JSON.parse(event.data);
  } catch {
    return;
  }

  if (message.t === 'pong' && message.ts === pendingPing) {
    clearTimeout(pongTimer);
    pendingPing = null;
    missedPongs = 0;
  }
});

socket.addEventListener('close', () => {
  clearInterval(pingTimer);
  clearTimeout(pongTimer);
});

A 30–60 second ping interval and reconnecting after two or three consecutive missed pongs are reasonable starting points. Tune the policy to your network and product requirements, and use exponential backoff with jitter when reconnecting.

Backend Configuration Example

This Node.js 18+ function retrieves both values after authenticating the application user and assigning a peer ID:

Node.js backendJS
async function getSignalConfig(channel, peerId) {
  const baseUrl = 'https://global.xirsys.net';
  const encodedChannel = encodeURIComponent(channel);
  const auth = Buffer.from(
    `${process.env.XIRSYS_IDENT}:${process.env.XIRSYS_SECRET}`
  ).toString('base64');
  const headers = { Authorization: `Basic ${auth}` };

  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 [hostData, tokenData] = await Promise.all([
    hostResponse.json(),
    tokenResponse.json()
  ]);

  if (
    !hostResponse.ok ||
    !tokenResponse.ok ||
    hostData.s !== 'ok' ||
    tokenData.s !== 'ok'
  ) {
    throw new Error('Unable to create Xirsys signaling configuration');
  }

  return { host: hostData.v, token: tokenData.v };
}
Questions about this page? Email experts@xirsys.com