# Xirsys API quick reference

Use this page as a compact reference for the current TURN, signaling, channel, and
statistics APIs. All privileged requests belong on a trusted backend; never ship
your Xirsys secret in browser or mobile-client code.

## API base and authentication

Use the global API gateway or pin a regional gateway:

```text
https://global.xirsys.net
```

Authenticate with HTTP Basic authentication:

```text
Authorization: Basic base64(IDENT:SECRET)
```

Examples below use Node.js 18 or later and these server-side environment
variables:

```text
XIRSYS_IDENT
XIRSYS_SECRET
XIRSYS_CHANNEL
```

Channel names are case-sensitive complete identifiers. Use the exact channel
name configured in your Xirsys dashboard; do not build slash-delimited paths.

## Response envelope

Successful Xirsys API responses normally use this envelope:

```json
{
  "s": "ok",
  "v": {}
}
```

Check both the HTTP status and `s`. On failure, preserve the response body and
request details in your server logs, but redact credentials and tokens.

## TURN / ICE credentials

Request short-lived ICE credentials with `PUT /_turn/{channel}`:

```javascript
const baseUrl = 'https://global.xirsys.net';
const channel = process.env.XIRSYS_CHANNEL;
const encodedChannel = encodeURIComponent(channel);
const auth = Buffer.from(
  `${process.env.XIRSYS_IDENT}:${process.env.XIRSYS_SECRET}`
).toString('base64');

const response = await fetch(
  `${baseUrl}/_turn/${encodedChannel}?webrtc=1&expire=60`,
  {
    method: 'PUT',
    headers: {
      Authorization: `Basic ${auth}`
    }
  }
);
const data = await response.json();

if (!response.ok || data.s !== 'ok') {
  throw new Error(`Xirsys TURN request failed (${response.status})`);
}

const iceServers = data.v.iceServers;
```

`expire` is the credential lifetime in seconds. It defaults to 60 seconds and is
capped at 21,600 seconds (six hours). This lifetime controls how long the
credentials can authenticate a new TURN allocation; it does not set the duration
of an already established call.

`webrtc=1` returns the standard WebRTC array inside the Xirsys response
envelope. The ICE server object already contains `urls` as an array, so
`format=urls` is not needed:

```json
{
  "s": "ok",
  "v": {
    "iceServers": [
      {
        "username": "temporary-username",
        "credential": "temporary-credential",
        "urls": [
          "stun:xx-turn1.xirsys.com",
          "turn:xx-turn1.xirsys.com:80?transport=udp",
          "turn:xx-turn1.xirsys.com:3478?transport=udp",
          "turn:xx-turn1.xirsys.com:80?transport=tcp",
          "turn:xx-turn1.xirsys.com:3478?transport=tcp",
          "turns:xx-turn1.xirsys.com:443?transport=tcp",
          "turns:xx-turn1.xirsys.com:5349?transport=tcp"
        ]
      }
    ]
  }
}
```

Pass the returned array directly to WebRTC:

```javascript
const peerConnection = new RTCPeerConnection({ iceServers });
```

`format=urls` remains supported, but it returns a single ICE server object
at `v.iceServers`; wrap that object in an array before passing it to WebRTC.
Without a response-format parameter, the API returns the existing seven-object
legacy response. If both `webrtc=1` and `format=urls` are present, the result is
the same as `webrtc=1`; `format=urls` is redundant. With
`webrtc=0&format=urls`, the API returns the existing single object. Only
`webrtc=1` selects the standard array; `true`, `yes`, and `urls=1` do not.

### Geo-route for the end user

Normally, TURN selection reflects the location of the backend making the API
request. However, to provide the closest and fastest data center for your end
user, add `geo=1` and include the end user's public IP address as `user_ip`:

```javascript
const response = await fetch(
  `${baseUrl}/_turn/${encodedChannel}?webrtc=1&geo=1&expire=60`,
  {
    method: 'PUT',
    headers: {
      Authorization: `Basic ${auth}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      user_ip: trustedEndUserPublicIp
    })
  }
);
```

Derive `trustedEndUserPublicIp` from authenticated request context or a trusted
proxy chain. Do not accept an arbitrary IP address from an untrusted request
field. Both `geo=1` and a usable public `user_ip` are required. If the IP is
missing, private, or cannot be geolocated, Xirsys falls back to normal routing.
New Free accounts can use geo routing and all available regions during their
first 30 days. After the trial, Free accounts use their assigned home TURN
region even when a geo hint is provided. Paid accounts retain geo routing.

> **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.

## Signaling V2

Each peer needs a signaling host and a token. Fetch both from your backend and
return only those temporary values to the client.

### 1. Select a host

```javascript
const hostResponse = await fetch(`${baseUrl}/_host?type=signal`, {
  headers: { Authorization: `Basic ${auth}` }
});
const hostData = await hostResponse.json();

if (!hostResponse.ok || hostData.s !== 'ok') {
  throw new Error(`Xirsys host request failed (${hostResponse.status})`);
}

const signalingHost = hostData.v; // For example: wss://host:443/ws
```

### 2. Create a token

```javascript
const peerId = 'peer-123';
const tokenResponse = await fetch(
  `${baseUrl}/_token/${encodedChannel}` +
    `?k=${encodeURIComponent(peerId)}&expire=120`,
  {
    method: 'PUT',
    headers: { Authorization: `Basic ${auth}` }
  }
);
const tokenData = await tokenResponse.json();

