Getting started / Quickstart

Quickstart

In this guide you create an API token, request ICE credentials from your backend, and pass them to RTCPeerConnection. It takes about five minutes.

You need a Turnix account (the free plan is enough) and a backend that your WebRTC client already talks to.

Let your AI agent do the integration
Add Turnix (managed TURN/STUN servers) to the WebRTC app in this repository.

Context about Turnix:
- Endpoint: POST https://turnix.io/api/v1/credentials/ice
- Auth header: "Authorization: Bearer <token>". The token is a secret project API token. Read it from the environment variable TURNIX_API_TOKEN. Never send it to the browser or mobile client, never log it, never commit it.
- Optional body fields (JSON or form-encoded): ttl (integer seconds, digits only), room, initiator_client, receiver_client (letters, digits, "-" and "_" only), preferred_region or fixed_region (a region slug).
- Optional header X-TURN-CLIENT-IP: the end user's public IP. Set it, because otherwise Turnix picks the TURN region closest to the backend server instead of the user.
- Success (200) response: {"iceServers":[{"urls":["stun:stun.turnix.io:3478"]},{"username":"<uuid>","credential":"<hex>","urls":["turn:<region>.turnix.io:3478?transport=udp","turn:<region>.turnix.io:3478?transport=tcp","turns:<region>.turnix.io:443?transport=udp","turns:<region>.turnix.io:443?transport=tcp"]}],"expiresAt":"<ISO 8601 UTC timestamp>"}
- Errors are JSON with an "error" field: 401 (missing or unknown token), 422 (invalid parameter, unknown or offline region), 429 (the organization's data usage limit is reached; body also has "message", "limit" and "used").
- Docs: https://turnix.io/docs/api-reference/credentials

Do the following:
1. Inspect the repository. Find the backend framework, where the client creates RTCPeerConnection, and how the client already calls the backend (auth, fetch helper, base URL). Follow the existing conventions. If there is no backend, stop and ask me where the endpoint should live.
2. Backend: add an authenticated endpoint (for example GET /api/turn-credentials) that only signed-in users of this app can call. It calls the Turnix endpoint server-side with the token from TURNIX_API_TOKEN, forwards the caller's public IP in X-TURN-CLIENT-IP (respect the app's trusted-proxy setup when reading the IP), sends ttl=3600, and sends room/initiator_client when the app has a call or user identifier (sanitize to [A-Za-z0-9_-]). Use a 10-second timeout. Return only {"iceServers": [...], "expiresAt": "..."} to the client.
3. Error handling: if TURNIX_API_TOKEN is missing, fail at startup or return 500 with a clear log message. Map Turnix 401 to a 502 with a log line saying the token is invalid; pass 429 through as 503 with a "TURN usage limit reached" message; never include the token in errors or logs.
4. Frontend: before creating each RTCPeerConnection, fetch the new endpoint and pass iceServers into new RTCPeerConnection({ iceServers }). Keep any existing STUN servers only if the app needs them. If expiresAt has passed or is less than 60 seconds away when a new connection or ICE restart is needed, fetch fresh credentials first (for an ICE restart, call pc.setConfiguration({ ...pc.getConfiguration(), iceServers }) and then pc.restartIce()). Do not cache credentials in localStorage.
5. Configuration: add TURNIX_API_TOKEN to .env.example (or the project's equivalent) with an empty value and a comment, and document it in the README. Make sure .env is git-ignored.
6. Tests: add a backend test that mocks the Turnix API and checks that the endpoint sends the Bearer token and X-TURN-CLIENT-IP, returns iceServers, and does not expose the token. Then, if TURNIX_API_TOKEN is set in my environment, make one real call with curl to the Turnix endpoint and show me the status code and the number of iceServers (do not print the credentials).
7. Summarize the files you changed and anything I still need to do (for example, setting TURNIX_API_TOKEN in production).

1. Create a project#

A project groups your API tokens, credentials and statistics. Most teams use one project per app and environment, for example myapp-staging and myapp-production. Billing and usage limits are shared across all projects in your organization.

  1. Open Projects in the dashboard.
  2. Create a project and make it the active project.

2. Create an API token#

  1. Open API Tokens for the active project.
  2. Click Create new API Token, give it a name, and copy the token.
  3. Store it as a secret on your server, for example in the TURNIX_API_TOKEN environment variable.
Keep the token on your server

The token is shown only once, right after you create it. Anyone who has it can request credentials that count against your traffic. Never put it in browser code, mobile apps or public repositories. If it leaks, remove the token in the dashboard and create a new one.

3. Request ICE credentials from your backend#

Call POST /credentials/ice with the token. Pass the end user's IP in X-TURN-CLIENT-IP so Turnix picks the region closest to the user rather than to your server.

bash
curl -X POST https://turnix.io/api/v1/credentials/ice \
  -H "Authorization: Bearer $TURNIX_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ttl": 3600}'
