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.
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
{
"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#
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}'
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();
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#
# 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"}'
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
});
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#
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
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",
}),
});
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#
fixed_regionif given (error if unknown or offline)preferred_regionif given, known and online- The online region nearest to the client IP (
X-TURN-CLIENT-IP, thenX-Forwarded-For, then the connection IP) - 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:
{
"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.