if (!tokenResponse.ok || tokenData.s !== 'ok') {
  throw new Error(`Xirsys token request failed (${tokenResponse.status})`);
}

const token = tokenData.v;
```

Tokens default to a 60-second lifetime. `expire=0` creates a long-lived but still
finite token; prefer short-lived tokens and mint a fresh token when reconnecting.
The `k` value is the peer identifier within the channel and should be unique
among simultaneously connected peers.

### 3. Connect and send messages

```javascript
const socket = new WebSocket(
  `${signalingHost}/v2/${encodeURIComponent(token)}`
);

socket.addEventListener('open', () => {
  socket.send(JSON.stringify({
    t: 'u',
    m: {
      f: peerId,
      // t: remotePeerId, // [Optional] Direct messages only.
      o: 'offer'
    },
    p: {
      sdp: localOffer
    }
  }));
});
```

User packets require top-level `t`, `m`, and `p` fields. For a user message:

- `t` is `"u"`.
- `m.f` is required sender metadata supplied by your application.
- `m.o` is the application operation, such as `"offer"`, `"answer"`, or
  `"candidate"`.
- `m.t` is the optional target peer ID. Include it only for a direct message.
- `p` is the application payload.

Without `m.t`, the message is public to the token's channel and is delivered to
all other connected subscribers; the sender does not receive an echo. The
token, not `m.f`, determines channel membership.

### 4. Keep the WebSocket healthy

Use application-level JSON heartbeats to keep signaling connections healthy:

```text
client -> {"t":"ping","ts":1721846400000}
server -> {"t":"pong","ts":1721846400000}
```

The server echoes `ts`, replies only to the sending socket, and does not
broadcast the pong as a user message. Do not include `m` or `p` in a heartbeat
packet. Raw text `ping` or `PING` is also accepted and returns `{"t":"pong"}`,
but timestamped JSON is recommended.

A 30-60 second interval, a 10-15 second pong timeout, and reconnecting after two
or three missed pongs are reasonable starting points. On reconnect, use backoff
and retrieve a fresh host and token.

## Channels (`/_ns`)

Use `/_ns/{channel}` to create and delete a channel. Each channel is one
complete name, such as `myChannel` or `myOtherChannel`; forward slashes do not
designate channel hierarchy.

Create a channel:

```bash
curl --request PUT \
  --user "$XIRSYS_IDENT:$XIRSYS_SECRET" \
  "https://global.xirsys.net/_ns/myChannel"
```

List the account's complete channel names:

```bash
curl --user "$XIRSYS_IDENT:$XIRSYS_SECRET" \
  "https://global.xirsys.net/_ns?depth=100"
```

Delete a channel:

```bash
curl --request DELETE \
  --user "$XIRSYS_IDENT:$XIRSYS_SECRET" \
  "https://global.xirsys.net/_ns/myChannel"
```

Deleting a path is destructive. Resolve the exact target before issuing the
request.

## TURN and STUN statistics

Query account-wide statistics with `GET /_stats`. Add `/{channel}` to limit the
results to one channel. Select the measurement with `l` and use
colon-separated UTC dates.

Account-wide aggregate:

```bash
curl --user "$XIRSYS_IDENT:$XIRSYS_SECRET" \
  "https://global.xirsys.net/_stats?l=turn:sent&gs=2026:05:01&ge=2026:05:24"
```

Channel-specific grouped intervals:

```bash
curl --user "$XIRSYS_IDENT:$XIRSYS_SECRET" \
  "https://global.xirsys.net/_stats/myChannel?l=turn:sent&gs=2026:05:01&ge=2026:05:24&break=1"
```

Supported measurement names include:

```text
turn:session  session duration in seconds
turn:recv     received bytes
turn:sent     sent bytes
stun:session  session duration in seconds
```

`gs` and `ge` are the inclusive start and end dates of the requested period.
They accept only `YYYY:MM:DD`. `break=1` returns the period as grouped
intervals. Public reporting data is rolled up hourly, so very recent activity
may not appear immediately.

An aggregate includes these fields:

```json
{
  "count": 100,
  "max": 2000000,
  "min": 1000,
  "sum": 100000000
}
```

- `count` is the number of stat records included.
- `sum` is the total of their `v` values.
- `max` and `min` are the largest and smallest individual `v` values.

For `turn:recv` and `turn:sent`, `v` is bytes. This example totals 100,000,000
bytes (about 95.37 MiB), with an average of 1,000,000 bytes per sample. For
`turn:session` and `stun:session`, `v` is duration in seconds, so `sum` is total
session seconds and `count` is the number of session records.

`count` is the number of records reduced, not always a user-visible connection
count. The dashboard uses `count` for its STUN and TURN connections chart, but
uses `sum` for bandwidth and session-duration reporting.

## Security checklist

- Keep `IDENT` and `SECRET` on a trusted backend.
- Use HTTPS and WSS.
- Use short-lived TURN credentials and signaling tokens.
- Validate channel names, peer IDs, and any trusted proxy-derived IP address.
- Never place credentials in a URL or client bundle.
- Redact authorization headers, tokens, and TURN credentials from logs.
- Use static TURN credentials only in controlled infrastructure where they can
  be protected and rotated.

For expanded examples, use the documentation navigation for **TURN (ICE)**,
**Signaling**, **Channels**, **Statistics**, and **Secure API Calls**.
