API reference / Credentials

Credentials

POST /credentials/ice#

Creates a new TURN credential and returns it, with the STUN and TURN URLs, as an iceServers array you can pass straight to RTCPeerConnection.

text
POST https://turnix.io/api/v1/credentials/ice

Call this endpoint from your backend, once per new peer connection. See Credentials & TTL for the lifecycle.

Headers#

Header Required Description
Authorization Yes Bearer YOUR_API_TOKEN. See Authentication.
Content-Type With a body application/json or application/x-www-form-urlencoded
X-TURN-CLIENT-IP Recommended The end user's public IP address, used to choose the nearest region. Without it, Turnix uses X-Forwarded-For or the IP of your server. See Regions & geo-routing.

Parameters#

All parameters are optional and go in the request body.

Parameter Type Description
ttl integer Lifetime of the credential in seconds. Digits only. Default: 30. No minimum or maximum is enforced.
preferred_region string Region slug to use if it is online. If it is unknown, offline or not in your plan, Turnix falls back to GeoIP without an error.
fixed_region string Region slug that must be used. Fails with 422 if it is unknown or offline. Takes precedence over preferred_region.
room string Room label. Letters, digits, - and _ only.
initiator_client string Label of the participant that starts the connection. Same character rules.
receiver_client string Label of the answering participant. Same character rules.

Get region slugs from GET /regions.

Response#

200 OK

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"
}
Field Description
iceServers Array in the RTCIceServer format. Always two entries: STUN first, then TURN.
iceServers[1].username Unique TURN username (a UUID) for this credential
iceServers[1].credential TURN password for this credential
iceServers[1].urls TURN URLs on the selected region's host <region>.turnix.io: UDP and TCP on port 3478, and TLS on port 443. See hostnames and ports.
expiresAt When the credential stops being accepted for new relays (ISO 8601, UTC)

The selected region is the subdomain of the TURN URLs.

Examples#

Basic request#

bash
curl -X POST https://turnix.io/api/v1/credentials/ice \
  -H "Authorization: Bearer $TURNIX_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-TURN-CLIENT-IP: 203.0.113.42" \
  -d '{"ttl": 3600}'
javascript
const res = 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": userIp,
  },
  body: JSON.stringify({ ttl: 3600 }),
});
if (!res.ok) throw new Error(`Turnix ${res.status}: ${await res.text()}`);
const { iceServers, expiresAt } = await res.json();
python
import os, requests

res = requests.post(
    "https://turnix.io/api/v1/credentials/ice",
    headers={
        "Authorization": f"Bearer {os.environ['TURNIX_API_TOKEN']}",
        "X-TURN-CLIENT-IP": user_ip,
    },
    json={"ttl": 3600},
    timeout=10,
)
res.raise_for_status()
data = res.json()
ice_servers, expires_at = data["iceServers"], data["expiresAt"]

Choose a region#

bash
# Prefer us-west, fall back to the nearest region
curl -X POST https://turnix.io/api/v1/credentials/ice \
  -H "Authorization: Bearer $TURNIX_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ttl": 3600, "preferred_region": "us-west"}'

# Require us-west, fail if it is not available
curl -X POST https://turnix.io/api/v1/credentials/ice \
  -H "Authorization: Bearer $TURNIX_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ttl": 3600, "fixed_region": "us-west"}'
javascript
const res = await fetch("https://turnix.io/api/v1/credentials/ice", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TURNIX_API_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ ttl: 3600, preferred_region: "us-west" }), // or fixed_region
});
python
res = requests.post(
    "https://turnix.io/api/v1/credentials/ice",
    headers={"Authorization": f"Bearer {os.environ['TURNIX_API_TOKEN']}"},
    json={"ttl": 3600, "preferred_region": "us-west"},  # or fixed_region
    timeout=10,
)

Label clients and room#

bash
curl -X POST https://turnix.io/api/v1/credentials/ice \
  -H "Authorization: Bearer $TURNIX_API_TOKEN" \
  -d ttl=3600 \
  -d room=standup-2026-10-30 \
  -d initiator_client=user-1842 \
  -d receiver_client=user-77
javascript
const res = await fetch("https://turnix.io/api/v1/credentials/ice", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TURNIX_API_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    ttl: 3600,
    room: "standup-2026-10-30",
    initiator_client: "user-1842",
    receiver_client: "user-77",
  }),
});
python
res = requests.post(
    "https://turnix.io/api/v1/credentials/ice",
    headers={"Authorization": f"Bearer {os.environ['TURNIX_API_TOKEN']}"},
    json={
        "ttl": 3600,
        "room": "standup-2026-10-30",
        "initiator_client": "user-1842",
        "receiver_client": "user-77",
    },
    timeout=10,
)

Region selection#

  1. fixed_region if given (error if unknown or offline)
  2. preferred_region if given, known and online
  3. The online region nearest to the client IP (X-TURN-CLIENT-IP, then X-Forwarded-For, then the connection IP)
  4. The platform's default region if the IP cannot be located

On the free plan, only regions marked is_trial are considered. Details: Regions & geo-routing.

Errors#

Status Body Cause
401 {"error":"Not Authorized","code":"unauthorized"} Missing Authorization header or not a Bearer token
401 {"error":"Token not found","code":"unauthorized"} Unknown or removed token
401 {"error":"You are not on any subscription","code":401} The organization has no active plan
422 {"error":"TTL must be an integer"} ttl is not a whole, non-negative number
422 {"error":"room contains invalid characters","code":"unprocessable_entity"} room, initiator_client or receiver_client is empty or contains characters other than letters, digits, -, _ (the field name varies)
422 {"error":"Specified Region Not Found"} fixed_region is unknown, or not available on your plan
422 {"error":"Region Offline"} fixed_region is offline or in maintenance
422 {"error":"Error By limit calculation"} Usage could not be checked. Retry later; contact support if it persists.
429 see below The organization reached its data usage limit

429 Too Many Requests is returned when your plan has no overage and the traffic for the current billing period has reached the plan's allowance:

json
{
  "error": "Usage limit exceeded",
  "message": "You have reached your data usage limit",
  "limit": "15 GB",
  "used": "15.02 GB"
}

Retrying does not help. Upgrade the plan or wait for the next period. See Usage, limits & billing.

Last updated October 30, 2026