javascript
// Express example, Node.js 18+ (built-in fetch)
app.get("/api/turn-credentials", requireLogin, async (req, res) => {
  const response = await fetch("https://turnix.io/api/v1/credentials/ice", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.TURNIX_API_TOKEN}`,
      "Content-Type": "application/json",
      "X-TURN-CLIENT-IP": req.ip, // the end user's public IP
    },
    body: JSON.stringify({ ttl: 3600 }),
  });

  if (!response.ok) {
    console.error("Turnix error", response.status, await response.text());
    return res.status(502).json({ error: "Could not get TURN credentials" });
  }

  const { iceServers, expiresAt } = await response.json();
  res.json({ iceServers, expiresAt });
});
python
# Flask example
import os
import requests
from flask import jsonify, request

@app.get("/api/turn-credentials")
@login_required
def turn_credentials():
    response = requests.post(
        "https://turnix.io/api/v1/credentials/ice",
        headers={
            "Authorization": f"Bearer {os.environ['TURNIX_API_TOKEN']}",
            "X-TURN-CLIENT-IP": request.remote_addr,  # the end user's public IP
        },
        json={"ttl": 3600},
        timeout=10,
    )
    if not response.ok:
        app.logger.error("Turnix error %s %s", response.status_code, response.text)
        return jsonify(error="Could not get TURN credentials"), 502

    data = response.json()
    return jsonify(iceServers=data["iceServers"], expiresAt=data["expiresAt"])

The response contains one STUN entry and one TURN entry with four transports:

json
{
  "iceServers": [
    { "urls": ["stun:stun.turnix.io:3478"] },
    {
      "username": "3f1c2b9e-8f4e-4d7a-9a51-2f6a0c7d1e42",
      "credential": "9b2f4c1d7e8a6b3c5d0e1f2a3b4c5d6e",
      "urls": [
        "turn:us-west.turnix.io:3478?transport=udp",
        "turn:us-west.turnix.io:3478?transport=tcp",
        "turns:us-west.turnix.io:443?transport=udp",
        "turns:us-west.turnix.io:443?transport=tcp"
      ]
    }
  ],
  "expiresAt": "2026-10-30T13:00:00.000Z"
}

4. Pass the ICE servers to RTCPeerConnection#

In the browser, fetch the credentials from your endpoint and create the peer connection with them:

javascript
const res = await fetch("/api/turn-credentials", { credentials: "include" });
if (!res.ok) throw new Error("Could not get TURN credentials");
const { iceServers } = await res.json();

const pc = new RTCPeerConnection({ iceServers });
// ...add tracks, create the offer and exchange SDP through your signaling as usual

Request fresh credentials for every new connection. They expire at expiresAt; see Credentials & TTL for how to choose a TTL and handle ICE restarts.

5. Check that TURN works#

  1. Open chrome://webrtc-internals (Chrome) or about:webrtc (Firefox) during a call.
  2. Look for ICE candidates of type relay with an address on <region>.turnix.io. If you see them, TURN works.
  3. To force the relay path for testing, create the connection with iceTransportPolicy: "relay":
javascript
const pc = new RTCPeerConnection({ iceServers, iceTransportPolicy: "relay" });

Credentials, TURN sessions and traffic for your project appear in the dashboard.

Troubleshooting#

Symptom Cause and fix
401 {"error":"Not Authorized"} The Authorization header is missing or does not start with Bearer.
401 {"error":"Token not found"} The token is wrong or was removed. Create a new one.
422 {"error":"TTL must be an integer"} Send ttl as whole seconds, digits only.
429 {"error":"Usage limit exceeded"} Your organization used its included traffic on a plan without overage (the free plan). Upgrade in Billing. See Usage, limits & billing.
No relay candidates The credentials expired before the connection started (use a longer ttl), or UDP and port 3478 are blocked. The turns:…:443?transport=tcp URL is the most firewall-friendly option; make sure you pass all URLs from the response.
Relay works but latency is high The region was chosen from your server's location. Send the user's IP in X-TURN-CLIENT-IP, or set preferred_region. See Regions & geo-routing.

Next steps#

Last updated October 30, 2026