# 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](https://turnix.io/docs/concepts/credentials) for the lifecycle.

## Headers

| Header | Required | Description |
|--------|----------|-------------|
| `Authorization` | Yes | `Bearer YOUR_API_TOKEN`. See [Authentication](https://turnix.io/docs/api-reference/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](https://turnix.io/docs/concepts/regions#which-ip-is-used-for-geoip). |

## 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](https://turnix.io/docs/concepts/clients-rooms) 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`](https://turnix.io/docs/api-reference/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`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers) 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](https://turnix.io/docs/concepts/regions#region-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**

```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}'
```

**Node.js**

```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**

```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

**cURL**

```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"}'
```

**Node.js**

```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**

```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

**cURL**

```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
```

**Node.js**

```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**

```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](https://turnix.io/docs/concepts/regions).

## 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](https://turnix.io/docs/concepts/usage-limits).
