Time-limited TURN credentials
Never ship a TURN password inside a web or mobile app: anyone can copy it and relay traffic on your account. Your backend asks us for credentials instead, and hands the result to the device.
How it works
Section titled “How it works”- Your backend calls
POST https://api.ngturn.io/v1/credentialswith your API key. - We mint a short-lived username and password and return them as a ready-made
iceServerslist. - Your backend passes that list to the device, which uses it to open a peer connection.
- The credential stops working for new connections once it expires. A call already running is not cut off.
The API key stays on your server and is never sent to a device. The credentials we return are safe to hand out: they expire on their own, they are tied to your account, and you can revoke the key behind them at any time.
Ask for credentials
Section titled “Ask for credentials”export async function turnCredentials({ ttl = 43200, region = 'auto' } = {}) {const res = await fetch('https://api.ngturn.io/v1/credentials', { method: 'POST', headers: { Authorization: `Bearer ${process.env.NGTURN_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ ttl, region }),});if (!res.ok) throw new Error(`ngTurn credentials failed: ${res.status}`);return res.json(); // { iceServers, ttl, expiresAt }}import os, requests
def turn_credentials(ttl: int = 43200, region: str = "auto"): res = requests.post( "https://api.ngturn.io/v1/credentials", headers={"Authorization": f"Bearer {os.environ['NGTURN_API_KEY']}"}, json={"ttl": ttl, "region": region}, timeout=10, ) res.raise_for_status() return res.json() # {"iceServers": [...], "ttl": ..., "expiresAt": ...}func TurnCredentials(ttl int, region string) (*CredentialsResponse, error) { payload, _ := json.Marshal(map[string]any{"ttl": ttl, "region": region}) req, _ := http.NewRequest("POST", "https://api.ngturn.io/v1/credentials", bytes.NewReader(payload)) req.Header.Set("Authorization", "Bearer "+os.Getenv("NGTURN_API_KEY")) req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("ngTurn credentials failed: %s", res.Status) } var out CredentialsResponse return &out, json.NewDecoder(res.Body).Decode(&out)}What comes back
Section titled “What comes back”{ "iceServers": [ { "urls": ["stun:relay.ngturn.io:3478"] }, { "urls": [ "turn:relay.ngturn.io:3478?transport=udp", "turn:relay.ngturn.io:3478?transport=tcp", "turns:relay.ngturn.io:443?transport=tcp" ], "username": "1790000000:acc_7Qm2x.key_Hk29d", "credential": "…" } ], "ttl": 43200, "expiresAt": "2026-10-01T12:00:00Z"}Pass iceServers straight into new RTCPeerConnection({ iceServers }). The list is already ordered
so the fastest path is tried first.
Options
Section titled “Options”| Field | Default | Notes |
|---|---|---|
ttl |
43200 (12 hours) |
Seconds. Maximum 24 hours |
region |
auto |
auto picks the nearest relay. eu or us pins every client to one region, for data residency or firewall allowlisting |
Make the lifetime longer than your longest expected call. A credential is checked when the connection is set up, so a call that starts inside the window runs to its end either way — but an ICE restart part-way through needs one that is still valid.
Reuse them
Section titled “Reuse them”Fetch once and reuse until roughly half the lifetime has passed, then fetch again. With the 12-hour default that is one call to us every six hours, not one per user and not one per connection.
When a relay goes away
Section titled “When a relay goes away”If a relay becomes unreachable mid-call, the connection does not recover on its own. Fetch fresh credentials and perform an ICE restart:
pc.addEventListener('connectionstatechange', async () => {if (pc.connectionState === 'failed') { const { iceServers } = await turnCredentials(); pc.setConfiguration({ iceServers }); await pc.restartIce();}});